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

প্রথম Git repository — init, add, commit আর তিনটি এলাকা

Your First Git Repository — init, add, commit and the Working Tree, Index and Repository

Taught in Bangla, this lesson creates a first repository with git init, explains the working tree, staging area and repository, the four file states and reading git status. It covers git add including add -p, add . versus add -A, git commit and the commit -a trap, git rm and git mv, what a commit object stores and how its ID is derived, with runnable models and real Git commands.

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

  • git init কী বানায়, .git ফোল্ডারে মোটা দাগে কী থাকে, আর -b main কেন
  • Working tree, staging area আর repository — তিনটি এলাকা আর তাদের মধ্যে file-এর যাতায়াত
  • File-এর অবস্থা: untracked, unmodified, modified, staged — আর git status ও git status -s পড়া
  • git add, add -p, add . বনাম add -A, git commit, commit -a-র ফাঁদ, git rm, git mv
  • একটি commit-এর ভেতরে কী থাকে, commit ID কীভাবে তৈরি হয়, আর ছোট এক-উদ্দেশ্যের commit কেন

১ · git init — একটি সাধারণ ফোল্ডার থেকে repository

পর্ব ০৭-এ (version control আর Git setup) আপনি Git install করেছেন আর নিজের নাম-email সেট করেছেন। এবার আসল কাজ। এই পর্বে আমরা একটি কাল্পনিক অনলাইন বাজার অ্যাপের ছোট একটি project শুরু করব — নাম bazar. শুরুতে এটি একটি সাধারণ ফোল্ডার। Git জানেই না এর অস্তিত্ব আছে।

একটি ফোল্ডারকে Git-এর repository বানানোর command একটিই: git init. Repository মানে সেই জায়গা, যেখানে project-এর সব file আর তাদের পুরো পরিবর্তনের ইতিহাস জমা থাকে। নিচের সব আউটপুট এই পর্বের জন্য একটি খালি scratch ফোল্ডারে আসল Git ২.৫৩ চালিয়ে নেওয়া; লম্বা path আমরা … দিয়ে ছোট করেছি।

$ git init -b main bazar
Initialized empty Git repository in …/bazar/.git/
$ cd bazar
$ git config user.name "Rahim"
$ git config user.email "rahim@example.com"
$ git config core.autocrlf false
$ ls -A
.git
$ ls .git
HEAD
config
description
hooks
info
objects
refs
$ git status
On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)

তিনটি জিনিস খেয়াল করুন। এক, git init-এর পরে ফোল্ডারে দৃশ্যমান কিছুই বদলায়নি; শুধু একটি লুকানো .git ফোল্ডার এসেছে (Linux ও macOS-এ নাম ডট দিয়ে শুরু হলে সেটি লুকানো থাকে, তাই ls -A লাগল)। দুই, git status বলছে No commits yet — repository আছে, কিন্তু ইতিহাস এখনো শূন্য। তিন, আমরা user.name আর user.email শুধু এই repository-র জন্য সেট করেছি; নিজের কম্পিউটারে সাধারণত পর্ব ০৭-এর মতো --global দিয়ে একবারই সেট করা হয়। শেষ লাইনের core.autocrlf false শুধু এই উদাহরণের জন্য — Windows আর Linux-এ line ending-এর সতর্কবার্তা যাতে আলাদা না দেখায় (বিস্তারিত পর্ব ১০-এ)।

.git ফোল্ডারে কী থাকে — মোটা দাগে

.git ফোল্ডারটিই আসল repository. আপনার project ফোল্ডারের বাকি সবকিছু শুধু “এখন যে কপিতে কাজ চলছে”। ভেতরের নামগুলোর মোটামুটি অর্থ:

নামমোটা দাগে কাজ
objects/সব file-এর বিষয়বস্তু, ফোল্ডারের তালিকা আর commit — প্রতিটি তার নিজের hash নামে রাখা (পর্ব ৫২)
refs/Branch আর tag-এর নাম, প্রতিটি একটি commit-এর দিকে নির্দেশ করে (পর্ব ১২)
HEADএখন আপনি কোন branch-এ আছেন, তার একটি ছোট লেখা
configশুধু এই repository-র সেটিং (উপরের git config এখানেই লিখেছে)
hooks/নির্দিষ্ট ঘটনায় নিজে থেকে চলা script-এর জায়গা (পর্ব ৫৬)
indexStaging area — প্রথম git add-এর পরে তৈরি হয়, তাই উপরের তালিকায় এখনো নেই

.git ফোল্ডার হাতে বদলাবেন না, আর মুছবেন না। এটি মুছে ফেললে ফোল্ডারের file-গুলো থেকে যাবে, কিন্তু সব commit আর ইতিহাস হারিয়ে যাবে — অন্য কোথাও (যেমন একটি remote-এ, পর্ব ১৪) কপি না থাকলে আর ফেরানো যাবে না। উল্টো দিকে, একটি project-এর কপি অন্য কাউকে দিতে চাইলে .git-সহ পুরো ফোল্ডার দিলে ইতিহাসও সাথে যায়।

-b main কেন — প্রথম branch-এর নাম

প্রতিটি repository একটি branch দিয়ে শুরু হয় — branch মানে কাজের একটি ধারা, যার বিস্তারিত পর্ব ১২-এ (branch আর HEAD). কোনো সেটিং না থাকলে Git ২.৫৩ প্রথম branch-এর নাম রাখে master, আর সাথে একটি লম্বা পরামর্শ দেখায়:

$ git init demo-plain
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint: 	git config --global init.defaultBranch <name>
…
Initialized empty Git repository in …/demo-plain/.git/

বার্তাটি নিজেই বলছে, Git ৩.০-তে এই default branch-এর নাম main হবে। বড় hosting platform-গুলোও লেখার সময়ে নতুন repository-তে main ব্যবহার করে। দুই উপায়ে আগেই main বেছে নিতে পারেন: প্রতিবার git init -b main (পুরো রূপ --initial-branch=main), অথবা একবার git config --global init.defaultBranch main. দুটোই Git ২.২৮ (২০২০) থেকে আছে; এর আগের Git-এ init-এর পরে git branch -m main দিয়ে নাম বদলাতে হতো। নামটি শুধু একটি লেবেল — master নামেও Git একই রকম কাজ করে।

২ · তিনটি এলাকা — খসড়া খাতা, বাছাই করা পাতা আর ফেয়ার কপি

Git শেখার সবচেয়ে গুরুত্বপূর্ণ ধারণা এটিই। এটি বুঝলে পরের সব command — diff, restore, reset — অনেক সহজ হয়ে যায়।

পরীক্ষার হলে একজন পরীক্ষার্থীর কথা ভাবুন। তাঁর কাছে তিনটি জিনিস। খসড়া খাতা — যেখানে তিনি যা খুশি লেখেন, কাটেন, আবার লেখেন। ফেয়ার কপির জন্য বাছাই করা উত্তর — খসড়া থেকে যে অংশগুলো তিনি ঠিক করেছেন যে খাতায় তুলবেন। আর জমা দেওয়া খাতা — একবার পরিদর্শকের হাতে গেলে সেটি স্থায়ী; পরে নতুন পাতা জোড়া যায়, কিন্তু জমা দেওয়া পাতা বদলানো যায় না। খসড়ার সব কাটাকাটি ফেয়ার কপিতে যায় না; তিনি বেছে বেছে তোলেন। Git-এর তিনটি এলাকা ঠিক এরকম।

  • Working tree (খসড়া খাতা): আপনার project ফোল্ডারের সাধারণ file — editor-এ যেগুলো খোলেন আর বদলান। Git এগুলো দেখে, কিন্তু নিজে থেকে কিছু জমা রাখে না।
  • Staging area বা index (বাছাই করা পাতা): পরের commit-টি ঠিক কেমন হবে, তার একটি প্রস্তুত তালিকা — প্রতিটি file-এর কোন version যাবে। এটি .git/index নামের একটি file.
  • Repository (জমা দেওয়া খাতা): .git-এর ভেতরে সব commit. HEAD দেখায় এখন কোন commit-এর ওপর আপনি দাঁড়িয়ে আছেন — সাধারণত বর্তমান branch-এর শেষ commit.

তিনটি এলাকা আর তাদের মধ্যে যাতায়াত

Working tree → (add) → staging area → (commit) → repository

Working tree, staging area আর repository — add, commit আর restore Working tree কাজের ফোল্ডার খসড়া খাতা Staging area (index) বাছাই করা পাতা Repository (.git-এর commit) জমা দেওয়া খাতা git add git commit restore --staged git restore দাগ-কাটা তীর: ফেরার পথ (পর্ব ১১) commit-এ যায় শুধু index-এর কপি
ছবি ৮.১ — git add ফোল্ডারের বর্তমান কপি index-এ তোলে; git commit index-এর পুরো কপিকে একটি স্থায়ী commit বানায়। দাগ-কাটা পথে ফেরা যায়: git restore --staged শেষ commit-এর কপি index-এ ফেরায়, git restore index-এর কপি দিয়ে ফোল্ডারের বদল মুছে দেয় (সাবধান — এটি আপনার না-তোলা বদল ফেলে দেয়)। ফেরার পুরো গল্প পর্ব ১১-এ।
মূল ধারণা

একই file-এর একসাথে তিনটি আলাদা কপি থাকতে পারে: ফোল্ডারে একটি, index-এ একটি, শেষ commit-এ একটি। git status আসলে দুটি তুলনা করে বলে — index বনাম শেষ commit (কী commit-এর জন্য তৈরি) আর ফোল্ডার বনাম index (কী এখনো তোলা হয়নি)।

কথাটি কোডে দেখা যাক। নিচের ছোট মডেলে তিনটি file-এর জন্য তিনটি এলাকা রাখা আছে — W (working tree), I (index), H (HEAD commit). add ফোল্ডার থেকে index-এ কপি করে, commit পুরো index কপি করে। প্রতি ধাপে প্রতিটি file-এর পাশে git status -s-এর মতো দুই অক্ষরের সংকেত ছাপা হয় (প্রথম অক্ষর index বনাম HEAD, দ্বিতীয় অক্ষর ফোল্ডার বনাম index). এটি Git-এর আসল কোড নয়, শুধু ধারণাটির মডেল।

