সরাসরি পর্বে যান
পর্ব ৫২ · ৭০-এর মধ্যে · মডিউল ৯

Git-এর ভেতরে — blob, tree, commit আর content-addressed storage

Git Internals — Blobs, Trees, Commits, Plumbing and Content-Addressed Storage

Taught in Bangla, this lesson opens the .git folder and explains Git as a content-addressed key-value store. It computes object IDs by hand, reads blob, tree, commit and tag objects, follows HEAD to a file, builds a commit with plumbing commands only, explains deduplication, packfiles and delta chains, and discusses SHA-1 weaknesses and SHA-256 repositories, with real Git commands.

এই পর্বে যা শিখবেন

  • .git ফোল্ডারের প্রতিটি অংশ কী কাজ করে — HEAD, config, objects, refs, index, logs, hooks, packed-refs
  • একটি object ID হাতে হিসাব করা, আর ব্রাউজারেই পূর্ণ SHA-1 লিখে Git-এর আসল ID মিলিয়ে দেখা
  • blob, tree, commit আর tag-এর ভেতরের লেখা পড়া, আর একটি commit থেকে একটি ফাইলের বিষয়বস্তু পর্যন্ত পৌঁছানো
  • শুধু plumbing command দিয়ে একটি পূর্ণ commit বানানো
  • Deduplication, packfile, delta chain আর git verify-pack পড়া
  • SHA-1-এর দুর্বলতা, hardened SHA-1 আর SHA-256 repository — কী পারে, কী এখনো পারে না

১ · মডিউল ৯-এর শুরু — porcelain-এর নিচে কী আছে

এই কোর্সের মডিউল ২ আর ৩-এ আপনি প্রতিদিনের Git শিখেছেন: add, commit, branch, merge, rebase, push. এগুলো দিয়েই বেশিরভাগ মানুষ বছরের পর বছর কাজ চালিয়ে নেন। কিন্তু একদিন এমন কিছু ঘটে যা এই command-গুলো দিয়ে বোঝা যায় না — একটি commit “হারিয়ে” গেল, repository হঠাৎ কয়েক GB হয়ে গেল, merge অদ্ভুত ফল দিল, বা কেউ জিজ্ঞেস করলেন “Git-এর SHA-1 তো ভাঙা, তাহলে আমাদের code কি নিরাপদ?” তখন দরকার হয় Git-এর ভেতরটা চেনা।

মডিউল ৯ (“উন্নত Git আর automation”) ঠিক সেই কাজটি করবে। এই পর্বে আমরা ভিত তৈরি করব — Git ডিস্কে আসলে কী রাখে। তার ওপর পরের পর্বগুলো দাঁড়াবে:

🧱 পর্ব ৫২ · এই পর্ব

Object, hash, index, packfile — Git-এর ভাণ্ডারের নকশা।

git-internals

🛟 পর্ব ৫৩

Ref, reflog আর উদ্ধার — হারানো commit ফিরিয়ে আনা আর garbage collection.

refs, reflog, gc

🔀 পর্ব ৫৪–৫৫

Merge-এর ভেতরে (merge base, ort, diff3, rerere) আর Git দিয়ে bug খোঁজা (bisect, blame, log -S).

merge, bisect

🔧 পর্ব ৫৬–৫৮

শক্তিশালী হাতিয়ার (stash, worktree, hook), নিরাপত্তা (signed commit, secret) আর Continuous integration.

hooks, security, CI

Git-এর নিজের documentation command-গুলোকে দুই ভাগে ভাগ করে। প্রতিদিন যেগুলো ব্যবহার করেন, সেগুলো porcelain command — মানুষের জন্য বানানো, সুন্দর আউটপুট, অনেক কাজ একসাথে। আর নিচের স্তরে আছে plumbing command — ছোট, এক-কাজের, script-এ ব্যবহারের উপযোগী, যাদের আউটপুট version বদলালেও সাধারণত বদলায় না। নামটি বাথরুমের উপমা থেকে: ঝকঝকে চীনামাটির বেসিন (porcelain) চোখে পড়ে, আর দেয়ালের ভেতরে পাইপ (plumbing) আসল কাজ করে।

একটি পুরোনো সরকারি দপ্তরের রেকর্ড রুমের কথা ভাবুন। সামনের কাউন্টারে কর্মচারী আপনার আবেদন নিয়ে বলেন “জমা হয়ে গেছে” — এটি porcelain. কিন্তু ভেতরে আসলে কয়েকটি আলাদা কাজ হয়: কাগজটির একটি নম্বর দেওয়া, সেই নম্বরের তাকে রাখা, রেজিস্টার খাতায় এন্ট্রি, আর ফাইল-কভারে সূচিপত্র হালনাগাদ। আজ আমরা কাউন্টার পেরিয়ে রেকর্ড রুমে ঢুকব, আর প্রতিটি কাজ নিজের হাতে করব।

Porcelain (প্রতিদিন)ভেতরে যে plumbing কাজকী করে
git add fhash-object -w + update-indexবিষয়বস্তু object হিসেবে রাখা, index-এ নাম জোড়া
git commitwrite-tree + commit-tree + update-refindex থেকে tree, tree থেকে commit, branch সামনে সরানো
git show, git logcat-file, rev-list, rev-parseobject পড়া, commit-এর তালিকা, নাম থেকে ID
git switchread-tree, checkout-index, symbolic-reftree থেকে ফাইল সাজানো, HEAD বদলানো

এই পর্বের প্রতিটি “উদাহরণ” Windows-এ Git ২.৫৩ চালিয়ে একটি খালি scratch ফোল্ডারে নেওয়া। লেখক Rahim (rahim@example.com), সময় ঠিক করে দেওয়া — ১৫ জানুয়ারি ২০২৬, সকাল ১০টা (+০৬:০০) থেকে প্রতি commit-এ এক মিনিট করে, আর core.autocrlf false. তাই একই ফাইল, একই command আর একই সময় দিলে আপনি Linux, macOS বা Windows-এ হুবহু একই hash পাবেন। শুধু count-objects-এর KB-র ঘর ফাইল সিস্টেম-ভেদে আলাদা হতে পারে (নিচে বলা আছে)।

২ · .git ফোল্ডারের ভেতরে এক চক্কর

Repository মানে আসলে project-এর ভেতরের লুকানো .git ফোল্ডারটি। এটি মুছে ফেললে ফাইলগুলো থাকবে, কিন্তু সব ইতিহাস হারাবে; আবার শুধু এটি কপি করলেই পুরো ইতিহাস কপি হয়ে যায়। একটি একদম নতুন repository দিয়ে শুরু করি:

$ git -c init.defaultBranch=main init -q
$ ls -F .git
HEAD
config
description
hooks/
info/
objects/
refs/
$ cat .git/HEAD
ref: refs/heads/main
$ find .git/objects -type f
$ ls .git/hooks
applypatch-msg.sample
commit-msg.sample
…
pre-commit.sample
pre-push.sample
…
update.sample

নতুন repository-তে objects/ ফাঁকা — find একটি ফাইলও দেখায়নি। HEAD বলছে “আমি main-এ আছি”, যদিও main-এর কোনো ফাইল তখনো নেই (একে unborn branch বলে, পর্ব ১২-এ দেখেছেন)। hooks/-এ শুধু .sample ফাইল, তাই কোনো hook এখনো চালু নয়। এবার দুটি ফাইল (greeting.txt-এ hello, src/cart.js-এ let total = 0;) যোগ করে প্রথম commit করার পর দেখি:

$ ls -F .git
COMMIT_EDITMSG
HEAD
config
description
hooks/
index
info/
logs/
objects/
refs/
$ ls -F .git/refs .git/refs/heads .git/logs
.git/logs:
HEAD
refs/

.git/refs:
heads/
tags/

.git/refs/heads:
main

নতুন এসেছে তিনটি জিনিস: index (staging area-র ফাইল), logs/ (reflog) আর COMMIT_EDITMSG (শেষ commit বার্তার খসড়া)। আর refs/heads/main ফাইলটি এখন জন্মেছে। পুরো মানচিত্রটি নিচের সারণিতে।

জিনিসকী রাখেকোন পর্বে বিস্তারিত
HEADআপনি এখন কোথায়: সাধারণত ref: refs/heads/main, detached হলে সরাসরি একটি commit IDপর্ব ১২
configএই repository-র local সেটিং (remote, branch-এর upstream, core.*)পর্ব ১০
objects/সব object — খোলা (loose) ফাইল objects/ce/0136…, আর objects/pack/-এ packfileএই পর্ব
refs/heads/ (branch), tags/ (tag), remotes/ (remote-tracking branch) — প্রতিটি একটি ছোট ফাইলে একটি IDপর্ব ১২, ১৪, ৫৩
packed-refsঅনেক ref একটি ফাইলে গুছিয়ে রাখা (সাধারণত git gc-এর পরে আসে)এই পর্ব, ৫৩
indexপরের commit-এর প্রস্তুত তালিকা — path, mode, blob ID (binary ফাইল)পর্ব ৮, এই পর্ব
logs/Reflog: HEAD আর প্রতিটি branch কখন কোথা থেকে কোথায় সরেছেপর্ব ১১, ৫৩
hooks/নির্দিষ্ট ঘটনায় চলা script (pre-commit, commit-msg…); .sample মানে বন্ধপর্ব ৫৬
info/exclude, descriptionশুধু এই clone-এর ignore নিয়ম; GitWeb-এর জন্য বর্ণনাপর্ব ১০

.git-এর ভেতরের ফাইল হাতে সম্পাদনা বা মোছা শেখার জন্য একটি throwaway repository-তে ঠিক আছে, আসল project-এ নয়। একটি ভুল byte পুরো repository নষ্ট করতে পারে। আর ref পড়তে cat .git/refs/heads/main নয়, git rev-parse main ব্যবহার করুন — কারণ ref packed-refs-এ বা নতুন reftable ভাণ্ডারেও থাকতে পারে।

৩ · বিষয়বস্তুই ঠিকানা — object ID-র হিসাব

পর্ব ৭-এ (Version control কী) একটি সরল ৩২-bit hash দিয়ে আপনি content-addressed storage-এর ধারণা দেখেছেন। এবার আসল জিনিস। Git-এর object ভাণ্ডার একটি key–value store: value হলো বিষয়বস্তু, আর key হলো সেই বিষয়বস্তুর hash. আপনি নাম ঠিক করেন না; বিষয়বস্তুই নাম ঠিক করে।

দর্জির দোকানে প্রতিটি মাপের কাগজে দোকানদার একটি নম্বর লেখেন। কিন্তু ধরুন নম্বরটি তিনি নিজে বেছে নেন না — মাপগুলো (বুক ৩৮, কোমর ৩২, লম্বা ৪০…) একটি নির্দিষ্ট নিয়মে হিসাব করে নম্বর বের হয়। তাহলে একই মাপ দুবার এলে একই নম্বর হবে, আর কেউ চুপিচুপি একটি মাপ বদলালে নম্বর আর মিলবে না। Git-এর object ID ঠিক এই নম্বর, আর নিয়মটি হলো SHA-1.

নিয়মটি হুবহু এরকম। ধরুন বিষয়বস্তু hello আর একটি newline — মোট ৬ byte. Git hash করে এই byte-গুলো:

"blob" + " " + "6" + "\0" + "hello\n"
object ID = SHA-1 (header + বিষয়বস্তু)

অর্থাৎ প্রথমে object-এর ধরন (blob, tree, commit বা tag), একটি space, বিষয়বস্তুর আকার byte-এ (দশমিক অঙ্কে লেখা), একটি শূন্য byte (\0), তারপর বিষয়বস্তু। ফলাফলের ২০ byte-কে hexadecimal-এ লিখলে ৪০ অক্ষর। Git-এর hash-object command এই হিসাবটিই করে:

$ echo hello | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
$ printf hello | git hash-object --stdin
b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0
$ git add greeting.txt src/cart.js
$ find .git/objects -type f
.git/objects/65/70f20184cd4454ec84681d7683dc1fc5ddce35
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
$ git cat-file -t ce01362
blob
$ git cat-file -s ce01362
6
$ git cat-file -p ce01362
hello