// Three areas: working tree (W), staging area / index (I), last commit HEAD (H).
const work = {}, index = {}; let head = {};
function state(f) {                       // the two letters of "git status -s"
  if (!(f in index)) return "?? untracked";
  const x = !(f in head) ? "A" : index[f] !== head[f] ? "M" : " ";
  const y = !(f in work) ? "D" : work[f] !== index[f] ? "M" : " ";
  const name = { "A ": "new, staged", "AM": "new, staged + edited again",
    "M ": "modified, staged", " M": "modified, not staged",
    "MM": "staged + edited again", "  ": "unmodified" }[x + y];
  return x + y + " " + name;
}
function show(cmd) {
  console.log("$ " + cmd);
  for (const f of Object.keys(work).sort())
    console.log(`  ${f}  W=${work[f]}  I=${index[f] ?? "--"}  H=${head[f] ?? "--"}  ${state(f)}`);
}
const add = (...fs) => fs.forEach(f => { index[f] = work[f]; });
const commit = () => { head = { ...index }; };   // commit copies the index, not W

for (const f of ["a.txt", "b.txt", "c.txt"]) work[f] = "v1";
show("create a.txt b.txt c.txt");
add("a.txt", "b.txt");      show("git add a.txt b.txt");
work["b.txt"] = "v2";       show("edit b.txt");
commit();                   show('git commit -m "first"');
add("b.txt", "c.txt");      show("git add b.txt c.txt");
commit();                   show('git commit -m "second"');
work["a.txt"] = "v2";       show("edit a.txt");

আউটপুট:

$ create a.txt b.txt c.txt
  a.txt  W=v1  I=--  H=--  ?? untracked
  b.txt  W=v1  I=--  H=--  ?? untracked
  c.txt  W=v1  I=--  H=--  ?? untracked
$ git add a.txt b.txt
  a.txt  W=v1  I=v1  H=--  A  new, staged
  b.txt  W=v1  I=v1  H=--  A  new, staged
  c.txt  W=v1  I=--  H=--  ?? untracked
$ edit b.txt
  a.txt  W=v1  I=v1  H=--  A  new, staged
  b.txt  W=v2  I=v1  H=--  AM new, staged + edited again
  c.txt  W=v1  I=--  H=--  ?? untracked
$ git commit -m "first"
  a.txt  W=v1  I=v1  H=v1     unmodified
  b.txt  W=v2  I=v1  H=v1   M modified, not staged
  c.txt  W=v1  I=--  H=--  ?? untracked
$ git add b.txt c.txt
  a.txt  W=v1  I=v1  H=v1     unmodified
  b.txt  W=v2  I=v2  H=v1  M  modified, staged
  c.txt  W=v1  I=v1  H=--  A  new, staged
$ git commit -m "second"
  a.txt  W=v1  I=v1  H=v1     unmodified
  b.txt  W=v2  I=v2  H=v2     unmodified
  c.txt  W=v1  I=v1  H=v1     unmodified
$ edit a.txt
  a.txt  W=v2  I=v1  H=v1   M modified, not staged
  b.txt  W=v2  I=v2  H=v2     unmodified
  c.txt  W=v1  I=v1  H=v1     unmodified

সবচেয়ে শিক্ষণীয় লাইন চতুর্থ ধাপে। b.txt-কে আমরা v1 অবস্থায় add করেছিলাম, তারপর ফোল্ডারে v2 করেছি (AM — নতুন file staged, আবার বদলানো)। প্রথম commit-এর পরে দেখুন: H=v1, কিন্তু W=v2. অর্থাৎ commit নিয়েছে index-এর v1, ফোল্ডারের v2 নয় — তাই b.txt এখন " M", modified কিন্তু staged নয়। c.txt কখনো add হয়নি বলে প্রথম commit-এ নেই, ?? থেকে গেছে। দ্বিতীয় commit-এর পরে তিন এলাকা মিলে গেছে, সব file unmodified. শেষ ধাপে a.txt ফোল্ডারে বদলাতেই আবার " M".

৩ · File-এর চার অবস্থা আর git status

Git-এর চোখে project ফোল্ডারের প্রতিটি file হয় untracked, নয়তো tracked. Tracked মানে file-টি শেষ commit-এ আছে, বা অন্তত index-এ তোলা হয়েছে। Tracked file আবার তিন রকম হতে পারে:

অবস্থামানেstatus -s-এ
Untrackedফোল্ডারে আছে, কিন্তু Git কখনো নেয়নি — index-এও নেই, commit-এও নেই??
UnmodifiedTracked, আর তিন কপিই একইকিছু দেখায় না
Modifiedফোল্ডারের কপি index-এর কপি থেকে আলাদাদ্বিতীয় ঘরে M: " M"
StagedIndex-এর কপি শেষ commit থেকে আলাদা — পরের commit-এ যাবেপ্রথম ঘরে M বা নতুন হলে A

এক file একসাথে staged আর modified দুটোই হতে পারে — MM বা AM; তখন index-এ এক version, ফোল্ডারে আরেক। এবার আসল Git-এ দেখি। ফোল্ডারে তিনটি file লিখলাম — README.md, index.html আর app.js:

$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	README.md
	app.js
	index.html

nothing added to commit but untracked files present (use "git add" to track)
$ git status -s
?? README.md
?? app.js
?? index.html

লম্বা রূপে git status প্রতিটি অংশের নিচে বন্ধনীতে পরের সম্ভাব্য command-ও বলে দেয় — নতুনদের জন্য এটি দারুণ সহায়ক। ছোট রূপ git status -s (--short) প্রতি file-এ এক লাইন। এবার দুটি file add করি, তৃতীয়টি নয়:

$ git add README.md app.js
$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   README.md
	new file:   app.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	index.html

এখন app.js-এ একটি নতুন জিনিস ("tel") যোগ করলাম, কিন্তু আবার add করিনি:

$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   README.md
	new file:   app.js

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   app.js

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	index.html
$ git status -s
A  README.md
AM app.js
?? index.html

app.js এখন দুই জায়গায়: “Changes to be committed”-এ (index-এ পুরোনো version) আর “Changes not staged”-এ (ফোল্ডারে নতুন version). ছোট রূপে AM: প্রথম অক্ষর A — index-এ নতুন যোগ হয়েছে; দ্বিতীয় অক্ষর M — ফোল্ডারে তার পরে আবার বদলেছে। একটি ছোট খুঁটিনাটি: প্রথম commit-এর আগে unstage করার পরামর্শ git rm --cached, প্রথম commit-এর পরে সেটি হয়ে যায় git restore --staged — কারণ তখন ফেরার মতো একটি commit আছে।

নিচের stepper-এ একটি file-এর পুরো জীবনচক্র ধাপে ধাপে দেখুন। প্রতি ধাপে তিন এলাকায় app.js-এর কোন version আছে, আর git status -s কী দেখাবে:

app.js — কোন এলাকায় কোন version
Working tree
Staging area
Repository (HEAD)
git status -s
অবস্থা
  1. নতুন file. Editor-এ app.js লিখে save করলেন (version v1). File শুধু ফোল্ডারে; index-এ নেই, কোনো commit-ও নেই। Git একে untracked বলে — ??. এই অবস্থায় file হারালে Git কিছুই ফেরাতে পারবে না।
  2. git add app.js. ফোল্ডারের v1-এর একটি কপি index-এ উঠল। এখন file tracked আর staged; সংকেত A (added) প্রথম ঘরে, দ্বিতীয় ঘর ফাঁকা — কারণ ফোল্ডার আর index-এর কপি একই।
  3. git commit -m "…". Index-এর পুরো কপি থেকে প্রথম commit C1 তৈরি হলো, তাতে v1. তিন এলাকাই এখন v1 — file unmodified, তাই git status -s এর জন্য কোনো লাইনই দেখায় না।
  4. File বদলালেন. ফোল্ডারে v2. Index আর C1-এ এখনো v1. ফোল্ডার বনাম index আলাদা, তাই দ্বিতীয় ঘরে M: " M" — modified, কিন্তু staged নয়। এখন commit করলে কিছুই যাবে না।
  5. আবার git add app.js. v2 index-এ উঠল। এখন index বনাম C1 আলাদা, তাই প্রথম ঘরে M: "M " — staged. ফোল্ডার আর index একই, তাই দ্বিতীয় ঘর ফাঁকা।
  6. আবার commit. নতুন commit C2-তে v2; C2-এর parent C1. তিন এলাকা আবার মিলে গেল — unmodified. জীবনচক্র এভাবেই ঘোরে: বদল → add → commit.

৪ · git add — কোন file, কোন অংশ, কোন ফোল্ডার

git add-এর নাম একটু বিভ্রান্তিকর। এটি শুধু “নতুন file যোগ” নয়; এর আসল অর্থ হলো এই file-এর এখনকার বিষয়বস্তু পরের commit-এর জন্য index-এ তোলা। নতুন file, বদলানো file — দুটোতেই একই command. কয়েকটি রূপ:

Commandযা index-এ ওঠে (Git ২.x)
git add app.jsশুধু ওই file-এর বর্তমান বিষয়বস্তু
git add css/ওই ফোল্ডারের ভেতরের সব নতুন, বদলানো আর মুছে ফেলা file
git add .বর্তমান ফোল্ডার আর তার নিচের সব বদল — নতুন, বদলানো, মুছে ফেলা
git add -Aপুরো working tree-র সব বদল, আপনি যে ফোল্ডারেই থাকুন
git add -uশুধু আগে থেকে tracked file-এর বদল ও মুছে ফেলা; নতুন file নয়
git add -pপ্রতিটি বদলের টুকরো দেখে আপনি বেছে বেছে তোলেন

git add . বনাম git add -A

পুরোনো টিউটোরিয়ালে প্রায়ই লেখা থাকে, “add . মুছে ফেলা file নেয় না, তাই -A দিন।” কথাটি Git ১.x-এর জন্য সত্যি ছিল। Git ২.০ (২০১৪) থেকে git add . মুছে ফেলা file-ও নেয়। এখন দুটির পার্থক্য শুধু পরিধিতে: . মানে “এই ফোল্ডার আর তার নিচে”, আর -A মানে “পুরো project”. Project-এর মূল ফোল্ডারে দাঁড়িয়ে দুটো একই কাজ করে। নিচে আগে README.md-এ একটি লাইন যোগ করা হয়েছে, নতুন file css/style.css আর discount.js লেখা হয়েছে; তারপর css ফোল্ডারের ভেতর থেকে add .:

$ cd css
$ git add .
$ git status -s
 M ../README.md