echo শেষে newline বসায়, printf hello বসায় না — একটি byte-এর পার্থক্যে পুরো ID বদলে গেল। hash-object -w ছাড়া শুধু হিসাব করে, কিছু লেখে না। git add কিন্তু লেখে: objects/-এ দুটি ফাইল এলো। ফাইলের নাম ID-র প্রথম ২ অক্ষর ফোল্ডার, বাকি ৩৮ অক্ষর ফাইলের নাম — যাতে একটি ফোল্ডারে লক্ষ লক্ষ ফাইল না জমে। cat-file হলো object পড়ার plumbing: -t ধরন, -s আকার, -p সুন্দর করে বিষয়বস্তু। খেয়াল করুন, পুরো ৪০ অক্ষর না দিয়ে ce01362 লিখলেও চলেছে — যতক্ষণ ছোট রূপটি repository-তে একটিমাত্র object-কে বোঝায়।

এবার এই ID নিজে হিসাব করি — কোনো লাইব্রেরি ছাড়া। নিচের কোডে SHA-1 পুরোটাই লেখা (FIPS 180-4 মান অনুযায়ী padding, ৮০ ধাপের ঘূর্ণন)। শুরুতে মানক পরীক্ষা sha1("abc") মিলিয়ে নেওয়া হয়েছে, যাতে বোঝা যায় implementation ঠিক আছে:

// SHA-1 (FIPS 180-4) written out in full, so we can compute Git's object IDs by hand.
function sha1(bytes) {                       // bytes: array of numbers 0..255
  const m = [...bytes, 0x80], bits = bytes.length * 8;
  while (m.length % 64 !== 56) m.push(0);     // pad to 56 bytes mod 64
  for (let i = 7; i >= 0; i--) m.push(Math.floor(bits / 2 ** (8 * i)) & 255);
  const rol = (x, n) => (x << n) | (x >>> (32 - n));
  let h = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
  for (let o = 0; o < m.length; o += 64) {    // one 64-byte block at a time
    const w = [];
    for (let i = 0; i < 16; i++) w[i] = (m[o+4*i] << 24) | (m[o+4*i+1] << 16) | (m[o+4*i+2] << 8) | m[o+4*i+3];
    for (let i = 16; i < 80; i++) w[i] = rol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
    let [a, b, c, d, e] = h;
    for (let i = 0; i < 80; i++) {
      const f = i < 20 ? (b & c) | (~b & d) : i < 40 || i >= 60 ? b ^ c ^ d : (b & c) | (b & d) | (c & d);
      const k = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6][Math.floor(i / 20)];
      const t = (rol(a, 5) + f + e + k + w[i]) | 0;
      e = d; d = c; c = rol(b, 30); b = a; a = t;
    }
    h = h.map((x, i) => (x + [a, b, c, d, e][i]) | 0);
  }
  return h.map(x => (x >>> 0).toString(16).padStart(8, "0")).join("");
}
const utf8 = s => [...new TextEncoder().encode(s)];
// Git's rule: id = SHA-1( "blob " + <size in BYTES> + "\0" + <content> )
function blobId(text) {
  const body = utf8(text);
  return sha1([...utf8("blob " + body.length + "\0"), ...body]);
}
console.log("sha1('abc')     =", sha1(utf8("abc")));   // standard test vector
for (const s of ["hello\n", "hello", "", "সালাম\n"])
  console.log(JSON.stringify(s).padEnd(10), utf8(s).length + " bytes ->", blobId(s));

আসল আউটপুট:

sha1('abc')     = a9993e364706816aba3e25717850c26c9cd0d89d
"hello\n"  6 bytes -> ce013625030ba8dba906f756967f9e9ca394464a
"hello"    5 bytes -> b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0
""         0 bytes -> e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
"সালাম\n"  16 bytes -> de9867bbc5ecda5933a3e75facb3e8117b72ec25

প্রথম লাইনটি SHA-1-এর প্রকাশিত মানক উত্তর (a9993e36…), তাই হাতে লেখা implementation ঠিক। তারপর "hello\n"-এর ID হুবহু ce013625030b… — ওপরের git hash-object-এর সাথে অক্ষরে অক্ষরে এক। "hello" (newline ছাড়া) দিল b6fc4c6…, সেটিও Git-এর সাথে মেলে। খালি ফাইলের blob e69de29… — অনেক repository-তে এই ID দেখবেন, কারণ সব খালি ফাইল একই blob. আর বাংলা “সালাম” ও newline: অক্ষর ৫টি দেখালেও UTF-8-এ ১৬ byte, তাই header-এ লেখা হয় blob 16. Git অক্ষর গোনে না, byte গোনে; Git ২.৫৩-এ git hash-object-ও de9867b… দিয়েছে।

ডিস্কে একটি loose object দেখতে কেমন

যে object এখনো কোনো packfile-এ ঢোকেনি, তাকে বলে loose object: objects/ce/0136… ফাইলটিতে header আর বিষয়বস্তু zlib দিয়ে সংকুচিত করে রাখা। আমাদের repository-তে ফাইলটির আকার ২১ byte (wc -c দিয়ে মাপা)। Python-এর standard library দিয়ে একই কাজ করি:

# How Git stores one loose object: header + content -> SHA-1 name -> zlib bytes on disk
import hashlib
import zlib

content = b"hello\n"
store = b"blob %d\x00" % len(content) + content      # "<type> <size>\0<content>"
oid = hashlib.sha1(store).hexdigest()

print("bytes hashed :", store)
print("object id    :", oid)
print("loose path   : .git/objects/%s/%s" % (oid[:2], oid[2:]))

# Git compresses loose objects with zlib; default level (core.looseCompression) is 1
packed = zlib.compress(store, 1)
print("on-disk size :", len(packed), "bytes")
print("on-disk hex  :", packed.hex())
print("round trip ok:", zlib.decompress(packed) == store)

# The same object in a SHA-256 repository gets a longer, different name
print("sha256 id    :", hashlib.sha256(store).hexdigest())

# Changing one byte changes the whole name (avalanche effect)
other = b"blob 6\x00hellO\n"
print("hellO id     :", hashlib.sha1(other).hexdigest())

আসল আউটপুট:

bytes hashed : b'blob 6\x00hello\n'
object id    : ce013625030ba8dba906f756967f9e9ca394464a
loose path   : .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
on-disk size : 21 bytes
on-disk hex  : 78014bcac94f523063c848cdc9c9e702001dc50414
round trip ok: True
sha256 id    : 2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4
hellO id     : 4b32b59cf6f008703c95a6d2284f027e6ef86b54

হাতে বানানো ২১ byte আর Git-এর লেখা ফাইলের ২১ byte এই উদাহরণে হুবহু এক — শুরুর 78 01 হলো zlib-এর header, যেখানে 01 বলে “দ্রুততম সংকোচন”। Git-এর core.looseCompression না দিলে loose object-এর জন্য সাধারণত level 1 ব্যবহার হয়। তবে zlib-এর version বা সেটিং বদলালে সংকুচিত byte আলাদা হতে পারে; ID কখনো বদলায় না, কারণ ID হিসাব হয় সংকোচনের আগের byte থেকে। শেষ দুই লাইন দুটি জিনিস দেখায়: একই object SHA-256 repository-তে সম্পূর্ণ আলাদা, ৬৪ অক্ষরের নাম পায়; আর একটি অক্ষর (o → O) বদলালেই নাম চেনার অযোগ্যভাবে বদলে যায় — একে বলে avalanche effect.

মূল ধারণা

নাম = বিষয়বস্তুর ছাপ, তাই তিনটি জিনিস বিনা খরচে পাওয়া যায়: (১) অখণ্ডতা — object পড়ার সময় আবার hash করে মিলিয়ে নিলেই বোঝা যায় ডিস্কে কিছু নষ্ট হয়েছে কি না (git fsck এটিই করে); (২) deduplication — একই বিষয়বস্তু যতবার, যত নামে, যত ফোল্ডারেই থাকুক, object একটিই; (৩) অপরিবর্তনীয়তা — object “সম্পাদনা” করা যায় না, বদলালে সেটি নতুন নামের নতুন object. Hash table-এর মৌলিক ধারণা আর collision-এর গণিত দেখতে পারেন DSA কোর্সের hash table পর্বে; cryptographic hash কেন সাধারণ hash-এর চেয়ে কঠিন শর্ত মানে, তা Computer Networks কোর্সের cryptography পর্বে।

৪ · চার ধরনের object — blob, tree, commit আর tag

পর্ব ৮-এ (প্রথম Git repository) git cat-file -p HEAD দিয়ে একটি commit-এর ভেতরে উঁকি দিয়েছিলেন। এবার চার ধরনই খুলে দেখি। আমাদের repository-তে এখন দুটি commit: প্রথমটি “Add greeting and cart”, দ্বিতীয়টিতে (bd9d66d) যোগ হয়েছে docs/hello-copy.txt (আবার সেই hello) আর চালানো যায় এমন একটি script run.sh, আর তার ওপর একটি annotated tag v1.0.

Blob — শুধু বিষয়বস্তু

Blob-এ থাকে শুধু ফাইলের byte. ফাইলের নাম নেই, permission নেই, তৈরির সময় নেই। এজন্যই greeting.txt আর docs/hello-copy.txt — দুটি আলাদা নাম, আলাদা ফোল্ডার — একই blob ce01362 ব্যবহার করে। নাম কোথায় থাকে? Tree-তে।

Tree — একটি ফোল্ডারের সূচিপত্র

$ git cat-file -p 'HEAD^{tree}'
040000 tree 9999772157e6638b8b328b65ddf169040665ef85	docs
100644 blob ce013625030ba8dba906f756967f9e9ca394464a	greeting.txt
100755 blob 5bd7bd58778e6f16e1d1c147693b9abb354ecf34	run.sh
040000 tree be0cf5e51603159fd0863b82dca114773e4c29d4	src
$ git cat-file -p 9999772
100644 blob ce013625030ba8dba906f756967f9e9ca394464a	hello-copy.txt
$ git ls-tree HEAD~1 src
040000 tree be0cf5e51603159fd0863b82dca114773e4c29d4	src
$ git cat-file tree 7d0e48c | xxd
00000000: 3130 3036 3434 2067 7265 6574 696e 672e  100644 greeting.
00000010: 7478 7400 ce01 3625 030b a8db a906 f756  txt...6%.......V
00000020: 967f 9e9c a394 464a 3430 3030 3020 7372  ......FJ40000 sr
00000030: 6300 be0c f5e5 1603 159f d086 3b82 dca1  c...........;...
00000040: 1477 3e4c 29d4                           .w>L).

একটি tree হলো একটি ফোল্ডারের তালিকা: প্রতিটি লাইনে mode, ধরন, ID আর নাম, নাম অনুযায়ী সাজানো। উপ-ফোল্ডার src নিজেই আরেকটি tree. এভাবে tree-র ভেতরে tree মিলে পুরো project-এর গাছ। HEAD~1 src দেখাচ্ছে আগের commit-এর src-ও be0cf5e — ফোল্ডারটি বদলায়নি, তাই নতুন commit একই tree আবার ব্যবহার করেছে, কিছু কপি হয়নি। শেষ command-টি tree-র কাঁচা byte দেখায় (-p ছাড়া, ধরন বলে দিয়ে): "100644 greeting.txt", একটি 00 byte, তারপর ID-টি ৪০ অক্ষরের লেখা হিসেবে নয়, ২০ byte-এর binary হিসেবে (ce01 3625 …)। আর ফোল্ডারের mode ডিস্কে লেখা 40000 — সামনের শূন্য ছাড়া; ls-tree শুধু দেখানোর সময় 040000 করে সাজায়। মোট ৭০ byte, যা cat-file -s 7d0e48c-এর আকারের সাথে মেলে।