A  style.css
?? ../discount.js
$ cd ..
$ git add -A
$ git status -s
M  README.md
A  css/style.css
A  discount.js
$ git commit -m "Add stylesheet, discount and run note"
[main 5903454] Add stylesheet, discount and run note
 3 files changed, 5 insertions(+)
 create mode 100644 css/style.css
 create mode 100644 discount.js

css-এর ভেতর থেকে add . শুধু style.css তুলেছে (git status -s path-ও বর্তমান ফোল্ডার থেকে দেখায়, তাই ../README.md). মূল ফোল্ডারে ফিরে -A সব তুলেছে। সতর্কতা: add . বা add -A চোখ বন্ধ করে দিলে log file, password-এর file, বিশাল build ফাইল — সব উঠে যেতে পারে। তাই আগে git status দেখুন, আর যা কখনো track করবেন না তা .gitignore-এ লিখুন — পর্ব ১০ (.gitignore আর git config).

git add -p — একই file-এর অংশবিশেষ তোলা

ধরুন prices.js file-এ আপনি একসাথে দুটি আলাদা কাজ করেছেন: চালের দাম ৭০ থেকে ৭৫ টাকা করেছেন (আসল কাজ), আর একটি debug লাইন বসিয়েছেন (সাময়িক, commit-এ যাওয়ার কথা নয়)। git add prices.js দিলে দুটোই উঠবে। git add -p (--patch) file-এর বদলকে hunk-এ ভাগ করে — hunk মানে পাশাপাশি বদলানো লাইনের একটি টুকরো — আর প্রতিটির জন্য জিজ্ঞেস করে, “এটি তুলব?” নিচের stepper-এ আসল Git-এর আউটপুট দেখুন:

git diffফোল্ডার বনাম index: দুটি hunk
@@ -1,6 +1,6 @@
 // prices.js - price list for the bazar
 const prices = {
-  chal: 70,
+  chal: 75,
   dal: 120,
   tel: 180,
 };
@@ -14,6 +14,7 @@ function total(cart) {
 }

 function format(amount) {
+  console.log("debug", amount);
   return amount + " taka";
 }
$ git add -p prices.jsপ্রথম hunk — উত্তর y
@@ -1,6 +1,6 @@
 // prices.js - price list for the bazar
 const prices = {
-  chal: 70,
+  chal: 75,
   dal: 120,
   tel: 180,
 };
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? y
git add -p (চলছে)দ্বিতীয় hunk — উত্তর n
@@ -14,6 +14,7 @@ function total(cart) {
 }

 function format(amount) {
+  console.log("debug", amount);
   return amount + " taka";
 }

(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? n
$ git status -s · git diff --cachedএকই file, দুই অবস্থা
$ git status -s
MM prices.js
$ git diff --cached
…
@@ -1,6 +1,6 @@
 // prices.js - price list for the bazar
 const prices = {
-  chal: 70,
+  chal: 75,
   dal: 120,
   tel: 180,
 };
commit, তারপর debug লাইন বাদশুধু দামের বদল commit-এ
$ git commit -m "Raise rice price to 75 taka"
[main 37c2b34] Raise rice price to 75 taka
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git status -s
 M prices.js
$ git restore prices.js
$ git status -s
(কিছু নেই — debug লাইন ফোল্ডার থেকেও মুছে গেছে)
  1. দুটি বদল, দুটি hunk. git diff দেখাচ্ছে ফোল্ডারের কপি index থেকে কোথায় আলাদা। লাইন ৩-এর দাম আর লাইন ১৭-এর debug লাইন অনেক দূরে, তাই Git দুটিকে আলাদা hunk ধরেছে। @@ -1,6 +1,6 @@ মানে পুরোনো file-এর লাইন ১ থেকে ৬টি লাইন, নতুনেও লাইন ১ থেকে ৬টি।
  2. প্রথম hunk: y. git add -p প্রথম hunk দেখিয়ে জিজ্ঞেস করল। দামের বদলটি আমরা চাই, তাই উত্তর y (yes), অর্থাৎ হ্যাঁ। বন্ধনীর অক্ষরগুলো আরও বিকল্প: n মানে না, q মানে এখানেই বেরিয়ে যাওয়া, s মানে hunk আরও ছোট টুকরোয় ভাঙা (যদি ভাঙা যায়), e মানে হাতে সম্পাদনা, ? মানে সাহায্য। এখানে তালিকায় s নেই, কারণ এই hunk-এ একটিই বদলের জায়গা, আর ভাঙার সুযোগ নেই। তালিকাটি version-ভেদেও সামান্য আলাদা হয়।
  3. দ্বিতীয় hunk: n. Debug লাইনটি commit-এ চাই না, তাই n. এটি ফোল্ডারে থেকেই গেল, শুধু index-এ উঠল না। তালিকায় এবার k, j নেই — এগুলো আগের বা পরের সিদ্ধান্ত-না-নেওয়া hunk-এ যায়, আর প্রথম hunk-এর সিদ্ধান্ত আগেই হয়ে গেছে; বড় হাতের K, J দিয়ে তবু আগের বা পরের যেকোনো hunk-এ যাওয়া যায়।
  4. MM — একই file, দুই অবস্থা। প্রথম M: index-এ দামের বদল (staged). দ্বিতীয় M: ফোল্ডারে আরও একটি বদল (debug লাইন), যা তোলা হয়নি। git diff --cached (index বনাম শেষ commit) দেখায় commit-এ ঠিক কী যাবে — শুধু দামের লাইন। Commit-এর আগে এই command চালানো একটি ভালো অভ্যাস; বিস্তারিত পর্ব ০৯-এ।
  5. Commit-এ শুধু দাম। Commit 37c2b34-এ একটি লাইন বদল (১টি insertion, ১টি deletion). Debug লাইন তখনো ফোল্ডারে (" M"). git restore prices.js ফোল্ডারের কপিকে index-এর কপিতে ফিরিয়ে debug লাইনটি ফেলে দিল। সাবধান: restore না-তোলা বদল চিরতরে মুছে দেয় — পর্ব ১১-এ বিস্তারিত।

add -p-এর মূল্য বোঝা যায় যখন একটি file-এ একসাথে একটি bug fix আর একটি নতুন feature-এর কাজ মিশে যায়। দুটোকে দুটি আলাদা commit-এ রাখলে পরে একটিকে ফেরানো (revert, পর্ব ১১) বা review করা (পর্ব ৪৪) অনেক সহজ। VS Code-সহ অনেক editor-এ একই কাজ হয় লাইন বেছে “Stage Selected Ranges” জাতীয় বাটনে — নাম tool-ভেদে আলাদা।

৫ · git commit — একটি commit কী কী মনে রাখে

আমাদের bazar project-এ ফিরি। app.js আর index.html-ও add করে প্রথম commit:

$ git add app.js index.html
$ git status -s
A  README.md
A  app.js
A  index.html
$ git commit -m "Add product list page"
[main (root-commit) 8bfb77a] Add product list page
 3 files changed, 7 insertions(+)
 create mode 100644 README.md
 create mode 100644 app.js
 create mode 100644 index.html
$ git status
On branch main
nothing to commit, working tree clean
$ git log --oneline
8bfb77a Add product list page

Commit-এর আউটপুটের প্রথম লাইনটি পড়তে শিখুন: [main (root-commit) 8bfb77a] মানে branch main-এ, এটি root commit (প্রথম commit, যার আগে কিছু নেই), আর তার ID-র ছোট রূপ 8bfb77a. তারপর বার্তা, কয়টি file আর কয়টি লাইন বদলেছে। create mode 100644 মানে নতুন একটি সাধারণ file (চালানো যায় এমন file হলে 100755). এরপর git status বলছে working tree clean — তিন এলাকা মিলে গেছে।

একটি commit-এর ভেতরে উঁকি

git cat-file -p একটি Git object-এর ভেতরের লেখা হুবহু দেখায় (internals-এর বিস্তারিত পর্ব ৫২-এ, আজ শুধু উঁকি):

$ git cat-file -p HEAD
tree 9bb98eca3b3ee398a745f766c1639c8698cf6305
author Rahim <rahim@example.com> 1768449660 +0600
committer Rahim <rahim@example.com> 1768449660 +0600

Add product list page
$ git rev-parse HEAD
8bfb77a54601e9294b83f3f19c4519d36a524183
$ git rev-parse --short HEAD
8bfb77a

একটি commit আসলে এটুকুই — একটি ছোট লেখা:

  • tree: project-এর পুরো snapshot-এর ID — সেই মুহূর্তে index-এ থাকা সব file আর ফোল্ডারের তালিকা। শুধু পার্থক্য নয়, পুরো ছবি।
  • parent: আগের commit-এর ID. প্রথম commit-এ এই লাইনই নেই — উপরে দেখুন। Merge commit-এ (পর্ব ১৩) একাধিক parent থাকে।
  • author: যিনি পরিবর্তনটি মূলত লিখেছেন, সাথে সময় (1768449660 হলো ১৯৭০ থেকে গোনা সেকেন্ড, এখানে ১৫ জানুয়ারি ২০২৬, সকাল ১০টা ১ মিনিট, ঢাকার সময়) আর সময় অঞ্চল +0600.
  • committer: যিনি commit-টি repository-তে বসিয়েছেন। সাধারণত author-ই; আলাদা হয় যখন কেউ অন্যের পাঠানো patch বসান, বা পরে commit নতুন করে সাজানো হয় (rebase, পর্ব ১৫)।
  • বার্তা: একটি ফাঁকা লাইনের পরে commit message.

এই পুরো লেখাটিকে Git একটি hash function-এ দেয় — ঐতিহ্যগতভাবে SHA-1, আরও সঠিকভাবে বললে "commit <size>\0" শিরোনাম জুড়ে তার ওপর। ফল ৪০ অক্ষরের hexadecimal সংখ্যা, এটিই commit hash বা commit ID: 8bfb77a…. দৈনন্দিন কাজে প্রথম কয়েকটি অক্ষরই যথেষ্ট — Git সাধারণত অন্তত ৭টি দেখায়, আর repository বড় হলে দরকারমতো বেশি, যাতে দুটি commit-এর ছোট রূপ মিলে না যায়। নতুন Git-এ চাইলে git init --object-format=sha256 দিয়ে SHA-256 repository-ও বানানো যায় (তখন ID ৬৪ অক্ষরের); তবে লেখার সময়ে বেশিরভাগ repository SHA-1-এই চলে।

Author আর committer যেহেতু commit-এর লেখার অংশ, তাই আমরা এই পর্বের প্রতিটি commit-এর আগে সময় নির্দিষ্ট করে দিয়েছি (GIT_AUTHOR_DATE আর GIT_COMMITTER_DATE, প্রতিবার এক মিনিট এগিয়ে)। একই নাম, email, সময়, বার্তা আর file দিয়ে আপনি নিজে চালালে হুবহু একই hash পাবেন। সময় নির্দিষ্ট না করলে আপনার hash আলাদা আসবে — সেটাই স্বাভাবিক।

লম্বা বার্তা — editor দিয়ে commit

-m ছাড়া শুধু git commit দিলে Git আপনার editor খোলে (পর্ব ০৭-এ যেটি সেট করেছেন; না করলে প্রায়ই Vim). তাতে একটি খসড়া থাকে:


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
#	modified:   app.js
#

# দিয়ে শুরু হওয়া লাইন বাদ যায়; বার্তা ফাঁকা রেখে বন্ধ করলে commit বাতিল হয়। প্রথম লাইনে একটি ছোট সারাংশ, তারপর একটি ফাঁকা লাইন, তারপর “কেন” — এই কাঠামোয় লিখে save করে বন্ধ করলাম (terminal-এ editor খোলা থাকার সময় Git একটি “Waiting for your editor…” ধরনের বার্তা দেখাতে পারে):

$ git add app.js
$ git commit
[main be72ad5] Print each item on its own line
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git cat-file -p HEAD
tree 9c161ecf00137cb88feb9387296a968878d635a7
parent 8bfb77a54601e9294b83f3f19c4519d36a524183
author Rahim <rahim@example.com> 1768449720 +0600
committer Rahim <rahim@example.com> 1768449720 +0600

Print each item on its own line

The list was printed as one array, which is hard to read
on a phone screen. Print one item per line instead.
$ git log -1 --format=fuller
commit be72ad5af3400d3a74cea4f824820bde9ad28056
Author:     Rahim <rahim@example.com>
AuthorDate: Thu Jan 15 10:02:00 2026 +0600
Commit:     Rahim <rahim@example.com>
CommitDate: Thu Jan 15 10:02:00 2026 +0600

    Print each item on its own line

    The list was printed as one array, which is hard to read
    on a phone screen. Print one item per line instead.

এবার parent 8bfb77a… লাইন এসেছে — দ্বিতীয় commit প্রথমটির দিকে নির্দেশ করছে। --format=fuller author আর committer আলাদা করে দেখায়। -m দুবার দিলেও (git commit -m "সারাংশ" -m "বিস্তারিত") Git দুটি অনুচ্ছেদ বানায়, কিন্তু লম্বা বার্তার জন্য editor-ই সুবিধাজনক। ভালো বার্তার প্রচলিত নিয়ম (৫০ অক্ষরের মধ্যে সারাংশ, আদেশসূচক ক্রিয়া, Conventional Commits) পর্ব ১৮-এ।

নিচের stepper-এ দুটি commit পাশাপাশি দেখুন — কোনটিতে কী আছে:

bazar repository — commit-এর বিষয়বস্তু
Commit ID
Parent
Tree (snapshot)
app.js-এর দ্বিতীয় লাইন
Git-এর সারাংশ
  1. Commit-এর আগে। তিনটি file index-এ তোলা; repository-তে কোনো commit নেই, git status বলছে No commits yet. HEAD এমন একটি branch (main)-এর দিকে নির্দেশ করছে, যার এখনো কোনো commit নেই।
  2. প্রথম commit 8bfb77a. Index-এর তিনটি file নিয়ে tree 9bb98ec তৈরি হলো। Parent নেই — এটিই root commit. মোট ৭টি লাইন যোগ: README.md-এর ২টি, index.html-এর ৩টি, app.js-এর ২টি।
  3. দ্বিতীয় commit be72ad5. শুধু app.js-এর দ্বিতীয় লাইন বদলেছে, তবু tree 9c161ec-তে তিনটি file-ই আছে — commit সবসময় পুরো ছবি রাখে। বদলহীন README.md আর index.html-এর বিষয়বস্তু একই hash পায়, তাই Git সেগুলো দ্বিতীয়বার জমা রাখে না, শুধু আগেরটির দিকে নির্দেশ করে। Parent 8bfb77a.
  4. History একটি শিকল। Branch main এখন be72ad5-এর দিকে, HEAD main-এর দিকে। be72ad5 থেকে parent ধরে পেছনে গেলে 8bfb77a, তারপর আর কিছু নেই। git log --oneline এই শিকলই উল্টো ক্রমে (নতুন আগে) দুই লাইনে দেখায়।

Commit ID কেন এত “স্পর্শকাতর”

Commit-এর লেখায় parent-এর ID থাকে, আর সেই লেখার hash-ই commit-এর ID. তাহলে পুরোনো একটি commit-এর একটি byte বদলালে কী হয়? নিচের মডেলে commit বানানো হয়েছে {tree, parent, author, committer, message} দিয়ে; ID হিসেবে আমরা ছোট একটি 32-bit hash, FNV-1a, ব্যবহার করেছি — এটি শুধু Git-এর SHA-1 বা SHA-256-এর জায়গায় দাঁড়ানো একটি সহজ বিকল্প, নিরাপত্তার জন্য নয়। আসল Git hash করে "commit <size>\0" আর তারপর commit-এর লেখা।

// Commit objects: {tree snapshot, parent, author, message} -> an ID from the text.
// FNV-1a 32-bit stands in for Git's SHA-1 (or SHA-256) over "commit <size>\0" + text.
function fnv1a(s) {
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
  return h.toString(16).padStart(8, "0");
}
function treeId(files) {                  // canonical text: sorted "name content" lines
  return fnv1a(Object.keys(files).sort().map(f => f + " " + files[f]).join("\n"));
}
function makeCommit(files, parent, msg, time) {
  const text = "tree " + treeId(files) + "\n" + (parent ? "parent " + parent + "\n" : "") +
    "author Rahim <rahim@example.com> " + time + " +0600\n" +
    "committer Rahim <rahim@example.com> " + time + " +0600\n\n" + msg + "\n";
  return { id: fnv1a(text), text };
}
function history(firstAppJs) {
  const s1 = { "app.js": firstAppJs, "README.md": "# Bazar" };
  const s2 = { ...s1, "app.js": 'items = ["chal","dal","tel"]' };
  const s3 = { ...s2, "prices.js": "chal: 70" };
  const c1 = makeCommit(s1, null, "Add product list", 1768449660);
  const c2 = makeCommit(s2, c1.id, "Add oil to list", 1768449720);
  const c3 = makeCommit(s3, c2.id, "Add price list", 1768449780);
  return [c1, c2, c3];
}
const A = history('items = ["chal","dal"]');
console.log("--- first commit object (no parent line) ---\n" + A[0].text);
console.log("--- second commit object ---\n" + A[1].text);
const B = history('items = ["chal","dal"] ');       // one extra space in commit 1
for (let i = 0; i < 3; i++)
  console.log(`commit ${i + 1}: original ${A[i].id}   one-byte change ${B[i].id}`);

আউটপুট:

--- first commit object (no parent line) ---
tree dc80d4b1
author Rahim <rahim@example.com> 1768449660 +0600
committer Rahim <rahim@example.com> 1768449660 +0600

Add product list

--- second commit object ---
tree 38ba9274
parent 2934e7c2
author Rahim <rahim@example.com> 1768449720 +0600
committer Rahim <rahim@example.com> 1768449720 +0600

Add oil to list

commit 1: original 2934e7c2   one-byte change bc213395
commit 2: original f04a6621   one-byte change abfc9356
commit 3: original 140993bc   one-byte change b544020f

প্রথম commit-এর লেখায় parent লাইন নেই, দ্বিতীয়টিতে parent 2934e7c2 — প্রথমটির ID. এবার শেষ তিন লাইন দেখুন: প্রথম commit-এর app.js-এ শুধু একটি space যোগ করায় তার tree বদলেছে, তাই ID 2934e7c2 থেকে bc213395. দ্বিতীয় আর তৃতীয় commit-এর tree কিন্তু একই রয়ে গেছে (তাদের app.js তো আলাদা করে লেখা), তবু তাদের ID-ও বদলেছে — কারণ তাদের লেখার parent লাইন বদলেছে। একটি পুরোনো commit বদলানো মানে তার পরের প্রতিটি commit নতুন। এজন্যই শেষ commit-এর ID-ই পুরো ইতিহাসের একটি আঙুলের ছাপ; আর এজন্যই অন্যের সাথে ভাগ করা history বদলানো (rebase, amend — পর্ব ১১ ও ১৫) সাবধানে করতে হয়।

আরও গভীরে: Git-এর object store একটি content-addressed storage — প্রতিটি file-এর বিষয়বস্তু (blob), ফোল্ডারের তালিকা (tree) আর commit তার নিজের hash নামে .git/objects-এ থাকে; পর্ব ৫২-এ এগুলো হাতে বানাব। Commit-এর parent-শিকল একটি Merkle-ধাঁচের কাঠামো, যা Ralph Merkle-এর ১৯৭০-এর দশকের hash tree-র ধারণার আত্মীয়। ২০১৭ সালে Google ও CWI Amsterdam-এর গবেষকেরা SHA-1-এর প্রথম ব্যবহারিক collision (“SHAttered”) প্রকাশ করেন; এরপর Git ২.১৩ থেকে collision-আক্রমণ ধরার ব্যবস্থাসহ একটি “hardened” SHA-1 ব্যবহার করে, আর SHA-256-এ যাওয়ার কাজ চলছে। Pro Git বইয়ের (Scott Chacon ও Ben Straub, ২য় সংস্করণ ২০১৪) “Git Basics” আর “Git Internals” অধ্যায় এই পর্বের ভালো সঙ্গী।

৬ · git commit -a — সুবিধা আর তার ফাঁদ

git commit -a (--all) মানে commit-এর আগে সব tracked file-এর বদল আর মুছে ফেলা নিজে থেকে stage করে নেওয়া। ছোট কাজে এটি সময় বাঁচায় — আলাদা git add লাগে না। কিন্তু ফাঁদটি নামেই লুকানো: “all” মানে সব tracked file, নতুন (untracked) file নয়। নিচে app.js-এ লবণ ("lobon") যোগ করা হয়েছে, আর একটি নতুন file discount.js লেখা হয়েছে; ডেভেলপার ভাবছেন দুটোই commit হবে:

$ git status -s
 M app.js
?? discount.js
$ git commit -a -m "Add discount and salt"
[main 26d4771] Add discount and salt
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git status -s
?? discount.js
$ git show --stat --oneline HEAD
26d4771 Add discount and salt
 app.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

বার্তায় লেখা “Add discount”, কিন্তু commit-এ আছে শুধু app.js (1 file changed). discount.js এখনো ??. দলের অন্য কেউ এই commit নিলে app.js হয়তো discount.js-কে খুঁজবে আর পাবে না — “আমার কম্পিউটারে তো চলছিল!” সমস্যার একটি ক্লাসিক উৎস। এরপর আমরা অংশ ৪-এর git add -A দিয়ে file-টি পরের commit-এ তুলেছিলাম।

নিচের মডেলে একটি seed-যুক্ত PRNG (mulberry32) সাতটি file-এর একটি এলোমেলো-দেখতে কিন্তু প্রতিবার একই অবস্থা বানায়, তারপর দেখায় commit -a আর “add -A, তারপর commit” — কোনটি কোন file নেয়:

// The "git commit -a" trap: which files really go into the commit?
// A seeded PRNG (mulberry32) builds a random-looking but repeatable folder.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(8);
const kinds = ["untracked (new)", "tracked, modified", "tracked, unchanged", "tracked, deleted"];
const names = ["cart.js", "login.js", "notes.md", "offer.js", "pay.js", "logo.svg", "faq.md"];
const files = names.map(name => ({ name, kind: kinds[Math.floor(rnd() * 4)] }));

// commit -a = "stage every change to files Git ALREADY tracks", then commit.
const inCommitA = f => f.kind === "tracked, modified" || f.kind === "tracked, deleted";
// add -A (or add .) first = also stage new files.
const inAddAll = f => f.kind !== "tracked, unchanged";

console.log("file        state                commit -a   add -A + commit");
for (const f of files)
  console.log(f.name.padEnd(12) + f.kind.padEnd(21) +
    (inCommitA(f) ? "yes" : "no").padEnd(12) + (inAddAll(f) ? "yes" : "no"));
const lost = files.filter(f => inAddAll(f) && !inCommitA(f)).map(f => f.name);
console.log("\nleft behind by commit -a: " + (lost.join(", ") || "none"));
console.log("still shown as ?? after commit -a: " + lost.length + " file(s)");

আউটপুট:

file        state                commit -a   add -A + commit
cart.js     untracked (new)      no          yes
login.js    tracked, unchanged   no          no
notes.md    tracked, modified    yes         yes
offer.js    tracked, unchanged   no          no
pay.js      tracked, unchanged   no          no
logo.svg    tracked, modified    yes         yes
faq.md      tracked, deleted     yes         yes

left behind by commit -a: cart.js
still shown as ?? after commit -a: 1 file(s)

বদলানো দুটি tracked file (notes.md, logo.svg) আর মুছে ফেলা faq.md — তিনটিই commit -a নেয়। বদলহীন file কোনো পথেই commit-এর “বদল” নয় (তবে snapshot-এ আগের মতোই থাকে)। পার্থক্য একটি জায়গায়: নতুন cart.js. commit -a একে ফেলে যায়, আর commit-এর পরে সেটি ?? হয়েই থাকে। mulberry32(8)-এর seed বদলে চালিয়ে দেখুন — যতগুলো untracked file আসবে, ততগুলোই বাদ পড়বে। নিয়মটি মনে রাখুন: commit-এর আগে একবার git status.

উল্টো ঝুঁকিও আছে। commit -a সব tracked file-এর সব বদল নেয় — অংশ ৪-এর debug লাইনের মতো যা আপনি commit করতে চাননি, সেটিও। add -p দিয়ে বেছে তোলার পরে ভুলে commit -a দিলে সেই বাছাই নষ্ট হয়ে যায়।

৭ · মুছে ফেলা, নাম বদল আর খালি ফোল্ডার

git rm — ফোল্ডার আর index দুই জায়গা থেকে

একটি tracked file সাধারণ rm বা File Explorer দিয়ে মুছলে Git সেটিকে “deleted, not staged” (" D") দেখায়; তারপর git add বা git rm দিয়ে মোছাটা stage করতে হয়। git rm দুটি কাজ একসাথে করে — ফোল্ডার থেকে file মোছে আর মোছাটা stage করে:

$ git rm discount.js
rm 'discount.js'
$ git status -s
D  discount.js
$ ls
README.md
app.js
css
index.html
prices.js
$ git commit -m "Remove discount until prices are final"
[main 9610b7c] Remove discount until prices are final
 1 file changed, 3 deletions(-)
 delete mode 100644 discount.js

File-টি শেষ commit থেকে বাদ গেছে, কিন্তু ইতিহাস থেকে নয় — আগের commit 5903454-এ discount.js এখনো আছে, দরকার হলে ফেরানো যাবে (পর্ব ১১)।

git rm --cached — Git থেকে বাদ, ফোল্ডারে থাকুক

ভুল করে একটি log file commit করে ফেলেছেন। File-টি নিজের কম্পিউটারে রাখতে চান, শুধু Git যেন আর track না করে। তখন --cached: শুধু index থেকে সরায়, ফোল্ডারে হাত দেয় না।

$ git add debug.log
$ git commit -m "Add debug log by mistake"
…
$ git rm --cached debug.log
rm 'debug.log'
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    debug.log

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	debug.log
$ ls
README.md
app.js
css
debug.log
home.html
images
price-list.js
$ git commit -m "Stop tracking debug.log"
[main 8732561] Stop tracking debug.log
 1 file changed, 1 deletion(-)
 delete mode 100644 debug.log
$ git status -s
?? debug.log

একই file দুই তালিকায় — index থেকে “deleted” আর ফোল্ডারে “untracked”. ls দেখাচ্ছে file ফোল্ডারেই আছে। কিন্তু ?? হয়ে থাকলে কেউ আবার git add . দিয়ে তুলে ফেলতে পারেন; তাই সাথে সাথে debug.log বা *.log .gitignore-এ লেখা উচিত — পর্ব ১০।

Password বা API key ভুলে commit হলে git rm --cached যথেষ্ট নয়। এটি শুধু পরের commit থেকে file বাদ দেয়; আগের commit-এ গোপন তথ্যটি থেকেই যায়, আর repository কোথাও push হয়ে থাকলে সবাই দেখতে পারেন। প্রথম কাজ হলো সেই key বাতিল করে নতুন key নেওয়া; history পরিষ্কারের উপায় পর্ব ৫৭-এ।

git mv আর rename detection

$ git mv index.html home.html
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    index.html -> home.html
$ git commit -m "Rename index.html to home.html"
[main 14ebbe2] Rename index.html to home.html
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename index.html => home.html (100%)

মজার তথ্য: Git কোথাও “নাম বদল” লিখে রাখে না। Commit-এ থাকে শুধু snapshot — আগের snapshot-এ index.html, নতুনটিতে home.html, বিষয়বস্তু একই। git status, diff বা log দুটি snapshot তুলনা করে বলে, “একটি file গেছে, প্রায় একই রকম একটি এসেছে — নিশ্চয়ই নাম বদল।” একে বলে rename detection; ডিফল্টে বিষয়বস্তু অন্তত ৫০% মিললে Git নাম বদল ধরে ((100%) মানে হুবহু একই)। তাই git mv আসলে শুধু একটি সুবিধা — mv আর git add-এর যোগফল। সাধারণ mv করে পরে add করলেও ফল একই:

$ mv prices.js price-list.js
$ git status -s
 D prices.js
?? price-list.js
$ git add -A
$ git status -s
R  prices.js -> price-list.js
$ git commit -m "Rename prices.js to price-list.js"
[main ad94308] Rename prices.js to price-list.js
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename prices.js => price-list.js (100%)

add-এর আগে Git দেখাচ্ছে একটি file মুছে গেছে (" D") আর একটি নতুন এসেছে (??)। দুটিই stage হতেই সে মিলিয়ে R (renamed) বলল। একটি পরামর্শ: নাম বদল আর বিষয়বস্তুর বড় পরিবর্তন একই commit-এ না করাই ভালো — মিল ৫০%-এর নিচে নামলে Git একে “একটি মুছে আরেকটি নতুন” দেখাবে, আর পরে file-এর ইতিহাস খোঁজা কঠিন হবে।

খালি ফোল্ডার Git দেখে না — .gitkeep

Git শুধু file track করে; ফোল্ডার আসে file-এর path থেকে। তাই খালি ফোল্ডারের কোনো অস্তিত্বই Git-এর কাছে নেই:

$ mkdir images
$ git status
On branch main
nothing to commit, working tree clean
$ touch images/.gitkeep
$ git status -s
?? images/
$ git add images
$ git commit -m "Add empty images folder"
[main 809cc53] Add empty images folder
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 images/.gitkeep

mkdir images-এর পরেও working tree clean. ভেতরে একটি ফাঁকা file রাখতেই ফোল্ডারটি দেখা দিল। এই file-এর প্রচলিত নাম .gitkeep — কিন্তু এটি শুধু একটি প্রথা, Git-এর কোনো বিশেষ নিয়ম নয়; যেকোনো নামেই কাজ হয়। অনেক দল ফাঁকা file-এর বদলে ছোট একটি README বা ফোল্ডারের ভেতরের সব কিছু উপেক্ষা করার একটি .gitignore রাখেন।

৮ · ছোট, এক-উদ্দেশ্যের commit — আর git log-এ এক নজর

এই পর্বের পুরো কাজের ইতিহাস এখন এরকম:

$ git log --oneline
8732561 Stop tracking debug.log
99f2ccd Add debug log by mistake
809cc53 Add empty images folder
ad94308 Rename prices.js to price-list.js
14ebbe2 Rename index.html to home.html
9610b7c Remove discount until prices are final
5903454 Add stylesheet, discount and run note
26d4771 Add discount and salt
37c2b34 Raise rice price to 75 taka
bb4928a Add price list and total
be72ad5 Print each item on its own line
8bfb77a Add product list page

git log --oneline প্রতিটি commit-কে এক লাইনে দেখায়: ছোট ID আর বার্তার প্রথম লাইন, নতুন commit সবার ওপরে। ১২টি commit, প্রতিটির একটি কাজ — দাম বাড়ানো, নাম বদল, একটি ভুল শোধরানো। log-এর অসংখ্য বিকল্প (--graph, -p, --stat, তারিখ দিয়ে খোঁজা) পর্ব ০৯-এ (status, diff, log আর show).

একটি খারাপ ইতিহাস কল্পনা করুন: সপ্তাহের শেষে একটিমাত্র commit, বার্তা update, তাতে ৪০টি file আর দাম বদল, নতুন page, debug লাইন আর একটি bug fix — সব একসাথে। এখন দুটি বিপদ:

  • ফেরানো: দাম বদলটি ভুল ছিল। ফেরাতে গেলে পুরো সপ্তাহের কাজ ফেরাতে হবে, নয়তো হাতে বেছে বেছে খুঁজতে হবে। ছোট commit হলে শুধু 37c2b34-কে revert করলেই হতো (পর্ব ১১)।
  • খোঁজা: কোন পরিবর্তনে bug এসেছে, Git-এর bisect commit ধরে ধরে binary search করে বের করতে পারে (পর্ব ৫৫)। কিন্তু উত্তর যদি হয় “৪০ file-এর ওই বিশাল commit”, তাতে প্রায় কিছুই জানা হলো না।
  • Review: ২০০ লাইনের এক-উদ্দেশ্যের পরিবর্তন একজন reviewer মন দিয়ে পড়তে পারেন; ২০০০ লাইনের মিশ্র পরিবর্তন সাধারণত চোখ বুলিয়ে “ঠিক আছে” পায় (code review, পর্ব ৪৪)।
মূল ধারণা

একটি atomic commit একটি যুক্তিসংগত পরিবর্তন বহন করে — পুরোটা নিলে project কাজ করে, অর্ধেক নিলে নয়। মাপকাঠি লাইনের সংখ্যা নয়, উদ্দেশ্যের সংখ্যা: বার্তায় “আর” (and) লিখতে হলে সম্ভবত দুটি commit হওয়া উচিত ছিল। আমাদের 26d4771 Add discount and salt ঠিক এই ভুলের উদাহরণ — নামেই দুটি কাজ, আর একটি আবার বাদও পড়েছে।

তবে উল্টো দিকেও বাড়াবাড়ি আছে। প্রতিটি টাইপো-র জন্য আলাদা commit, “wip”, “wip2”, “fix again” — এমন ইতিহাসও পড়া কঠিন। এর সমাধান হলো নিজের branch-এ ইচ্ছেমতো ছোট commit করা, আর অন্যদের সাথে ভাগ করার আগে interactive rebase (পর্ব ১৫) দিয়ে গুছিয়ে নেওয়া, বা pull request-এ squash করা (পর্ব ১৬)। Linux kernel-এর patch জমা দেওয়ার নির্দেশিকাতেও “প্রতিটি যুক্তিসংগত পরিবর্তন আলাদা patch-এ” রাখার কথা বলা আছে — বড় open-source project-এ এটি পুরোনো অভ্যাস।

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

  • Git-এর তিনটি এলাকা কী কী? — Working tree (ফোল্ডারের file), staging area বা index (পরের commit-এর প্রস্তুত তালিকা), repository (.git-এ জমা commit).
  • git add-এর পরে file বদলিয়ে commit করলে কোন version যায়? — add-এর সময়ের version, কারণ commit index থেকে তৈরি হয়; নতুন বদলের জন্য আবার add লাগে।
  • git commit -a কি নতুন file নেয়? — না; শুধু tracked file-এর বদল আর মুছে ফেলা নেয়।
  • git rm আর git rm --cached-এর পার্থক্য? — প্রথমটি ফোল্ডার আর index দুই জায়গা থেকে মোছে; দ্বিতীয়টি শুধু index থেকে, file ফোল্ডারে untracked হয়ে থাকে।
  • একটি commit-এ কী কী থাকে? — Tree (পুরো snapshot), parent (প্রথম commit-এ নেই), author, committer, সময় আর বার্তা; সব মিলিয়ে hash-ই commit ID.

৯ · AI-এর যুগে — Git command-এ AI-কে যাচাই

“আমার file commit হচ্ছে না” — এমন প্রশ্নে AI সাধারণত কয়েক সেকেন্ডে কয়েকটি command দিয়ে দেয়। বেশিরভাগ সময় কাজও হয়। কিন্তু এই পর্বের বিষয়ে AI-এর কয়েকটি পরিচিত ভুল আছে, আর তার কয়েকটি আপনার কাজ মুছে দিতে পারে:

AI যা বলেসমস্যাযাচাই
“Unstage করতে git reset --hard দিন”Index-এর সাথে ফোল্ডারের না-তোলা বদলও মুছে যায়, ফেরত পাওয়া কঠিনUnstage-এর জন্য git restore --staged; পর্ব ১১
“File আর track করবেন না? git rm দিন”--cached ছাড়া file ফোল্ডার থেকেও মুছে যায়git help rm, আগে একটি throwaway repo-তে চালান
“git commit -a সব file নেবে”নতুন file বাদ পড়েCommit-এর পরে git status, git show --stat
“git add . দিয়ে সব তুলে দিন”.env, key, log সবই উঠে যেতে পারেআগে git status, তারপর git diff --cached
“Git প্রতিটি commit-এ শুধু diff রাখে”ভুল ধারণা; commit পূর্ণ snapshot (জমা রাখার সময় Git পরে সংকোচন করে)git cat-file -p HEAD-এ tree দেখুন
বানানো flag, যেমন git add --new-onlyএমন flag নেই; Git error দেবে, বা মিলে যাওয়া অন্য কিছু করবেgit help add বা git-scm.com-এর documentation

AI-কে Git-এর প্রশ্ন করলে আপনার repository-র আসল অবস্থা দিন, আর ধ্বংসাত্মক command সম্পর্কে আগে সাবধান করতে বলুন:

আমি Git 2.53 ব্যবহার করি। নিচে আমার `git status` আর `git log --oneline -5`-এর পুরো আউটপুট দিলাম: [এখানে আউটপুট পেস্ট করুন] আমি চাই: শুধু app.js-এর দামের বদলটি পরের commit-এ যাক, debug লাইন নয়, আর নতুন discount.js-ও যাক। ১. ধাপে ধাপে command দাও, প্রতিটির পরে `git status -s` কেমন দেখাবে সেটাও বলো। ২. কোনো command যদি আমার না-তোলা কাজ বা ফাইল মুছে দিতে পারে (reset --hard, restore, rm, clean, push --force), সেটি আলাদা করে সতর্ক করো এবং নিরাপদ বিকল্প দাও। ৩. যে flag ব্যবহার করবে, সেটি `git help <command>`-এ কোন নামে আছে বলো — বানানো flag দিও না।

AI-এর দেওয়া Git command চালানোর আগে — চেকলিস্ট

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

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

প্র ০১অনেক version control-এ staging area নেই — সরাসরি commit হয়। Git কেন এই বাড়তি ধাপ রাখল, আর এটি কি সত্যিই দরকারি?

Subversion-এর মতো centralized ব্যবস্থায় svn commit আপনার কাজের কপির সব বদল (বা নির্দিষ্ট file-এর বদল) একবারে পাঠায়। Mercurial-ও ডিফল্টে সরাসরি commit করে; অংশবিশেষ বাছাই করতে তার আলাদা interactive ব্যবস্থা আছে। Git-এর index তাহলে কোথা থেকে এলো?

Git-এর জন্ম ২০০৫ সালে Linux kernel-এর কাজের জন্য, আর সেই দলের সংস্কৃতি ছিল email-এ patch পাঠানো। একজন maintainer-এর কাছে প্রতিদিন অসংখ্য patch আসে; প্রতিটি patch একটি যুক্তিসংগত পরিবর্তন হওয়ার কথা। তাই “কোন বদল একসাথে যাবে” সেটি সাজানোর একটি জায়গা স্বাভাবিকভাবেই দরকার ছিল। শুরুতে index ছিল মূলত ভেতরের একটি কাঠামো — কোন file-এর কোন বিষয়বস্তু, কোন সময়ে দেখা হয়েছে, তার তালিকা, যাতে বড় project-এ “কী বদলেছে” দ্রুত বের করা যায়। পরে add -p-এর মতো সুবিধা এসে এটিকে একটি ব্যবহারকারীর হাতিয়ারও বানায়।

লাভটা বাস্তব: একটি file-এ bug fix আর নতুন feature মিশে গেলে দুটিকে আলাদা commit-এ রাখা যায়; commit-এর আগে git diff --cached দিয়ে ঠিক কী যাচ্ছে তা পড়া যায়; merge conflict-এর সময় (পর্ব ১৩) index-ই মনে রাখে কোন file মীমাংসা হয়েছে। দাম হলো ধারণার জটিলতা — একই file-এর তিনটি কপি নতুনদের প্রায়ই বিভ্রান্ত করে, আর অনেক সমালোচক বলেন Git-এর UI এই জটিলতা খোলামেলা রেখে দিয়েছে। এজন্যই commit -a আছে, আর অনেক GUI index-কে প্রায় লুকিয়ে রাখে। নতুন কিছু version control (যেমন Jujutsu) ইচ্ছে করেই আলাদা staging area ছাড়া নকশা করা হয়েছে — সেখানে কাজের কপিই সবসময় একটি চলমান commit.

মূল উপলব্ধি: Staging area একটি নকশা-সিদ্ধান্ত, প্রকৃতির নিয়ম নয়। এটি “কী commit হবে” আর “ফোল্ডারে কী আছে” আলাদা করে আপনাকে পরিষ্কার ইতিহাস বানানোর ক্ষমতা দেয়, বিনিময়ে একটু বেশি ধারণা শিখতে হয়।

প্র ০২Commit যদি প্রতিবার পুরো snapshot রাখে, তাহলে ১০,০০০ commit-এর repository কি বিশাল হয়ে যাবে না? পুরোনো ব্যবস্থাগুলো কেন diff রাখত?

১৯৮০-র দশকের RCS আর তার ওপর গড়া CVS প্রতিটি file-এর ইতিহাস রাখত delta হিসেবে — একটি পূর্ণ version আর বাকিগুলো “আগের থেকে কী বদলেছে”। তখন disk ছিল দামি, তাই ধারণাটি যুক্তিসংগত। সমস্যা হলো, একটি পুরোনো version পেতে অনেকগুলো delta পরপর প্রয়োগ করতে হয়, আর “পুরো project কোনো এক মুহূর্তে কেমন ছিল” প্রশ্নের উত্তর file ধরে ধরে জোড়া দিতে হয়।

Git-এর চিন্তা উল্টো: ধারণায় প্রতিটি commit পুরো snapshot, কিন্তু জমা রাখায় চালাকি। প্রথম চালাকি content addressing — একটি file-এর বিষয়বস্তুর hash-ই তার নাম। দুটি commit-এ README.md না বদলালে দুটির tree একই hash-এর দিকে নির্দেশ করে, বিষয়বস্তু একবারই জমা থাকে। এই পর্বের দ্বিতীয় commit-এ তিনটি file-এর snapshot, কিন্তু নতুন জমা হয়েছে শুধু app.js-এর নতুন বিষয়বস্তু, একটি নতুন tree আর commit-টি। দ্বিতীয় চালাকি packfile: git gc বা push-এর সময় Git অনেক object একসাথে একটি file-এ ভরে, আর মিল থাকা object-গুলোকে একে অপরের delta হিসেবে সংকুচিত করে। অর্থাৎ delta আছে, কিন্তু সেটি জমা রাখার কৌশল, ইতিহাসের মডেল নয়।

লাভ: যেকোনো commit checkout করা দ্রুত, দুটি যেকোনো commit-এর তুলনা সহজ, branch বানানো প্রায় খরচহীন (শুধু একটি pointer, পর্ব ১২)। দাম: বিশাল binary file (ভিডিও, ডিজাইন file) প্রতিবার বদলালে প্রতিটি version আলাদা জমা হয়, কারণ সেগুলো ভালোভাবে সংকুচিত হয় না — এজন্যই Git LFS-এর মতো সমাধান (পর্ব ১৯)।

মূল উপলব্ধি: “ব্যবহারকারী কী দেখেন” (snapshot) আর “ভেতরে কীভাবে জমা থাকে” (hash দিয়ে ভাগাভাগি আর সংকোচন) — দুটি আলাদা স্তর। ভালো নকশায় সহজ মডেল উপরে থাকে, কৌশল নিচে লুকানো।

প্র ০৩একটি byte বদলালে পরের সব commit-এর ID বদলে যায় — এটি কি একটি অসুবিধা, নাকি জেনেশুনে রাখা একটি সুবিধা?

দুটোই, তবে মূলত সুবিধা। প্রতিটি commit-এর লেখায় parent-এর hash থাকায় শেষ commit-এর ID আসলে পুরো ইতিহাসের একটি সারাংশ। দুজনের কাছে একই branch-এর শেষ ID একই হলে নিশ্চিত হওয়া যায় (hash function নিরাপদ ধরে নিয়ে) যে তাঁদের পুরো ইতিহাস হুবহু এক — প্রতিটি file, প্রতিটি বার্তা, প্রতিটি সময়। disk-এর ত্রুটি বা network-এ কোনো byte নষ্ট হলে hash মিলবে না, Git সাথে সাথে ধরবে (git fsck). আর কেউ চুপিচুপি পুরোনো একটি commit বদলে দিলে পরের সব ID বদলে যাবে, তাই অন্যদের কপির সাথে মিলিয়ে সেটি ধরা পড়বে। ব্লকচেইনের hash-শিকলের ধারণাও এর আত্মীয়।

অসুবিধা দেখা যায় history নতুন করে লেখার সময়। commit --amend বা rebase (পর্ব ১১, ১৫) আসলে পুরোনো commit বদলায় না; নতুন ID-র নতুন commit বানায়। আপনার সহকর্মী পুরোনো ID-গুলোর ওপর কাজ করে থাকলে দুই ইতিহাস আলাদা হয়ে যায় — এজন্যই প্রচলিত নিয়ম, “অন্যের সাথে ভাগ করা history নতুন করে লিখবেন না”।

নিশ্চয়তাটি hash function-এর ওপর নির্ভর করে। ২০১৭ সালে প্রকাশিত “SHAttered” গবেষণায় দুটি ভিন্ন PDF দেখানো হয় যাদের SHA-1 একই — বিপুল গণনার খরচে। Git এরপর collision-চেষ্টা চেনার ব্যবস্থাসহ SHA-1 ব্যবহার শুরু করে, আর SHA-256 repository-র সমর্থন যোগ করে; পুরো ecosystem-এর রূপান্তর এখনো চলমান। মনে রাখবেন, commit-এর author নাম আর email যে কেউ যা খুশি লিখতে পারেন — hash অখণ্ডতা দেয়, পরিচয়ের প্রমাণ নয়। পরিচয়ের জন্য signed commit লাগে (পর্ব ৫৭)।

মূল উপলব্ধি: Content-addressed ইতিহাস “কী হয়েছিল” প্রশ্নে অখণ্ডতা দেয়, কিন্তু “কে করেছিল” প্রশ্নে নয়। আর ইতিহাস বদলানো কখনো “সম্পাদনা” নয় — সবসময় নতুন ইতিহাস তৈরি।

প্র ০৪Linus Torvalds-এর Git-এর প্রথম commit-এরও কোনো parent ছিল না। প্রথম commit কেন বিশেষ, আর একটি repository-তে কি একাধিক root commit থাকতে পারে?

Git-এর নিজের source code-এর repository-র সবচেয়ে পুরোনো commit সাধারণভাবে এপ্রিল ২০০৫-এর বলে উল্লেখ করা হয়, বার্তা মোটামুটি “Initial revision of "git", the information manager from hell”. এটিও একটি সাধারণ commit — tree, author, committer, বার্তা — শুধু parent লাইন নেই। এই পর্বের 8bfb77a-এর মতোই।

Root commit বিশেষ কয়েকটি কারণে। এক, history-র শিকল এখানেই শেষ; git log এখানে এসে থামে। দুই, “আগের commit-এর সাথে তুলনা” এখানে খাটে না — তাই প্রথম commit-এর git show সব file-কে নতুন দেখায়, আর অনেক command-এ “HEAD~1” (আগেরটি) খুঁজলে error আসে। তিন, প্রথম commit-এর আগে HEAD এমন একটি branch-এর দিকে নির্দেশ করে যার কোনো commit নেই (Git একে “unborn branch” বলে) — এজন্যই git status “No commits yet” বলে, আর unstage-এর পরামর্শও আলাদা।

একাধিক root commit সম্ভব, আর বাস্তবে আছে। দুটি আলাদা project-এর ইতিহাস একসাথে মেলালে (merge-এ --allow-unrelated-histories, বা subtree, পর্ব ১৯) মিলিত repository-তে দুটি root থাকে; Git-এর নিজের repository-তেও এমন কয়েকটি root আছে বলে জানা যায়, কারণ কিছু আলাদা tool পরে মেলানো হয়েছিল। git switch --orphan দিয়েও parent-হীন নতুন branch শুরু করা যায় — যেমন একটি website-এর তৈরি file আলাদা branch-এ রাখতে।

মূল উপলব্ধি: Git-এর ইতিহাস একটি শিকল নয়, একটি graph — যেখানে commit-এর parent শূন্য (root), এক (সাধারণ) বা একাধিক (merge) হতে পারে। প্রথম commit শুধু সেই graph-এর একটি প্রান্ত।

প্র ০৫একজন নতুন developer ভুলে .env file (যাতে database password) commit করে push করেছেন। তিনি git rm --cached .env দিয়ে নতুন commit করলেন। সমস্যা কি মিটল?

না। git rm --cached শুধু পরের snapshot থেকে file বাদ দেয়। আগের commit-এর tree-তে .env এখনো আছে, আর সেই commit-এর hash তার পরের সব commit-এর ভিত্তি — অর্থাৎ ইতিহাস না বদলে সেটি সরানো যায় না। যিনি repository clone করবেন, তিনি পুরোনো commit checkout করে password পড়তে পারবেন। Public repository হলে ঝুঁকি আরও বড়: স্বয়ংক্রিয় bot-গুলো public repository-তে ফাঁস হওয়া key খোঁজে বলে বিভিন্ন নিরাপত্তা রিপোর্টে বারবার বলা হয়েছে, তাই push আর অপব্যবহারের মাঝে সময় খুব কম হতে পারে।

সঠিক ক্রম, নিরাপত্তার দিক থেকে: প্রথমে secret বাতিল করে নতুন secret নেওয়া (rotate) — password বদলানো, API key revoke করা। এটিই আসল প্রতিরক্ষা, কারণ ফাঁস হওয়া তথ্য ফেরানো যায় না, শুধু অকেজো করা যায়। তারপর .env-কে .gitignore-এ যোগ (পর্ব ১০), আর একটি .env.example রাখা যাতে অন্যরা জানেন কোন setting লাগবে। এরপর দরকার হলে history থেকে file সরানো — git filter-repo-এর মতো tool দিয়ে; এতে পরের সব commit-এর ID বদলে যায় (প্র ০৩), তাই দলের সবাইকে নতুন করে clone করতে হয়, আর hosting platform-এর cache বা fork-এ পুরোনো কপি থেকে যেতে পারে। পর্ব ৫৭-এ বিস্তারিত।

প্রতিরোধ সবচেয়ে সস্তা: project শুরুর দিনেই .gitignore, commit-এর আগে git status আর git diff --cached পড়া, চোখ বন্ধ করে git add . না দেওয়া, আর pre-commit hook বা platform-এর secret scanning চালু রাখা (লেখার সময়ে বড় hosting platform-গুলো এমন সুবিধা দেয়, platform-ভেদে আলাদা)।

মূল উপলব্ধি: Git ভুলতে পারে না — সেটাই তার শক্তি, আর গোপন তথ্যের ক্ষেত্রে সেটাই বিপদ। একবার commit হওয়া secret-কে “ফাঁস হয়ে গেছে” ধরে নিয়ে আগে secret বদলান, তারপর history.

অনুশীলন

নিজের কম্পিউটারে একটি খালি scratch ফোল্ডারে চালিয়ে মিলিয়ে নিন — আসল project-এ নয়।

  1. Command-এর ক্রম লিখুন: একটি নতুন ফোল্ডার result-portal (একটি বিশ্ববিদ্যালয়ের ফলাফল পোর্টাল — কাল্পনিক)। ভেতরে আছে README.md, grade.js আর notes.txt (আপনার ব্যক্তিগত নোট)। লক্ষ্য: (ক) branch main দিয়ে repository শুরু, (খ) প্রথম commit-এ শুধু README.md আর grade.js, (গ) তারপর grade.js-এ দুটি বদল — একটি bug fix আর একটি নতুন feature — দুটি আলাদা commit-এ। notes.txt কখনো commit হবে না।
    git init -b main result-portal
    cd result-portal
    # (file তিনটি লেখা হলো)
    git status -s                 # ?? README.md  ?? grade.js  ?? notes.txt
    git add README.md grade.js    # নাম ধরে — add . নয়
    git status -s                 # A  README.md  A  grade.js  ?? notes.txt
    git commit -m "Add grade calculator"
    # grade.js-এ দুটি বদল করা হলো
    git add -p grade.js           # bug fix-এর hunk: y, feature-এর hunk: n
    git diff --cached             # শুধু fix যাচ্ছে কি না দেখা
    git commit -m "Fix rounding of GPA"
    git add grade.js              # বাকি feature-এর বদল
    git commit -m "Add letter grade"
    git log --oneline             # তিনটি commit

    মূল বিষয়: git add . বা commit -a এড়ানো — প্রথমটি notes.txt তুলে দিত। স্থায়ী সমাধান হলো notes.txt-কে .gitignore-এ রাখা (পর্ব ১০)। দুটি বদল একই hunk-এ পড়লে (খুব কাছাকাছি লাইন) s দিয়ে ভাঙার চেষ্টা করুন, না ভাঙলে e দিয়ে হাতে সম্পাদনা।

  2. আউটপুট অনুমান করুন: একটি repository-তে a.txt আর b.txt commit করা আছে, ফোল্ডার clean. এবার পরপর চালানো হলো:
    echo two >> a.txt
    git add a.txt
    echo three >> a.txt
    git rm -q b.txt
    echo new > c.txt
    git status -s
    git status -s কী দেখাবে? আর এখন git commit -a দিলে কোন file-এর কোন পরিবর্তন commit-এ যাবে?

    Git ২.৫৩-এ চালিয়ে পাওয়া আসল আউটপুট:

    $ git status -s
    MM a.txt
    D  b.txt
    ?? c.txt
    $ git commit -a --dry-run --short
    M  a.txt
    D  b.txt
    ?? c.txt

    MM a.txt: index-এ “two” যোগ হওয়া version (staged, প্রথম M), ফোল্ডারে তার পরে “three”-ও (দ্বিতীয় M). D b.txt: git rm মোছাটা সরাসরি stage করেছে। ?? c.txt: নতুন, untracked. --dry-run দেখাচ্ছে commit -a করলে a.txt-এর দুটি লাইনই (“two” আর “three”) যাবে, কারণ -a আগে সব tracked file-এর বর্তমান অবস্থা stage করে (MM থেকে M); b.txt-এর মোছা যাবে; c.txt যাবে না। শুধু git commit (-a ছাড়া) দিলে যেত শুধু “two” আর b.txt-এর মোছা।

  3. সমালোচনা করুন: একটি দলের git log --oneline-এর শেষ ছয়টি লাইন: f00a1b2 final, c3d4e5f final2, 9a8b7c6 update, 1e2f3a4 fixed login and added dark mode and changed db password, 5b6c7d8 wip, 0d9e8f7 asdf. কী কী সমস্যা দেখছেন? দলের জন্য তিনটি সহজ নিয়ম প্রস্তাব করুন।

    সমস্যা: (১) final, update, wip, asdf — বার্তা থেকে কিছুই বোঝা যায় না; ছয় মাস পরে কেউ জানবেন না কোন commit কী করেছে। (২) 1e2f3a4-এ তিনটি অসম্পর্কিত কাজ — login fix আর dark mode আলাদাভাবে revert বা review করা যাবে না। (৩) সবচেয়ে গুরুতর: বার্তা বলছে db password বদলানো হয়েছে — password কি কোড বা config file-এ লেখা? তাহলে সেটি ইতিহাসে চিরকাল থাকবে; এখনই password rotate করে .env আর .gitignore-এ সরানো দরকার (পর্ব ৫৭)। (৪) wip আর asdf সম্ভবত অসম্পূর্ণ, project হয়তো সেখানে চলেই না।

    প্রস্তাবিত নিয়ম: ক) প্রতি commit-এ একটি উদ্দেশ্য; বার্তায় “and” লাগলে ভাগ করা। খ) বার্তার প্রথম লাইনে কী বদলেছে, আদেশসূচক ক্রিয়ায় (যেমন “Fix login redirect after timeout”), দরকারে নিচে কেন — বিস্তারিত নিয়ম পর্ব ১৮-এ। গ) কোনো secret কখনো repository-তে নয়; commit-এর আগে git diff --cached পড়া। নিজের branch-এ wip চলতে পারে, কিন্তু ভাগ করার আগে গুছিয়ে নেওয়া (পর্ব ১৫)।

  4. Model বাড়ান: অংশ ২-এর three_areas.js-এ একটি commitAll() function যোগ করুন, যা আসল git commit -a-র মতো আচরণ করবে। তারপর শেষে “edit a.txt; create d.txt; commitAll()” চালিয়ে দেখান d.txt untracked থেকে যায়। কোন শর্তে একটি file “tracked”?
    const commitAll = () => {
      for (const f of Object.keys(index))          // tracked = index-এ আছে
        if (f in work) index[f] = work[f];          // বদল stage
        else delete index[f];                       // মুছে ফেলা stage
      commit();
    };
    work["a.txt"] = "v3"; work["d.txt"] = "v1";
    commitAll(); show("git commit -a");

    প্রত্যাশিত শেষ লাইনগুলো: a.txt W=v3 I=v3 H=v3 unmodified আর d.txt W=v1 I=-- H=-- ?? untracked. মূল কথা হলো loop চলছে index-এর key-এর ওপর, work-এর ওপর নয় — তাই নতুন file নজরেই আসে না। এই মডেলে “tracked” মানে index-এ একটি entry আছে; আসল Git-এও তাই — শেষ commit-এর সব file checkout-এর সময় index-এ থাকে, আর git add নতুন file-কে index-এ ঢুকিয়ে tracked বানায়। (মুছে ফেলা file-এর জন্য মডেলের show শুধু work-এর file দেখায়, তাই সেটি তালিকায় আসবে না।)

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

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