Modeধরনমানে
100644blobসাধারণ ফাইল
100755blobচালানো যায় এমন ফাইল (executable bit); Git শুধু এই একটি permission মনে রাখে
120000blobSymbolic link — blob-এ থাকে লক্ষ্য-ফাইলের path, আসল বিষয়বস্তু নয়
040000treeউপ-ফোল্ডার (ডিস্কে 40000)
160000commitGitlink — submodule-এর একটি নির্দিষ্ট commit-এর ID; object-টি এই repository-তে থাকে না (পর্ব ১৯)

খেয়াল করুন, Git খালি ফোল্ডার রাখতে পারে না: tree তৈরি হয় ফাইলের তালিকা থেকে, আর খালি ফোল্ডারে কোনো entry নেই। এজন্যই মানুষ খালি ফোল্ডারে .gitkeep নামের একটি খালি ফাইল রাখেন — নামটি শুধু একটি রীতি, Git-এর কোনো বিশেষ নিয়ম নয়।

Commit আর annotated tag

$ git cat-file -p HEAD
tree f32d1ce7e4f75215b9768171448a5c8ecfd38b0d
parent 17f23e62f1fb909f900e5bd02d6e717a5e6a4569
author Rahim <rahim@example.com> 1768449660 +0600
committer Rahim <rahim@example.com> 1768449660 +0600

Add copy and run script
$ git tag -a v1.0 -m "First release"
$ git cat-file -t v1.0
tag
$ git cat-file -p v1.0
object bd9d66df7d58229921e670ab68ac9471f51750cb
type commit
tag v1.0
tagger Rahim <rahim@example.com> 1768449720 +0600

First release
$ git rev-parse v1.0 'v1.0^{commit}'
0b5a053c1addf8d449d3b9232eeda4ecac173c31
bd9d66df7d58229921e670ab68ac9471f51750cb

Commit object একটি সাধারণ লেখা: ঠিক একটি tree লাইন (পুরো project-এর snapshot), শূন্য বা বেশি parent লাইন (প্রথম commit-এ নেই, merge commit-এ দুই বা বেশি), author (কে লিখেছেন), committer (কে commit বানিয়েছেন) — প্রতিটির সাথে Unix সময় (১৯৭০ থেকে সেকেন্ড; 1768449660 মানে ১৫ জানুয়ারি ২০২৬, ১০:০১ +০৬:০০) — তারপর একটি খালি লাইন আর বার্তা। Signed commit-এ committer-এর পরে একটি gpgsig header-ও থাকে (পর্ব ৫৭)। Annotated tag আলাদা একটি object (0b5a053): কোন object, তার ধরন, tag-এর নাম, কে কখন বানালেন আর বার্তা। v1.0^{commit} মানে “tag খুলে ভেতরের commit-এ পৌঁছানো” — তাই দ্বিতীয় লাইনে commit-এর ID. Lightweight tag কিন্তু কোনো object নয়, শুধু refs/tags/-এ একটি ref (পর্ব ১৮)।

সব object একসাথে, ছবিতে। প্রতিটি বাক্সে আসল ছোট ID; তীর মানে “এই ID-টি ভেতরে লেখা আছে”:

দুই commit-এর object graph — কে কাকে দেখায়

Tag → commit → tree → blobs and sub-trees, with real short IDs; unchanged objects are shared

Git object graph: tag, commit, tree, blob tag v1.0 0b5a053 commit bd9d66d commit 17f23e6 object parent tree f32d1ce docs/ greeting.txt run.sh src/ tree 7d0e48c greeting.txt src/ tree 9999772 hello-copy.txt blob ce01362 hello blob 5bd7bd5 echo run tree be0cf5e cart.js blob 6570f20 let total = 0; সাধারণ তীর: নতুন commit bd9d66d-এর গাছ গোলাপি ড্যাশ: পুরোনো 17f23e6 — একই object আবার
ছবি ৫২.১ — ৯টি object দিয়ে দুটি পূর্ণ snapshot. পুরোনো commit-এর tree 7d0e48c নিজের ফাইল কপি করেনি: তার greeting.txt আর src/ একই ce01362 আর be0cf5e-কে দেখায়। ID যেহেতু বিষয়বস্তু থেকে আসে, এই ভাগাভাগি আপনাআপনি ঘটে। তীর শুধু নতুন থেকে পুরোনোর দিকে যায় — কোনো object তার পরের object-কে চেনে না।

এই কাঠামোকে বলে Merkle tree (আরও নিখুঁতভাবে Merkle DAG): প্রতিটি node-এর নাম তার সন্তানদের নামসহ বিষয়বস্তুর hash. ফলে একটি commit ID পুরো ইতিহাসের ছাপ — কোথাও একটি blob-এর একটি byte বদলালে সেই blob, তার tree, ওপরের সব tree, commit আর পরের সব commit-এর ID বদলে যায়। পর্ব ১৫-এ (Rebase) দেখেছেন rebase কেন “নতুন commit” বানায়; কারণ এটিই — parent বদলালে commit-এর লেখা বদলায়, তাই নাম বদলায়।

৫ · git add আর git commit আসলে কী লেখে

পর্ব ৮-এ তিনটি এলাকা শিখেছেন: working tree, staging area (index) আর repository. এবার দেখি দুটি porcelain command .git-এর ঠিক কোন কোন ফাইলে কী লেখে। “পরে” চাপুন:

.git-এর ভেতরে (ID ছোট করে)
শেষ কাজ
objects/ (নতুন)
index
refs/heads/main
logs/HEAD
  1. শুরু। Working tree-তে দুটি ফাইল: greeting.txt (hello) আর src/cart.js. Git এখনো এদের কিছুই জানে না — objects/ খালি, index ফাইলই নেই, আর HEAD যে main-এর কথা বলে তার ফাইলও নেই।
  2. git add. দুটি কাজ: প্রতিটি ফাইলের বিষয়বস্তু blob হিসেবে objects/-এ লেখা (hash-object -w-এর মতো), আর index-এ path → blob ID জোড়া লেখা (update-index-এর মতো)। অর্থাৎ add-এর মুহূর্তেই বিষয়বস্তু ভাণ্ডারে চলে যায় — commit না করলেও। পরে ফাইল আবার বদলে add করলে পুরোনো blob-টি কোথাও থেকে পৌঁছানো যায় না এমন object হয়ে পড়ে থাকে (পর্ব ৫৩-এ এর উদ্ধার আর পরিষ্কার)।
  3. Commit, ধাপ ক — tree লেখা। Index-এর সাজানো তালিকা থেকে Git প্রতিটি ফোল্ডারের tree বানায়, নিচ থেকে ওপরে: আগে src/-এর tree be0cf5e, তারপর root tree 7d0e48c, যার ভেতরে src-এর ID. এটি write-tree-এর কাজ।
  4. Commit, ধাপ খ — commit object. Root tree-র ID, parent (প্রথম commit-এ নেই), author, committer, সময় আর বার্তা মিলিয়ে লেখা তৈরি করে hash করা হলো: 17f23e6. এটি commit-tree-এর কাজ। এখনো কোনো branch এটিকে চেনে না।
  5. Commit, ধাপ গ — branch সরানো। HEAD বলে refs/heads/main, তাই Git সেই ফাইলে 17f23e6 লেখে (update-ref), আর logs/HEAD ও logs/refs/heads/main-এ এক লাইন যোগ করে — শূন্যে ভরা ID থেকে 17f23e6, “commit (initial)”. মোট নতুন object: ২ blob + ২ tree + ১ commit = ৫টি।

Index — একটি সাজানো binary তালিকা

$ xxd -l 12 .git/index
00000000: 4449 5243 0000 0002 0000 0002            DIRC........
$ git ls-files --stage
100644 ce013625030ba8dba906f756967f9e9ca394464a 0	docs/hello-copy.txt
100644 ce013625030ba8dba906f756967f9e9ca394464a 0	greeting.txt
100755 5bd7bd58778e6f16e1d1c147693b9abb354ecf34 0	run.sh
100644 6570f20184cd4454ec84681d7683dc1fc5ddce35 0	src/cart.js

Index ফাইলের প্রথম ১২ byte (প্রথম git add-এর পরে): DIRC (“dircache”, Git-এর প্রথম দিকের নাম), version 2, আর entry-সংখ্যা 2. তারপর প্রতিটি entry-তে ফাইলের সময়, আকার, mode, blob ID আর path — path অনুযায়ী সাজানো, ফোল্ডার নয়, পূর্ণ path. দ্বিতীয় commit-এর পরে ls-files --stage-এর চার লাইন দেখুন: দুটি path, একই blob. তৃতীয় কলামের 0 হলো stage নম্বর; merge conflict-এর সময় একই path-এর ১, ২, ৩ নম্বর stage-এ তিনটি version থাকে — পর্ব ৫৪-এ (Merge-এর ভেতরে) এর ব্যবহার। Index-এ ফাইলের সময় আর আকার রাখার কারণ গতি: git status প্রতিটি ফাইল আবার hash না করে আগে এগুলো মিলিয়ে দেখে।

এবার আসল পরীক্ষা: commit ID-ও কি হাতে বানানো যায়? নিচের কোডটি প্রথম commit-এর দুটি blob, দুটি tree আর commit-এর লেখা byte ধরে ধরে বানিয়ে একই SHA-1 দিয়ে hash করে। Tree-র entry-তে ID ২০ byte-এর binary, আর ফোল্ডারের mode 40000 — ঠিক ওপরের xxd-এ যেমন দেখেছেন:

// Same full SHA-1 as before (compacted); now rebuild tree and commit objects byte by byte.
function sha1(bytes) {
  const m = [...bytes, 0x80], bits = bytes.length * 8;
  while (m.length % 64 !== 56) m.push(0);
  for (let i = 7; i >= 0; i--) m.push(Math.floor(bits / 2 ** (8 * i)) & 255);
  const rol = (x, n) => (x << n) | (x >>> (32 - n));
  let h = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
  for (let o = 0; o < m.length; o += 64) {
    const w = [];
    for (let i = 0; i < 16; i++) w[i] = (m[o+4*i] << 24) | (m[o+4*i+1] << 16) | (m[o+4*i+2] << 8) | m[o+4*i+3];
    for (let i = 16; i < 80; i++) w[i] = rol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
    let [a, b, c, d, e] = h;
    for (let i = 0; i < 80; i++) {
      const f = i < 20 ? (b & c) | (~b & d) : i < 40 || i >= 60 ? b ^ c ^ d : (b & c) | (b & d) | (c & d);
      const t = (rol(a, 5) + f + e + [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6][i / 20 | 0] + w[i]) | 0;
      e = d; d = c; c = rol(b, 30); b = a; a = t;
    }
    h = h.map((x, i) => (x + [a, b, c, d, e][i]) | 0);
  }
  return h.map(x => (x >>> 0).toString(16).padStart(8, "0")).join("");
}
const utf8 = s => [...new TextEncoder().encode(s)];
const raw = hex => hex.match(/../g).map(x => parseInt(x, 16));      // 40 hex -> 20 bytes
const obj = (type, body) => sha1([...utf8(type + " " + body.length + "\0"), ...body]);
const entry = (mode, name, id) => [...utf8(mode + " " + name + "\0"), ...raw(id)];
const greeting = obj("blob", utf8("hello\n")), cart = obj("blob", utf8("let total = 0;\n"));
const src = obj("tree", entry("100644", "cart.js", cart));          // the folder src/
const root = obj("tree", [...entry("100644", "greeting.txt", greeting), ...entry("40000", "src", src)]);
const who = "Rahim <rahim@example.com> 1768449600 +0600";            // 2026-01-15 10:00 +06:00
const text = `tree ${root}\nauthor ${who}\ncommitter ${who}\n\nAdd greeting and cart\n`;
console.log("blob greeting.txt :", greeting);
console.log("blob src/cart.js  :", cart);
console.log("tree src/         :", src);
console.log("tree (root)       :", root);
console.log("commit            :", obj("commit", utf8(text)));

আসল আউটপুট:

blob greeting.txt : ce013625030ba8dba906f756967f9e9ca394464a
blob src/cart.js  : 6570f20184cd4454ec84681d7683dc1fc5ddce35
tree src/         : be0cf5e51603159fd0863b82dca114773e4c29d4
tree (root)       : 7d0e48c11379f4439db90c8123902e9bec32e59e
commit            : 17f23e62f1fb909f900e5bd02d6e717a5e6a4569

পাঁচটি ID-ই আসল repository-র সাথে অক্ষরে অক্ষরে মিলেছে: blob ce01362 ও 6570f20, tree be0cf5e ও 7d0e48c, আর commit 17f23e6. অর্থাৎ commit ID-র পেছনে কোনো গোপন উপাদান নেই — tree, parent, নাম, email, সময়, timezone আর বার্তা। এর যেকোনো একটি বদলালে ID বদলাবে: +0600-এর বদলে +0530, বার্তার শেষে newline না রাখা, বা author-এর বানানে একটি অক্ষর। এজন্যই git commit --amend দিয়ে শুধু বার্তা বদলালেও নতুন commit ID হয়, কিন্তু tree ID একই থাকে।

৬ · HEAD থেকে একটি ফাইল পর্যন্ত — নাম ধরে ধরে খোঁজা

“HEAD-এ src/cart.js-এ কী লেখা?” — এই প্রশ্নের উত্তর দিতে Git কয়েকটি লাফ দেয়। git show HEAD:src/cart.js বা git rev-parse HEAD:src/cart.js এক লাইনে করে, কিন্তু ভেতরে প্রতিটি লাফ আলাদা। পর্ব ১২-এ (Branch) প্রথম দুটি লাফ দেখেছেন; এবার পুরো পথ:

  1. HEAD.git/HEADsymbolic ref
  2. branchmainref ফাইল
  3. commitbd9d66dsnapshot + ইতিহাস
  4. root treef32d1ceproject-এর মূল ফোল্ডার
  5. tree src/be0cf5eউপ-ফোল্ডার
  6. blob6570f20বিষয়বস্তু
$ cat .git/HEADHEAD একটি নাম রাখে
ref: refs/heads/main
$ git rev-parse mainbranch একটি ID রাখে
bd9d66df7d58229921e670ab68ac9471f51750cb
$ git cat-file -p bd9d66dcommit-এর প্রথম লাইনে tree
tree f32d1ce7e4f75215b9768171448a5c8ecfd38b0d
parent 17f23e62f1fb909f900e5bd02d6e717a5e6a4569
author Rahim <rahim@example.com> 1768449660 +0600
committer Rahim <rahim@example.com> 1768449660 +0600

Add copy and run script
$ git cat-file -p f32d1ceনাম “src” খুঁজি
040000 tree 9999772…	docs
100644 blob ce01362…	greeting.txt
100755 blob 5bd7bd5…	run.sh
040000 tree be0cf5e51603159fd0863b82dca114773e4c29d4	src
$ git cat-file -p be0cf5eনাম “cart.js” খুঁজি
100644 blob 6570f20184cd4454ec84681d7683dc1fc5ddce35	cart.js
$ git cat-file -p HEAD:src/cart.jsঅবশেষে বিষয়বস্তু
let total = 0;
  1. HEAD পড়া। .git/HEAD-এ কোনো ID নেই, আছে একটি branch-এর নাম: refs/heads/main. HEAD detached হলে এখানে সরাসরি একটি commit ID থাকত, আর পরের ধাপটি বাদ পড়ত।
  2. Ref থেকে ID. main-এর ref (ফাইলে বা packed-refs-এ) বলে bd9d66d…. Script-এ এই দুই ধাপ একসাথে করে git rev-parse HEAD.
  3. Commit খোলা। Commit object-এর প্রথম লাইন tree f32d1ce… — এই commit-এর পুরো snapshot. Parent লাইনটি এই খোঁজায় লাগে না; সেটি ইতিহাসে পেছনে হাঁটার জন্য। এক লাইনে: git rev-parse 'HEAD^{tree}'.
  4. Root tree-তে নাম খোঁজা। Path-এর প্রথম অংশ src. Tree-র তালিকায় নামটি পাওয়া গেল, mode 040000 — অর্থাৎ আরেকটি tree, ID be0cf5e. এক লাইনে: git rev-parse HEAD:src.
  5. উপ-ফোল্ডারে নাম খোঁজা। Path-এর পরের অংশ cart.js → blob 6570f20. এক লাইনে: git rev-parse HEAD:src/cart.js. লক্ষ করুন, working tree-র src/cart.js ফাইলে git hash-object src/cart.js চালালেও একই 6570f20 আসে, কারণ ফাইলটি বদলায়নি — git status “clean” বলার মূল কারণ এটিই।
  6. Blob খোলা। Blob-এর বিষয়বস্তু let total = 0;. পুরো পথে একটিও “খোঁজা” ধীর নয়: প্রতিটি লাফ একটি ID দিয়ে সরাসরি একটি object খোলা, আর tree-র তালিকা সাজানো থাকায় নামও দ্রুত মেলে।
$ git rev-parse HEAD
bd9d66df7d58229921e670ab68ac9471f51750cb
$ git rev-parse 'HEAD^{tree}'
f32d1ce7e4f75215b9768171448a5c8ecfd38b0d
$ git rev-parse HEAD:src
be0cf5e51603159fd0863b82dca114773e4c29d4
$ git rev-parse HEAD:src/cart.js
6570f20184cd4454ec84681d7683dc1fc5ddce35
$ git hash-object src/cart.js
6570f20184cd4454ec84681d7683dc1fc5ddce35

rev-parse হলো Git-এর “নাম থেকে ID” অনুবাদক: branch, tag, HEAD~2, v1.0^{commit}, HEAD:path — সবকিছুকে পূর্ণ ID-তে বদলায়। Windows-এর PowerShell বা cmd-তে ^ আর {} বিশেষ অর্থ বহন করতে পারে, তাই উদ্ধৃতিচিহ্নে রাখুন। শেষ লাইনটি working tree-র ফাইল hash করেছে; ID মেলায় প্রমাণ হলো ফাইলটি commit-এর মতোই আছে।

৭ · শুধু plumbing দিয়ে একটি commit

এবার porcelain একদম বাদ। git add বা git commit ছাড়া, শুধু plumbing দিয়ে notes.txt (বিষয়বস্তু milk 60) যোগ করে তৃতীয় commit বানাব। Working tree-তে ফাইলটি তৈরিই করব না — দেখব Git কী বলে।

$ echo "milk 60" | git hash-object -w --stdin
91bd68f5aaf6a47f67d780330dfe074748fcad75
$ git update-index --add --cacheinfo 100644,91bd68f5aaf6a47f67d780330dfe074748fcad75,notes.txt
$ git write-tree
68bac8fb55ee7f99890cd580c17baea3273d2293
$ echo "Add notes via plumbing" | git commit-tree 68bac8f -p HEAD
80794bae52ca2d7c9c3cf5ade267e84a75f1c6e0
$ git update-ref refs/heads/main 80794bae52ca2d7c9c3cf5ade267e84a75f1c6e0
$ git log --oneline
80794ba Add notes via plumbing
bd9d66d Add copy and run script
17f23e6 Add greeting and cart
$ git status --short
 D notes.txt
$ git restore notes.txt
$ cat notes.txt
milk 60
$ git reflog -1
80794ba HEAD@{0}: 

পাঁচটি command, আর git log একটি সাধারণ commit-এর মতোই দেখাচ্ছে — কারণ এটি সাধারণ commit-ই। কয়েকটি সূক্ষ্ম বিষয়: (১) commit-tree author আর committer নিল config আর GIT_AUTHOR_DATE/GIT_COMMITTER_DATE থেকে, ঠিক porcelain-এর মতো। (২) git status বলছে D notes.txt — index আর commit-এ ফাইলটি আছে, working tree-তে নেই, তাই Git ভাবছে আপনি এটি মুছেছেন। git restore (Git ২.২৩ থেকে; পুরোনো সমতুল্য git checkout -- notes.txt) index থেকে ফাইলটি বের করে আনল। (৩) update-ref-এ -m না দেওয়ায় reflog-এর বার্তা খালি। ভালো অভ্যাস: git update-ref -m "commit: Add notes" refs/heads/main <নতুন> <পুরোনো> — শেষে পুরোনো ID দিলে Git মিলিয়ে নেয় যে এর মধ্যে কেউ branch সরায়নি, না মিললে কাজটি বাতিল করে।

প্রতিটি command-এর পরে অবস্থা
command
ছাপা ফল
object (loose)
index
main
working tree-তে notes.txt
  1. Blob লেখা। -w থাকায় milk 60 আর newline (৮ byte) blob 91bd68f হিসেবে objects/-এ জমা হলো। আগে ছিল ১০টি object (২ commit-এর ৯টি + tag ১টি), এখন ১১টি। কোনো ফাইলের নাম এখনো জড়িত নয়।
  2. Index-এ নাম জোড়া। --cacheinfo 100644,91bd68f…,notes.txt বলে: “mode 100644, এই blob, এই path.” --add লাগে কারণ path-টি index-এ নতুন। Working tree-র দিকে Git তাকায়ইনি।
  3. Index থেকে tree. write-tree পাঁচটি entry থেকে নতুন root tree 68bac8f লিখল। docs/ আর src/ বদলায়নি, তাই তাদের tree নতুন করে লেখার দরকার হয়নি — নতুন object মাত্র একটি।
  4. Tree থেকে commit. commit-tree tree, parent (-p HEAD = bd9d66d), author/committer আর stdin-এর বার্তা দিয়ে commit 80794ba বানাল। কিন্তু main এখনো bd9d66d-এ! এ অবস্থায় থেমে গেলে commit-টি ভাণ্ডারে থাকবে, কিন্তু কোনো ref থেকে পৌঁছানো যাবে না।
  5. Branch সরানো। update-ref main-কে 80794ba-এ সরাল আর reflog-এ এক লাইন লিখল। HEAD যেহেতু main-এর নাম রাখে, এখন git log তিনটি commit দেখায়। এই ধাপটিই git commit-এর শেষ কাজ।
  6. Working tree মেলানো। git status তুলনা করে HEAD, index আর working tree; notes.txt শুধু শেষেরটিতে নেই, তাই D. git restore notes.txt index-এর blob থেকে ফাইলটি লিখল। Porcelain git commit কখনো এই অবস্থায় পড়ে না, কারণ সে working tree-র ফাইল থেকেই শুরু করে।

২০০৫ সালের এপ্রিলে Linus Torvalds Git-এর প্রথম সংস্করণ লেখেন। তার README-তে Git-কে বলা হয়েছিল “the stupid content tracker”, আর প্রথম command-গুলোর নাম ছিল init-db, update-cache, write-tree, commit-tree, cat-file — আজকের plumbing-এর পূর্বপুরুষ। মানুষের জন্য সুবিধাজনক command (porcelain) প্রথমে আলাদা টুল হিসেবে আসে, যেমন Petr Baudis-এর Cogito; পরে সেগুলোর ধারণা Git-এর ভেতরেই ঢোকে। এজন্যই Git-এর দুই স্তর এত স্পষ্ট: নিচের স্তরটি আগে জন্মেছিল।

৮ · যুক্তিতে snapshot, ডিস্কে delta — deduplication আর packfile

“প্রতিটি commit পূর্ণ snapshot” শুনলে মনে হতে পারে repository দ্রুত বিশাল হবে। দুটি কৌশল এটি ঠেকায়। প্রথমটি আপনি দেখেই ফেলেছেন: deduplication — না-বদলানো ফাইল আর ফোল্ডার নতুন commit-এ একই object আবার ব্যবহার করে। একটি ছোট website-এর তিনটি commit দিয়ে হিসাবটি করি; প্রতিটি commit-এ সব ফাইলের পূর্ণ snapshot, কিন্তু ভাণ্ডারে একই বিষয়বস্তু একবারই:

// Storage model: 3 commits of a small site, each a FULL snapshot of 4 files.
// Content-addressed store: the key is "blob <size>\0<content>" (real Git hashes this key).
const fill = (ch, n) => ch.repeat(n);             // fake file contents of n bytes
const v1 = { "logo.png": fill("L", 40000), "index.html": fill("a", 6000),
             "style.css": fill("s", 3000), "README.md": fill("r", 1000) };