ক্রমএকটি নতুন project-এর প্রথম commit পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. git init -b main — repository তৈরি
  2. Editor-এ file লেখা — file untracked
  3. git add <file> — index-এ তোলা
  4. git status বা git diff --cached — কী যাচ্ছে যাচাই
  5. git commit -m "…" — root commit তৈরি
  6. git log --oneline — ইতিহাসে এক লাইন

একটি Git repository-তে git add app.js চালানোর পরে app.js-এ আরও একটি লাইন যোগ করে save করা হলো, তারপর git commit -m "Update app". Commit-এ app.js-এর কোন version যাবে?

  • Save করা সর্বশেষ version, নতুন লাইনসহ
  • git add চালানোর সময়ের version, নতুন লাইন ছাড়া
  • কোনোটিই নয় — Git commit বাতিল করবে
  • দুই version মিলিয়ে একটি নতুন version

git status -s একটি file-এর পাশে AM দেখাচ্ছে। এর অর্থ কী?

  • File-টি untracked, Git এখনো দেখেনি
  • File-টিতে merge conflict হয়েছে
  • File-টি commit হয়ে গেছে, পরে বদলেছে
  • নতুন file staged হয়েছে, তারপর ফোল্ডারে আবার বদলানো হয়েছে

একটি repository-তে notes.md বদলানো (tracked) আর cart.js নতুন (untracked). git commit -a -m "Add cart" চালালে কী হবে?

  • শুধু notes.md-এর বদল commit হবে; cart.js untracked থেকে যাবে
  • দুটি file-ই commit হবে
  • কিছুই commit হবে না, error আসবে
  • শুধু cart.js commit হবে