const v2 = { ...v1, "index.html": fill("b", 6100) };          // only index.html edited
const v3 = { ...v2, "style.css": fill("t", 3200),             // style.css edited and
             "about.html": fill("a", 6000) };                 // about.html = old index.html
const store = new Map();                                       // key -> size in bytes
let naiveBytes = 0, copies = 0;
for (const [name, snap] of [["C1", v1], ["C2", v2], ["C3", v3]]) {
  let added = 0, reused = 0;
  for (const content of Object.values(snap)) {
    const key = "blob " + content.length + "\0" + content;
    naiveBytes += content.length; copies++;                    // copy-every-file approach
    if (store.has(key)) reused++;
    else { store.set(key, content.length); added++; }
  }
  console.log(`${name}: ${Object.keys(snap).length} files, new blobs ${added}, reused ${reused}`);
}
const stored = [...store.values()].reduce((a, b) => a + b, 0);
console.log("unique blobs:", store.size, " naive file copies:", copies);
console.log("naive bytes :", naiveBytes);
console.log("blob bytes  :", stored, `(${(100 * (1 - stored / naiveBytes)).toFixed(1)}% saved)`);

আসল আউটপুট:

C1: 4 files, new blobs 4, reused 0
C2: 4 files, new blobs 1, reused 3
C3: 5 files, new blobs 1, reused 4
unique blobs: 6  naive file copies: 13
naive bytes : 156400
blob bytes  : 59300 (62.1% saved)

১৩টি ফাইল-কপির জায়গায় ৬টি blob; ১,৫৬,৪০০ byte-এর জায়গায় ৫৯,৩০০ byte — ৬২.১% সাশ্রয়, সংকোচন ছাড়াই। C3-এ about.html নতুন নাম, কিন্তু তার বিষয়বস্তু C1-এর index.html-এর হুবহু কপি; তাই কোনো নতুন blob লাগেনি — Git নাম দিয়ে নয়, বিষয়বস্তু দিয়ে চেনে। এই মডেলে key হিসেবে পুরো লেখাটি রাখা হয়েছে; আসল Git ঠিক এই key-র SHA-1 রাখে। Tree আর commit object-ও লাগে (প্রতিটি commit-এ নতুন root tree আর commit), কিন্তু সেগুলো কয়েকশো byte মাত্র।

কিন্তু deduplication একটি সমস্যা মেটায় না: ৪ KB-র একটি ফাইলে একটি লাইন বদলালে নতুন blob পুরো ৪ KB. এখানে আসে দ্বিতীয় কৌশল — packfile. git gc (বা Git নিজে মাঝে মাঝে git gc --auto) অনেক loose object একটি .pack ফাইলে ভরে, আর একই রকম object-গুলোর একটিকে পুরো রেখে বাকিদের শুধু পার্থক্য বা delta রাখে। আমাদের তিন-commit-এর repository-তে:

$ git count-objects -v
count: 13
size: 1
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
$ git gc -q
$ git count-objects -v
count: 0
size: 0
in-pack: 13
packs: 1
size-pack: 2
…
$ ls .git/objects/pack
pack-bb1a7494860ee2a214cb8c116e4a22f38b7eec99.idx
pack-bb1a7494860ee2a214cb8c116e4a22f38b7eec99.pack
pack-bb1a7494860ee2a214cb8c116e4a22f38b7eec99.rev
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
80794bae52ca2d7c9c3cf5ade267e84a75f1c6e0 refs/heads/main
0b5a053c1addf8d449d3b9232eeda4ecac173c31 refs/tags/v1.0
^bd9d66df7d58229921e670ab68ac9471f51750cb

count হলো loose object-এর সংখ্যা: gc-র আগে ১৩, পরে ০; সবগুলো এখন একটি pack-এ (in-pack: 13). size আর size-pack KiB-এ ডিস্কের জায়গা — এগুলো ফাইল সিস্টেম-ভেদে আলাদা (এই আউটপুট Windows-এর; Linux-এ প্রতিটি ছোট ফাইল সাধারণত পুরো একটি ৪ KiB block নেয়, তাই size অনেক বড় দেখাবে), কিন্তু সংখ্যাগুলো সব জায়গায় এক। Pack-এর সাথে থাকে .idx (ID → pack-এর ভেতরে কোন byte-এ, দ্রুত খোঁজার সূচি) আর নতুন Git-এ .rev (উল্টো সূচি)। gc branch আর tag-ও packed-refs ফাইলে গুছিয়েছে; ^bd9d66d… লাইনটি বলে annotated tag v1.0 খুললে কোন commit পাওয়া যায় (“peeled”). এখন আর .git/refs/heads/main ফাইলটি নেই — তাই script-এ ref পড়তে সবসময় git rev-parse.

Delta ভালোভাবে দেখতে একটি বড় ফাইল লাগে। আরেকটি scratch repository-তে ২০০ লাইনের একটি দামের তালিকা prices.csv (৪,৩৫১ byte), আর চারটি commit, প্রতিটিতে একটি দাম বদলানো:

$ git log --oneline
314cfec Price list v4
d839fdc Price list v3
0fb8885 Price list v2
ca45023 Price list v1
$ git gc -q
$ git verify-pack -v .git/objects/pack/pack-*.idx
314cfec9f9513c8d08765f1ebe0b8fb9510f6108 commit 212 151 12
…
5b0a2471a7904f6f54277a9bdee494b4d6bf973d blob   4354 1056 585
907485f20862f8b3d6ee18b116fbddd326c06360 tree   38 49 1641
…
2f2f909ca588bab7bd850c54f091923e16d2d592 blob   14 25 1739 1 5b0a2471a7904f6f54277a9bdee494b4d6bf973d
…
5229ab56116c4145f6c0f5eec825858722cb9a1c blob   21 33 1813 1 5b0a2471a7904f6f54277a9bdee494b4d6bf973d
…
975d72fec0e39a6bb5087d9ccdc188df951a0f29 blob   28 41 1895 1 5b0a2471a7904f6f54277a9bdee494b4d6bf973d
non delta: 9 objects
chain length = 1: 3 objects
.git/objects/pack/pack-9fa97ccabd6c7c53ca1b028d8e6544a0a4c8aad5.pack: ok

প্রতিটি লাইনে: ID, ধরন, আকার, pack-এ নেওয়া byte, pack-এর ভেতরে অবস্থান; delta হলে আরও দুটি ঘর — chain-এর গভীরতা আর ভিত্তি (base) object-এর ID. সবচেয়ে নতুন সংস্করণ (v4, 5b0a247, ৪,৩৫৪ byte) পুরো রাখা, zlib-এ সংকুচিত হয়ে ১,০৫৬ byte. আর পুরোনো তিনটি সংস্করণ রাখা হয়েছে v4-এর সাপেক্ষে delta হিসেবে — ১৪, ২১ আর ২৮ byte-এর নির্দেশ (“এই অংশ কপি করা, এখানে এই ক’টি byte বসানো” ধরনের), pack-এ ২৫, ৩৩ আর ৪১ byte. চারটি সংস্করণ মিলে pack-এ ১,০৫৬ + ২৫ + ৩৩ + ৪১ = ১,১৫৫ byte, যেখানে কাঁচা হিসাবে ৪টি × প্রায় ৪,৩৫০ = প্রায় ১৭,৪১০ byte. লক্ষ করুন, ভিত্তি হলো নতুনতম সংস্করণ, পুরোনোটি নয় — কারণ সাধারণত সাম্প্রতিক সংস্করণই বেশি পড়া হয়, তাই সেটি সরাসরি পাওয়া গেলে দ্রুত হয়। এটি Git-এর একটি heuristic, নিশ্চিত নিয়ম নয়। প্রথম repository-র pack-এও একটি tree delta হয়েছিল (f32d1ce, ভিত্তি 68bac8f).

একটি delta chain-এ একটি delta-র ভিত্তি আরেকটি delta হতে পারে; সেই object পড়তে Git শিকল ধরে ভিত্তি থেকে একটির পর একটি delta প্রয়োগ করে। লম্বা শিকল মানে কম জায়গা কিন্তু ধীর পড়া; Git-এর default সীমা pack.depth ৫০, আর কতগুলো object-এর সাথে তুলনা করবে তা pack.window (default ১০)। git gc --aggressive বড় window দিয়ে আরও খোঁজে — অনেক সময় নেয়, আর সাধারণ দলের জন্য প্রায়ই অপ্রয়োজনীয়।

মূল ধারণা

মডেল আর সংরক্ষণ আলাদা। Git-এর যুক্তির স্তরে প্রতিটি commit একটি পূর্ণ snapshot — তাই checkout, diff বা branch বদলানোর হিসাব সরল: দুটি tree মেলালেই হয়, delta-র শিকল পেছনে হাঁটতে হয় না। আর ডিস্কের স্তরে packfile delta আর zlib দিয়ে জায়গা বাঁচায়। পুরোনো RCS বা Subversion-এর মতো সিস্টেমে delta-ই ছিল মডেল; Git-এ delta শুধু একটি সংকোচন-কৌশল, যা যেকোনো দুটি একই রকম object-এর মধ্যে হতে পারে — এমনকি দুটি আলাদা ফাইলের মধ্যেও। Remote-এ fetch/push-এর সময়ও এই packfile-ই পাঠানো হয় (পর্ব ১৪)।

৯ · SHA-1 থেকে SHA-256 — নামের ভিত্তি যখন দুর্বল হয়

পুরো নকশা দাঁড়িয়ে আছে একটি অনুমানের ওপর: দুটি আলাদা বিষয়বস্তুর একই hash হবে না (collision). SHA-1-এর output ১৬০ bit, তাই নিখাদ ভাগ্যে collision প্রায় অসম্ভব। কিন্তু ইচ্ছা করে বানানো collision ভিন্ন কথা। ২০১৭ সালের ফেব্রুয়ারিতে Google আর CWI Amsterdam-এর গবেষকেরা “SHAttered” প্রকাশ করেন: একই SHA-1-এর দুটি আলাদা PDF ফাইল। এরপর Git ২.১৩ (২০১৭) থেকে default হিসেবে hardened SHA-1 (SHA-1DC) ব্যবহার করে — এটি সাধারণ বিষয়বস্তুতে হুবহু SHA-1-এর ফলই দেয়, কিন্তু SHAttered-ধাঁচের collision-আক্রমণের ছাপ দেখলে কাজটি থামিয়ে দেয়। নিরাপত্তার দিক থেকে এর মানে কী, কোন আক্রমণ বাস্তব আর কোনটি নয়, আর signed commit — সেসব পর্ব ৫৭-এর (Git-এ নিরাপত্তা) বিষয়।

দীর্ঘমেয়াদি সমাধান হলো hash বদলানো। Git ২.২৯ (২০২০) থেকে SHA-256-এর repository বানানো যায়, শুরুতে পরীক্ষামূলক হিসেবে:

$ git -c init.defaultBranch=main init -q --object-format=sha256
$ git rev-parse --show-object-format
sha256
$ echo hello | git hash-object --stdin
2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4
$ git log --oneline
4339515 Add greeting
$ git rev-parse HEAD
433951541da79b2a4b92d69a709eb825bc5b3171cf2c55660f9423ef2791938c
$ cat .git/config
[extensions]
	objectformat = sha256
[core]
	repositoryformatversion = 1
…

একই hello blob এখানে ৬৪ অক্ষরের 2cf8d83… — ওপরের Python-এর sha256 id লাইনের সাথে হুবহু মেলে, কারণ header (blob 6\0) আর নিয়ম একই, শুধু hash function আলাদা। ছোট ID (4339515) আগের মতোই কাজ করে। .git/config-এ repositoryformatversion = 1 আর extensions.objectformat থাকায় এই extension না বোঝা পুরোনো Git repository-টি খুলতেই অস্বীকার করবে — ভুল করে নষ্ট করার বদলে।

সীমাবদ্ধতা (লেখার সময়ে): SHA-256 আর SHA-1 repository-র মধ্যে সরাসরি fetch/push চলে না — দুটির object ID আলাদা, তাই একই ইতিহাস দুই জগতে দুই নামে। দুই ধরনের ID-র মধ্যে অনুবাদ করে আদান-প্রদানের ব্যবস্থা নিয়ে কাজ চলছে, কিন্তু সাধারণ ব্যবহারের জন্য এখনো প্রস্তুত বলে ধরে নেবেন না। Hosting platform আর অন্য টুলের (IDE, CI, library) সমর্থন platform-ভেদে আলাদা ও সীমিত। Git-এর documentation থেকে পরে “experimental” লেবেল সরানো হয়েছে, আর Git প্রকল্প ভবিষ্যতের একটি বড় সংস্করণে (Git ৩.০) নতুন repository-র default SHA-256 করার পরিকল্পনার কথা বলেছে — কিন্তু আজ একটি দলের project শুরু করার আগে নিজের সব টুলে পরীক্ষা করে নিন।

আরও গভীরে: Object format-এর নিখুঁত বিবরণ Git-এর নিজের documentation-এ — git help gitformat-pack আর git help gitformat-index (নতুন Git-এ), আর SHA-256-এ যাওয়ার পরিকল্পনা Git source-এর Documentation/technical/ ফোল্ডারের hash-function-transition নথিতে। Scott Chacon ও Ben Straub-এর Pro Git (২য় সংস্করণ, ২০১৪)-এর “Git Internals” অধ্যায় এই পর্বের মতোই plumbing দিয়ে commit বানায়। Hash দিয়ে গাছ সাজানোর ধারণাটি Ralph Merkle-এর ১৯৭০-এর দশকের কাজ থেকে; পরে IPFS, Docker image-এর layer digest, Nix store আর অনেক backup টুল একই content-addressing-এর ধারণা ব্যবহার করে। Packfile delta কীভাবে খোঁজা হয় (নাম আর আকার দিয়ে সাজিয়ে একটি sliding window), তা Git-এর source-এর pack-objects অংশে আছে।

পরীক্ষা ও ইন্টারভিউতে:

  • Git-এর চার ধরনের object কী কী? — blob (বিষয়বস্তু), tree (ফোল্ডার: mode, ধরন, নাম, ID), commit (tree, parent, author, committer, বার্তা), annotated tag.
  • Blob-এ ফাইলের নাম থাকে না কেন? — নাম tree-তে থাকে; ফলে একই বিষয়বস্তু যেকোনো নামে একটি blob, আর rename-এ blob বদলায় না।
  • Git কি diff রাখে, না snapshot? — মডেলে snapshot; ডিস্কে packfile-এর ভেতরে delta আর zlib সংকোচন।
  • Object ID কীভাবে হিসাব হয়? — "<type> <size>\0<content>"-এর SHA-1 (বা SHA-256 repository-তে SHA-256).
  • Plumbing আর porcelain-এর পার্থক্য? — plumbing ছোট, স্থির আউটপুটের, script-এর জন্য (cat-file, write-tree); porcelain মানুষের জন্য (add, commit).

১০ · AI-এর যুগে — Git-এর ভেতরের কথা যাচাই করে নেওয়া

Git internals নিয়ে AI বেশ আত্মবিশ্বাসী ব্যাখ্যা দেয়, আর তার অনেকটাই ঠিক। কিন্তু এই বিষয়ের সুবিধা হলো প্রতিটি দাবি দুই মিনিটে নিজের চোখে যাচাই করা যায় — একটি throwaway repository আর কয়েকটি plumbing command দিয়ে। AI সাধারণত যেসব ভুল করে:

AI যা বলতে পারেআসলেকীভাবে যাচাই
“Git প্রতিটি commit-এ শুধু diff রাখে”মডেলে snapshot; delta শুধু pack-এর সংকোচনgit cat-file -p HEAD-এ tree, তারপর verify-pack -v
“Object ID হলো শুধু ফাইলের SHA-1” (sha1sum file)header blob <size>\0 যোগ করে hashsha1sum আর git hash-object পাশাপাশি
“জায়গা বাঁচাতে .git/objects-এর পুরোনো ফোল্ডার মুছে দিন”Repository নষ্ট হবে; দরকারি object হারাবেgit count-objects -v, git gc, পরে git fsck
git gc --prune=now --aggressive নির্দ্বিধায়Reachable নয় এমন object সাথে সাথে মুছে যায় — reflog-এ না থাকলে উদ্ধারের পথ বন্ধআগে পর্ব ৫৩ পড়ুন; throwaway clone-এ চালান
বানানো flag, যেমন git cat-file --treeএমন flag নেই; -t, -s, -p, --batch-check আছেgit help cat-file
“SHA-256 repository এখন সব জায়গায় চলে”SHA-1 repository-র সাথে আদান-প্রদান আর টুলের সমর্থন সীমিতআপনার Git version-এর release notes আর hosting-এর docs

AI-কে internals-এর প্রশ্ন করার সময় তাকে দিয়েই যাচাইয়ের পরীক্ষা লিখিয়ে নিন, আর সেটি নিজে চালান:

আমি Git 2.53 ব্যবহার করি। Git-এর tree object-এ একটি ফোল্ডারের entry ডিস্কে ঠিক কীভাবে লেখা থাকে (mode, নাম, ID) — সহজ বাংলায় বোঝাও। তারপর একটি খালি scratch ফোল্ডারে চালানোর মতো ছোট command-এর ধারা দাও (git init থেকে শুরু, শুধু read-only plumbing যেমন cat-file, ls-tree, rev-parse), যা দিয়ে আমি তোমার প্রতিটি দাবি নিজে যাচাই করতে পারব। কোনো command যদি ডেটা মুছতে পারে (gc --prune, reset --hard, update-ref -d), আলাদা করে সতর্ক করো। যে flag সম্পর্কে নিশ্চিত নও, সেটি বলো — বানিয়ে লিখো না।

যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Linus Torvalds কেন Git-কে একটি “content-addressable filesystem” হিসেবে বানালেন, প্রচলিত version control-এর মতো ফাইলভিত্তিক ইতিহাস নয়?

২০০৫ সালে Linux kernel-এর দল যে বাণিজ্যিক টুল (BitKeeper) বিনামূল্যে ব্যবহার করত, সেই অনুমতি শেষ হয়ে যায়। Torvalds-এর সামনে শর্ত ছিল কঠিন: হাজারো মানুষ, লক্ষ লক্ষ লাইন, প্রতিদিন বহু patch, আর কোনো কেন্দ্রীয় server-এ ভরসা নয়। তিনি প্রথমে একটি ফাইলভিত্তিক, delta-নির্ভর নকশা থেকে সরে এসে একদম নিচের স্তর থেকে শুরু করেন — একটি ভাণ্ডার, যেখানে প্রতিটি জিনিসের নাম তার বিষয়বস্তুর hash. প্রথম README-তে Git-কে বলা হয় “the stupid content tracker”.

এই নকশার সুবিধাগুলো ঠিক তাঁর সমস্যার মাপে। প্রথমত অখণ্ডতা: hash-ই নাম, তাই কোনো ডিস্ক-ত্রুটি বা নেটওয়ার্কে নষ্ট byte সাথে সাথে ধরা পড়ে; আর একটি commit ID পুরো ইতিহাসের ছাপ, তাই দুই developer-এর repository-তে একই ID মানে হুবহু একই ইতিহাস। বিতরণ-ভিত্তিক কাজে এটি অমূল্য — কাউকে বিশ্বাস না করেও যাচাই করা যায়। দ্বিতীয়ত গতি: দুটি tree-র ID মিললে ভেতরে ঢোকারই দরকার নেই; বিশাল project-এ diff বা status তাই দ্রুত। তৃতীয়ত সরলতা: মাত্র চার ধরনের object, আর সবকিছু — branch, tag, merge — এদের ওপর পাতলা স্তর। চতুর্থত বিতরণ সহজ: দুই repository মেলাতে শুধু জানতে হয় কার কাছে কোন ID নেই; সেগুলো পাঠালেই হলো।

দামও আছে। Rename আলাদা করে মনে রাখা হয় না — Git পরে বিষয়বস্তুর মিল দেখে অনুমান করে, যা কখনো ভুল হয়। বিশাল binary ফাইল (ভিডিও, design-ফাইল) প্রতিটি সংস্করণে নতুন blob, তাই Git LFS-এর মতো সমাধান লাগে (পর্ব ১৯)। আর hash function দুর্বল হলে পুরো ভিত নড়ে — SHA-1 থেকে সরে আসার দীর্ঘ কাজ সেই দামেরই অংশ। ধারণাটি শুধু Git-এর নয়; একই বছরে জন্মানো Mercurial-ও hash দিয়ে revision চেনে, আর পরে container image, package store ও অনেক backup টুল একই ধারণা নিয়েছে।

মূল উপলব্ধি: সঠিক নিচের স্তর (অপরিবর্তনীয়, বিষয়বস্তু দিয়ে নাম) বেছে নিলে ওপরের অনেক কঠিন সমস্যা — যাচাই, ভাগাভাগি, বিতরণ — প্রায় আপনাআপনি সহজ হয়ে যায়। নকশার প্রথম সিদ্ধান্তই প্রায়ই সবচেয়ে দামি।

প্র ০২Git যুক্তিতে snapshot রাখে কিন্তু ডিস্কে delta — এই ভাগ ভালো নকশা কেন, আর delta-ভিত্তিক পুরোনো সিস্টেমের সাথে পার্থক্য কোথায়?

১৯৮০-এর দশকের RCS আর পরে CVS প্রতিটি ফাইলের ইতিহাস রাখত delta-র শিকল হিসেবে: সাধারণত সর্বশেষ সংস্করণ পুরো, আর পুরোনোগুলো “উল্টো delta”. Subversion-ও ভেতরে delta ব্যবহার করে। এসব সিস্টেমে delta শুধু সংরক্ষণের কৌশল নয়, ইতিহাসের মডেল-ও — একটি ফাইলের সংস্করণ মানে আগের সংস্করণের সাথে পার্থক্য। ফলে অনেক প্রশ্নের উত্তর দিতে শিকল ধরে হাঁটতে হয়, আর “পুরো project-এর এই মুহূর্তের অবস্থা” একটি গণনা, সরাসরি পাওয়া জিনিস নয়।

Git দুটি স্তর আলাদা করেছে। মডেল: প্রতিটি commit একটি পূর্ণ tree — দুই commit-এর পার্থক্য জানতে দুটি tree পাশাপাশি রেখে তুলনা, যেখানে ID মিললে পুরো উপ-ফোল্ডার এক লাফে বাদ। Branch বদলানো, merge-এর হিসাব, checkout — সবই এই সরল মডেলে। সংরক্ষণ: packfile সিদ্ধান্ত নেয় কোন object কার সাপেক্ষে delta হবে, আর সেটি ইতিহাসের ক্রম মানতে বাধ্য নয় — আলাদা ফাইল, আলাদা branch, এমনকি দুটি আলাদা path-এর মধ্যেও delta হতে পারে। Git নাম, আকার আর ধরন দিয়ে object সাজিয়ে একটি window-এর ভেতরে তুলনা করে সবচেয়ে ছোট delta খোঁজে; আর pack নতুন করে বানালে এই পছন্দ বদলাতেও পারে, ইতিহাস একটুও না বদলে।

এই বিচ্ছেদের লাভ: সংরক্ষণের কৌশল উন্নত হলে (নতুন delta খোঁজার নিয়ম, bitmap index, multi-pack index) কোনো commit ID বদলায় না, কোনো ব্যবহারকারীকে কিছু শিখতে হয় না। দামও আছে — একটি loose object-ভর্তি repository অনেক জায়গা নেয় যতক্ষণ না gc চলে, আর বড় repository-তে pack নতুন করে বানানো (repack) ভারী কাজ। এজন্যই Git নিজে মাঝে মাঝে gc --auto চালায় (loose object প্রায় ৬,৭০০ ছাড়ালে, default সেটিংয়ে), আর নতুন Git-এ git maintenance পেছনে নিয়মিত কাজগুলো সারে।