git cat-file -p দিয়ে একটি repository-র একটি commit দেখা হলো। এতে tree, author, committer আর বার্তা আছে, কিন্তু parent লাইন নেই। এর সবচেয়ে সম্ভাব্য অর্থ কী?

  • Commit-টি নষ্ট হয়ে গেছে
  • এটি একটি merge commit
  • এটি একটি root commit, যেমন repository-র প্রথম commit
  • Author আর committer একই ব্যক্তি বলে parent লেখা হয়নি

একটি repository-তে খালি ফোল্ডার images/ তৈরি করে git add images আর git status চালানো হলো। কী দেখা যাবে, আর ফোল্ডারটি track করার প্রচলিত উপায় কী?

  • images/ staged দেখাবে; আর কিছু লাগবে না
  • কোনো বদল দেখাবে না; ভেতরে .gitkeep নামের একটি ফাঁকা file রেখে add করা
  • Error দেখাবে; git add --empty-dirs দিতে হবে
  • images/ untracked দেখাবে; .gitignore-এ লিখলে track হবে

একটি commit করা file debug.log নিজের কম্পিউটারে রাখতে চান, কিন্তু Git যেন আর track না করে। কোন command?

  • git rm debug.log
  • git restore debug.log
  • git reset --hard
  • git rm --cached debug.log, তারপর commit

শব্দকোষ

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

মনে রাখুন

  1. git init -b main ফোল্ডারে একটি .git বানায় — সেটিই repository; হাতে বদলাবেন না, মুছবেন না।
  2. তিন এলাকা: working tree → git add → staging area (index) → git commit → repository. Commit নেয় index-এর কপি.
  3. অবস্থা: untracked (??), unmodified, modified (" M"), staged ("M ", A); দুই অক্ষর = index বনাম HEAD, ফোল্ডার বনাম index.
  4. Commit = tree (পুরো snapshot) + parent + author + committer + বার্তা, তার hash-ই ID; প্রথম commit-এ parent নেই।
  5. commit -a নতুন file নেয় না; rm --cached ফোল্ডারে রাখে; Git নাম বদল অনুমান করে; খালি ফোল্ডারে .gitkeep; আর প্রতি commit-এ একটি উদ্দেশ্য.

পরের পর্বে: আজ git status আর git log --oneline এক ঝলক দেখলেন। পর্ব ০৯-এ এগুলোর পুরো শক্তি — git diff-এর তিন রূপ (ফোল্ডার বনাম index, index বনাম commit, দুই commit), log-এর graph আর খোঁজা, আর git show দিয়ে যেকোনো commit-এর ভেতর দেখা।

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

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