সফটওয়্যার নকশার সাধারণ শিক্ষাও এখানে: এটি ঠিক database-এর logical আর physical স্তরের পার্থক্যের মতো — SQL-এ আপনি table নিয়ে ভাবেন, index বা page কীভাবে ডিস্কে আছে তা engine-এর দায়িত্ব।

মূল উপলব্ধি: ব্যবহারকারী যে মডেল নিয়ে ভাবেন, তা সরল রাখুন; জায়গা আর গতির চালাকি তার নিচে লুকান। দুটি একসাথে জট পাকালে দুটিই কঠিন হয়।

প্র ০৩SHAttered (২০১৭)-এর পরেও Git সাথে সাথে “ভেঙে” যায়নি কেন? আর SHA-256-এ যাওয়া এত ধীর কেন?

SHAttered ছিল একটি collision: আক্রমণকারী নিজে দুটি ফাইল বানান, যাদের SHA-1 এক। এটি second preimage নয় — অর্থাৎ অন্য কারও বানানো একটি নির্দিষ্ট ফাইলের সমান hash-এর নতুন ফাইল বানানো নয়। Git-এর ক্ষেত্রে বাস্তব ঝুঁকি হলো: কেউ আগে থেকে একজোড়া “ভালো” আর “খারাপ” ফাইল বানিয়ে একটি project-এ ভালোটি ঢোকালেন, পরে কোনো বিশেষ পরিস্থিতিতে কিছু মানুষকে খারাপটি দিলেন, একই ID-তে। গবেষণাটির জন্য বিপুল গণনা লেগেছিল (গবেষকদের প্রকাশিত হিসাবে প্রায় ৬,৫০০ CPU-বছরের সমান), আর Git object-এ header (blob <size>\0) থাকায় PDF দুটি সরাসরি Git object-এ কাজে লাগে না — তবে একই কৌশলে Git-এর জন্য আলাদা জোড়া বানানো তাত্ত্বিকভাবে সম্ভব।

Git-এর তাৎক্ষণিক জবাব ছিল চতুর: Marc Stevens ও Dan Shumow-এর collision-detection কৌশল (SHA-1DC) Git ২.১৩ থেকে default হয়। সাধারণ ডেটায় এটি সাধারণ SHA-1-এর হুবহু ফল দেয় — তাই একটি ID-ও বদলাতে হয়নি — কিন্তু collision-আক্রমণের গাণিতিক ছাপ দেখলে কাজ বন্ধ করে দেয়। একে বলে hardened SHA-1. এটি দীর্ঘমেয়াদি সমাধান নয়, কারণ SHA-1-এর বিরুদ্ধে আক্রমণ সস্তা হচ্ছে (২০২০ সালে “chosen-prefix” collision-ও দেখানো হয়)।

তাহলে SHA-256-এ যেতে দেরি কেন? কারণ hash Git-এর সব জায়গায়: প্রতিটি object-এর নাম, tree-র ভেতরের ২০ byte-এর ID, index ফাইল, pack-এর সূচি, network protocol, আর কোটি কোটি পুরোনো commit, যাদের ID মানুষ issue, commit বার্তা, documentation আর script-এ লিখে রেখেছেন। Hash বদলানো মানে প্রতিটি ID বদলে যাওয়া। Git-এর পরিকল্পনা তাই ধাপে ধাপে: আগে নতুন repository SHA-256-এ বানানো যাক (২.২৯), তারপর দুই ধরনের ID-র মধ্যে অনুবাদ করে পুরোনো জগতের সাথে আদান-প্রদান, আর বহু পরে default বদল। তার ওপর আছে পুরো ecosystem — hosting, IDE, CI, অন্য implementation (libgit2, JGit) — সবাইকে একসাথে আসতে হবে।

বিস্তারিত নিরাপত্তা-বিশ্লেষণ আর signed commit পর্ব ৫৭-এ।

মূল উপলব্ধি: Cryptographic ভিত্তি একদিন দুর্বল হবেই — তাই নকশায় শুরু থেকেই “algorithm বদলানোর পথ” (crypto agility) রাখা দরকার। Git-এর ধীর রূপান্তর দেখায়, পরে যোগ করা কত কঠিন।

প্র ০৪Plumbing আর porcelain-এর বিভাজন সফটওয়্যার নকশার কোন নীতির উদাহরণ? Script লিখতে কোনটি ব্যবহার করবেন, আর কেন?

এটি Unix-দর্শনের সরাসরি উত্তরাধিকার: “একটি প্রোগ্রাম একটি কাজ ভালোভাবে করুক, আর প্রোগ্রামগুলো একসাথে জোড়া লাগুক।” hash-object শুধু hash করে, write-tree শুধু index থেকে tree লেখে, update-ref শুধু একটি ref নিরাপদে বদলায়। Porcelain এগুলো জুড়ে মানুষের জন্য একটি সুবিধাজনক কাজ বানায়। নকশার ভাষায় এটি একটি স্তরভিত্তিক architecture (পর্ব ৩৮) আর একটি স্থির, সংকীর্ণ interface-এর উদাহরণ (পর্ব ৩১) — নিচের স্তরের contract বদলায় না, ওপরের স্তর স্বাধীনভাবে উন্নত হতে পারে।

বাস্তব সুবিধা দেখা যায় version বদলের সময়। Git-এর মানুষ-পড়ার আউটপুট (git status-এর লেখা, git log-এর রঙ, নতুন পরামর্শ-বার্তা) প্রায় প্রতিটি release-এ একটু বদলায় — যেমন ২.২৩-এ switch/restore আসার পর পরামর্শের লেখা বদলেছে। এই আউটপুট parse করা script একদিন নিঃশব্দে ভুল ফল দেবে। Plumbing-এর আউটপুট আর --porcelain ধরনের format (নামটি বিভ্রান্তিকর হলেও অর্থ “script-এর জন্য স্থির”) সেই প্রতিশ্রুতি দেয়। তাই CI-এর script বা hook-এ git rev-parse HEAD, git status --porcelain=v1, git for-each-ref --format=…, git cat-file --batch-check ব্যবহার করুন; git branch-এর আউটপুট থেকে * কেটে নাম বের করা নয়।

এই স্তরের কারণে Git-এর চারপাশে বিশাল ecosystem দাঁড়িয়েছে: GUI client, IDE-র Git প্যানেল, code review টুল, এমনকি Git-কে অন্য কাজে লাগানো (কিছু টুল Git-কে configuration বা ডেটার ভাণ্ডার হিসেবে ব্যবহার করে)। অনেক টুল অবশ্য Git চালানোর বদলে নিজেই format পড়ে-লেখে (libgit2, JGit, go-git) — এটি সম্ভব কারণ object format সরল ও নথিভুক্ত। দামও আছে: plumbing ভুলভাবে ব্যবহার করলে porcelain-এর নিরাপত্তা-জাল থাকে না — যেমন update-ref জোর করে একটি branch সরিয়ে দিতে পারে, কোনো “আপনি কি নিশ্চিত?” ছাড়াই।

মূল উপলব্ধি: মানুষের জন্য একটি interface আর যন্ত্রের জন্য আরেকটি — দুটিকে আলাদা রাখলে দুটিই নিজের মতো ভালো হতে পারে। আপনার নিজের টুল বা API নকশাতেও এই প্রশ্নটি করুন: “কে পড়বে — মানুষ, না program?”

প্র ০৫Content-addressing আর অপরিবর্তনীয়তার ধারণা Git-এর বাইরে কোথায় কোথায় দেখা যায়, আর একজন software engineer এ থেকে কী শিখবেন?

একবার চিনলে ধারণাটি সব জায়গায় চোখে পড়ে। Container image: Docker/OCI image-এর প্রতিটি layer চেনা হয় তার SHA-256 digest দিয়ে; দুটি image একই base layer ব্যবহার করলে ডিস্কে আর নেটওয়ার্কে সেটি একবারই আসে — ঠিক Git-এর blob ভাগাভাগির মতো। Package store: Nix-এর মতো package manager প্রতিটি package-কে তার উপকরণের hash-যুক্ত path-এ রাখে, ফলে দুটি আলাদা version পাশাপাশি থাকতে পারে। Backup: অনেক আধুনিক backup টুল ফাইলকে টুকরো করে প্রতিটি টুকরোর hash দিয়ে রাখে, তাই প্রতিদিনের backup-এ শুধু নতুন টুকরো যায়। বিতরণ-ভিত্তিক ফাইল ব্যবস্থা: IPFS-এ ফাইলের ঠিকানাই তার hash. Blockchain: প্রতিটি block আগের block-এর hash ধরে রাখে, আর লেনদেনগুলো Merkle tree-তে — Git-এর parent-শিকলের আত্মীয়। Web cache: build টুলের তৈরি app.3f9a1c.js ধরনের ফাইলনাম বিষয়বস্তুর hash থেকে, যাতে browser অনন্তকাল cache রাখতে পারে আর বদলালে নাম আপনাআপনি বদলায়।

এই উদাহরণগুলোর বিস্তারিত ব্যবস্থাপনা ভিন্ন (কোথাও SHA-256, কোথাও অন্য hash; কোথাও পুরো ফাইল, কোথাও টুকরো), কিন্তু সাধারণ সূত্র তিনটি: (১) যে ডেটা কখনো বদলায় না, তাকে নিশ্চিন্তে cache, কপি আর ভাগ করা যায়; (২) নাম যদি বিষয়বস্তু থেকে আসে, যাচাই আর deduplication বিনা খরচে; (৩) “বদল” মানে নতুন জিনিস বানিয়ে একটি ছোট, বদলযোগ্য pointer (Git-এ ref) সরানো। পর্ব ৩১-এ (Interface আর contract) immutability-র যে সুবিধা দেখেছেন — thread-safety, সহজ যুক্তি — Git সেটিকে ডিস্ক আর নেটওয়ার্কের মাপে প্রয়োগ করে।

সীমাও মনে রাখুন। অপরিবর্তনীয় ভাণ্ডারে “মুছে ফেলা” কঠিন: ভুল করে commit করা password ইতিহাস থেকে সরাতে পুরো ইতিহাস নতুন করে লিখতে হয়, আর অন্যদের clone-এ সেটি থেকেই যায় (পর্ব ৫৭)। ব্যক্তিগত তথ্য মোছার আইনি দাবি এলে content-addressed সিস্টেমে বাড়তি নকশা লাগে। আর পুরোনো ডেটা জমতে থাকে, তাই garbage collection লাগে (পর্ব ৫৩)।

মূল উপলব্ধি: “ডেটা বদলাবেন না, নতুন ডেটা বানিয়ে pointer সরান” — এই একটি নীতি cache, sync, যাচাই আর undo-কে সহজ করে। নতুন system নকশার সময় নিজেকে জিজ্ঞেস করুন: এর কোন অংশটি অপরিবর্তনীয় আর বিষয়বস্তু দিয়ে নাম দেওয়া যায়?

অনুশীলন

প্রতিটি command একটি খালি scratch ফোল্ডারে চালান, আসল project-এ নয়।

  1. আউটপুট অনুমান করুন: একটি খালি repository-তে চালানো হলো:
    printf 'hello\n' > a.txt, mkdir d, cp a.txt d/b.txt, git add ., git commit -m "Two copies". (ক) এরপর git count-objects কতটি object দেখাবে? (খ) git rev-parse HEAD:a.txt HEAD:d/b.txt-এর দুই লাইন কি একই? (গ) git cat-file -t HEAD:d কী ছাপবে? (ঘ) এবার শুধু git update-index --chmod=+x a.txt আর একটি commit — নতুন কতটি object হবে, আর a.txt-এর blob ID বদলাবে কি?

    Git ২.৫৩-এ আসল ফল:

    $ git count-objects
    4 objects, 0 kilobytes
    $ git rev-parse HEAD:a.txt HEAD:d/b.txt
    ce013625030ba8dba906f756967f9e9ca394464a
    ce013625030ba8dba906f756967f9e9ca394464a
    $ git cat-file -t HEAD:d
    tree
    …  (chmod +x আর দ্বিতীয় commit-এর পরে)
    $ git count-objects
    6 objects, 0 kilobytes
    $ git ls-tree HEAD
    100755 blob ce013625030ba8dba906f756967f9e9ca394464a	a.txt
    040000 tree dd5a3627ad3d4a1eaa9b180972bab37891a5e101	d

    (ক) ৪টি: একটি blob (দুটি ফাইলের বিষয়বস্তু এক), d-এর tree, root tree আর commit. (খ) হ্যাঁ, একই — ce01362, কারণ blob-এ নাম নেই। (গ) tree. (ঘ) ২টি নতুন (নতুন root tree আর নতুন commit), মোট ৬; blob একই থাকে, কারণ mode tree-র entry-তে লেখা, blob-এ নয়। d-এর tree-ও (dd5a362) বদলায়নি। এর পরে git commit --amend দিয়ে শুধু বার্তা বদলালে আরও একটি নতুন commit হবে, কিন্তু HEAD^{tree} একই থাকবে।

  2. Command-এর ধারা: একটি repository-তে main-এ একটি commit আছে। git add, git commit বা কোনো editor ছাড়া, শুধু plumbing দিয়ে main-এ একটি নতুন commit বানান, যাতে todo.txt ফাইল যোগ হয় (বিষয়বস্তু buy rice), বার্তা “Add todo list”, আর reflog-এ একটি অর্থবহ বার্তা থাকে। শেষে working tree-ও মেলান এবং যাচাই করুন।
    $ blob=$(echo "buy rice" | git hash-object -w --stdin)
    $ git update-index --add --cacheinfo 100644,$blob,todo.txt
    $ tree=$(git write-tree)
    $ commit=$(git commit-tree $tree -p HEAD -m "Add todo list")
    $ git update-ref -m "commit: Add todo list" refs/heads/main $commit
    $ git restore todo.txt
    $ git status --short
    $ git log --oneline
    aadf55c Add todo list
    5581e58 Add greeting
    $ git reflog -2
    aadf55c HEAD@{0}: commit: Add todo list
    5581e58 HEAD@{1}: commit (initial): Add greeting
    $ git cat-file -p HEAD:todo.txt
    buy rice

    ধাপগুলো: blob লেখা (-w ছাড়া কিছুই জমা হতো না), index-এ path জোড়া, index থেকে tree, tree আর parent থেকে commit (-m দিয়ে বার্তা; stdin-ও চলে), তারপর branch সরানো — -m দেওয়ায় reflog-এ “commit: Add todo list” লেখা হলো। git status --short কিছু না ছাপা মানে সব মিলেছে। আসল ID (aadf55c) ওপরের scratch repository-র (প্রথম commit ১০:০০-এ, এটি ১০:০৫-এ, Rahim); আপনার নাম বা সময় আলাদা হলে ID-ও আলাদা হবে, কিন্তু blob ID cc0353a সব জায়গায় এক। ভুলের জায়গা: update-index-এ --add ভুলে গেলে নতুন path যোগ হয় না; আর update-ref-এর বদলে HEAD-এ সরাসরি ID লিখলে detached HEAD হয়ে যাবে।

  3. জায়গার হিসাব: একটি project-এর ২০টি commit. প্রতিটি commit-এ তিনটি ফাইল: logo.png (২০০ KB, কখনো বদলায় না), app.js (৩০ KB, প্রতিটি commit-এ বদলায়, আকার একই) আর data.csv (৫০০ KB, commit ১-এ তৈরি, শুধু commit ৫, ১০ আর ১৫-এ বদলায়)। Tree আর commit object-এর জায়গা আর zlib উপেক্ষা করুন। (ক) প্রতিটি commit-এ সব ফাইল কপি করলে মোট কত KB? (খ) Content-addressed ভাণ্ডারে কতটি blob, কত KB? (গ) ধরুন pack-এ প্রতিটি ফাইলের শুধু একটি সংস্করণ পুরো থাকে আর বাকিগুলো delta — app.js-এর প্রতিটি delta ১ KB, data.csv-এর ৫ KB. তখন কত KB, আর (ক)-এর তুলনায় সাশ্রয় কত শতাংশ?

    (ক) প্রতি commit-এ ২০০ + ৩০ + ৫০০ = ৭৩০ KB; ২০ × ৭৩০ = ১৪,৬০০ KB (৬০টি ফাইল-কপি)।

    (খ) logo ১টি blob (২০০ KB); app.js ২০টি সংস্করণ = ২০ × ৩০ = ৬০০ KB; data.csv ৪টি সংস্করণ (commit ১, ৫, ১০, ১৫) = ৪ × ৫০০ = ২,০০০ KB. মোট ১ + ২০ + ৪ = ২৫টি blob, ২,৮০০ KB — (ক)-এর তুলনায় ১ − ২,৮০০/১৪,৬০০ ≈ ৮০.৮% সাশ্রয়।

    (গ) logo ২০০; app.js ৩০ + ১৯ × ১ = ৪৯; data.csv ৫০০ + ৩ × ৫ = ৫১৫. মোট ৭৬৪ KB. সাশ্রয় ১ − ৭৬৪/১৪,৬০০ ≈ ৯৪.৮%; (খ)-এর তুলনায় ৭৬৪/২,৮০০ ≈ ২৭.৩%, অর্থাৎ প্রায় চার ভাগের এক ভাগ। বাস্তবে zlib আরও কমাবে; কিন্তু logo.png-এর মতো আগেই সংকুচিত binary ফাইলে zlib বা delta প্রায় কাজ করে না — বড় binary ঘনঘন বদলালে তাই Git LFS বিবেচনা করুন (পর্ব ১৯)।

  4. সমালোচনা করুন: একজন সহকর্মী দলের wiki-তে “repository ছোট রাখার নিয়ম” লিখেছেন: “(১) প্রতি সপ্তাহে .git/objects-এর যেসব ফোল্ডার এক মাসের পুরোনো, মুছে দিন। (২) Branch-এর ID দেখতে cat .git/refs/heads/<নাম> ব্যবহার করুন। (৩) CI script-এ বর্তমান branch-এর নাম বের করতে git branch-এর আউটপুট থেকে *-ওয়ালা লাইন কাটুন।” প্রতিটি নিয়মের সমস্যা কী, আর বদলে কী লিখবেন?

    (১) বিপজ্জনক। Loose object-এর ফোল্ডারের নাম (ce/, 65/) ID-র প্রথম দুই অক্ষর, সময় নয় — একটি ফোল্ডারে আজকের আর পুরোনো দুই রকম object থাকতে পারে। পুরোনো object মানেই অদরকারি নয়: প্রথম commit-এর blob আজও প্রতিটি commit-এর অংশ হতে পারে (যেমন কখনো না-বদলানো logo.png). মুছলে git fsck “missing blob” দেখাবে, checkout ব্যর্থ হবে। বদলে: Git-কে নিজের gc --auto/git maintenance করতে দিন; দরকার হলে git gc, আর আকারের কারণ খুঁজতে git count-objects -v. বড় binary-র সমাধান Git LFS, আর ইতিহাস থেকে বড় ফাইল সরানো একটি পরিকল্পিত, দলীয় সিদ্ধান্ত (পর্ব ৫৭)।

    (২) ভঙ্গুর। git gc-এর পরে ref packed-refs-এ চলে যেতে পারে, আর নতুন Git-এ reftable ভাণ্ডার থাকলে refs/heads/-এ কোনো ফাইলই নেই। বদলে: git rev-parse <নাম> বা git show-ref.

    (৩) ভঙ্গুর। git branch মানুষের জন্য সাজানো porcelain আউটপুট — রঙ, detached HEAD-এ “(HEAD detached at …)” লেখা, বা ভবিষ্যৎ পরিবর্তনে script নিঃশব্দে ভুল করবে। বদলে: git symbolic-ref --short HEAD (detached হলে error দেয়, যা ধরা যায়) বা git rev-parse --abbrev-ref HEAD. সাধারণ নীতি: script-এ plumbing বা --porcelain/--format আউটপুট, আর .git-এর ভেতরে হাত নয়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমশুধু plumbing দিয়ে একটি নতুন ফাইলসহ commit বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📄 git hash-object -w — বিষয়বস্তু blob হিসেবে জমা
  2. 📝 git update-index --add --cacheinfo — index-এ path আর blob জোড়া
  3. 🗂️ git write-tree — index থেকে tree
  4. 📸 git commit-tree <tree> -p HEAD — tree থেকে commit
  5. 🏷️ git update-ref refs/heads/main <commit> — branch সরানো

Git-এর একটি blob object-এ কী কী থাকে?

  • ফাইলের নাম, mode আর বিষয়বস্তু
  • শুধু ফাইলের বিষয়বস্তু (ধরন আর আকারের ছোট header-সহ)
  • বিষয়বস্তু আর ফাইল শেষবার বদলানোর সময়
  • আগের সংস্করণের সাথে পার্থক্য (diff)

একটি ফাইলে আছে শুধু hello আর একটি newline (মোট ৬ byte). Git এর blob ID হিসাব করতে কোন byte-গুলো SHA-1-এ দেয়?

  • শুধু hello\n
  • greeting.txt নাম আর hello\n
  • "blob 6", তারপর একটি শূন্য byte, তারপর hello\n
  • zlib-এ সংকুচিত ২১ byte

একটি commit-এ docs/a.txt আর backup/a-copy.txt দুটি ফাইলের বিষয়বস্তু হুবহু এক। Object ভাণ্ডারে এদের জন্য কতটি blob থাকবে?

  • একটি
  • দুটি, কারণ ফোল্ডার আলাদা
  • দুটি, যদি না git gc চালানো হয়
  • একটিও না, tree-র ভেতরে সরাসরি থাকে

git verify-pack -v-এর একটি লাইন: 2f2f909… blob 14 25 1739 1 5b0a247…. শেষ দুটি ঘরের অর্থ কী?

  • blob-টির parent commit-এর সংখ্যা আর ID
  • pack-এর ভেতরে শুরু আর শেষের অবস্থান
  • কতটি pack-এ blob-টি আছে, আর প্রথম pack-এর নাম
  • Delta chain-এর গভীরতা ১, আর যে object-এর সাপেক্ষে delta তার ID

নিচের কোন command .git/objects-এ কিছুই লেখে না?

  • git hash-object -w notes.txt
  • git hash-object notes.txt
  • git write-tree
  • git commit-tree 68bac8f -p HEAD -m "x"

আগের commit-এর কোনো ফাইল না বদলে শুধু বার্তা বদলাতে git commit --amend -m "নতুন বার্তা" চালানো হলো। কী ঘটে?

  • একই commit ID থাকে, শুধু বার্তা বদলায়
  • নতুন commit ID আর নতুন tree ID
  • নতুন commit ID, কিন্তু একই tree ID
  • সব blob নতুন করে বানানো হয়, তাই সব ID বদলায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Git-এর ভিত একটি content-addressed key–value ভাণ্ডার: ID = SHA-1("<type> <size>\0<content>").
  2. blob শুধু বিষয়বস্তু; tree-তে mode, নাম আর ID; commit-এ tree, parent, author, committer, বার্তা; annotated tag আলাদা object.
  3. git add = blob লেখা + index; git commit = write-tree + commit-tree + update-ref.
  4. একই বিষয়বস্তু একবারই থাকে; git gc object-কে packfile-এ ভরে delta আর zlib দিয়ে জায়গা বাঁচায় — মডেলে snapshot, ডিস্কে delta.
  5. SHA-1-এর ওপর আক্রমণের জবাবে hardened SHA-1; SHA-256 repository আছে, কিন্তু আদান-প্রদান আর টুলের সমর্থন এখনো সীমিত।

পরের পর্বে: এখন আপনি জানেন একটি commit কোনো ref থেকে পৌঁছানো না গেলেও ভাণ্ডারে থেকে যায়। পর্ব ৫৩-এ সেই “হারানো” commit reflog আর fsck দিয়ে খুঁজে ফিরিয়ে আনব, আর দেখব garbage collection কখন, কী মুছে ফেলে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান