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

History আর পার্থক্য দেখা — status, diff, log আর show

Inspecting a Repository — git status, git diff, git log, git show and Revision Names

Taught in Bangla, this lesson pairs four questions with git status, git diff, git log and git show. It decodes short status codes, which areas each git diff form compares, how to read a unified diff hunk header line by line, and how diffs are computed with LCS and Myers. It also covers git log filters and formats, revision names like HEAD~2 versus HEAD^2, git shortlog and .mailmap, with runnable models.

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

  • git status-এর লম্বা আর ছোট (-s, -sb) আউটপুট পড়া, আর প্রতিটি দুই-অক্ষরের কোডের মানে
  • git diff-এর প্রতিটি রূপ কোন দুটি এলাকা বা commit মেলায়, আর --stat, --name-only, --word-diff
  • একটি unified diff লাইন ধরে ধরে পড়া — header, @@-এর চারটি সংখ্যা, +/− আর context
  • Diff কীভাবে হিসাব হয় — LCS, Myers-এর algorithm আর অন্য বিকল্প
  • git log-এর filter আর format, git show, HEAD~2 বনাম HEAD^2, আর git shortlog ও .mailmap

১ · তিনটি এলাকা, চারটি প্রশ্ন

একটি পরিচিত দৃশ্য দিয়ে শুরু করি। রহিম একটি কাল্পনিক মুদি দোকানের হিসাবের ছোট program বানাচ্ছেন — bill.py ফাইলে দামের যোগফল, prices.txt ফাইলে দামের তালিকা। সকালে কয়েকটি জিনিস বদলালেন, দুপুরে আরও কিছু। বিকেলে commit করতে বসে তাঁর মাথায় চারটি প্রশ্ন: কোন কোন ফাইল বদলেছি? প্রতিটিতে ঠিক কী বদলেছে? এর মধ্যে কতটা পরের commit-এ যাবে? আর গত সপ্তাহে Nasrin যে discount-এর কাজ করেছিলেন, সেটা কোন commit-এ?

Git-এ এই চারটি প্রশ্নের জন্য চারটি command আছে, আর চারটিই শুধু পড়ে — কোনো ফাইল, index বা history বদলায় না। তাই এগুলো ভয় ছাড়াই যতবার খুশি চালানো যায়। কিন্তু উত্তর ঠিকভাবে পড়তে হলে আগে মনে রাখতে হবে, পর্ব ৮-এ (প্রথম Git repository) দেখা তিনটি এলাকা কী কী।

পরীক্ষার খাতার কথা ভাবুন। প্রথমে খসড়া পাতায় অঙ্ক কষেন — কাটাকুটি, ভুল, আবার লেখা। এটি working tree. তারপর যে অংশগুলো ঠিক মনে হয়, সেগুলো বেছে ফেয়ার কপিতে তোলার জন্য আলাদা করেন — এটি staging area, যার আরেক নাম index. শেষে ফেয়ার কপি জমা দেন — এটি commit; জমা দেওয়া খাতা আর বদলানো যায় না, শুধু নতুন খাতা জমা দেওয়া যায়। এখন “খসড়ার সাথে বাছাই করা অংশের পার্থক্য কী?” আর “বাছাই করা অংশের সাথে গতবার জমা দেওয়া খাতার পার্থক্য কী?” — দুটি আলাদা প্রশ্ন। Git-এও তাই আলাদা command.

তিনটি এলাকা আর কোন diff কোন দুটি মেলায়

Working tree, index and HEAD — and the diff command between each pair

git diff HEAD git diff git diff --staged Working tree কাজের ফোল্ডার (খসড়া) Index staging area (বাছাই করা) HEAD শেষ commit (জমা দেওয়া) git add git commit git diff A B — দুটি commit মেলায়, ফোল্ডার আর index ছোঁয় না
ছবি ৯.১ — ওপরের তিনটি তীর তিন জোড়া এলাকার তুলনা। নিচের তীর দুটি দেখায় কাজ কোন দিকে এগোয়: git add খসড়া থেকে বাছাইয়ে, git commit বাছাই থেকে history-তে।

তিনটি এলাকায় একই ফাইলের তিনটি ভিন্ন রূপ থাকতে পারে। আর HEAD হলো “আপনি এখন কোথায়” তার চিহ্ন — সাধারণত একটি branch-এর নাম, আর সেই branch-এর সর্বশেষ commit. Branch আর HEAD-এর ভেতরের কাজ পর্ব ১২-এ (Branch — pointer, HEAD, switch) আসবে; আজ শুধু জানলেই চলবে যে HEAD মানে “শেষ commit”.

এই পর্বের সব আউটপুট আসল: Git ২.৫৩ দিয়ে একটি খালি folder-এ bazar নামের একটি ছোট repository বানিয়ে চালানো। লেখক Rahim (rahim@example.com) আর Nasrin (nasrin@example.com), দুজনই কাল্পনিক। Commit-এর সময় ইচ্ছে করে নির্দিষ্ট করা — ১৫ থেকে ২২ জানুয়ারি ২০২৬, প্রতিদিন সকাল ১০টা (+06:00). একই ফাইল, একই নাম আর একই সময় দিয়ে আপনি নিজে বানালে commit-এর hash-ও হুবহু একই আসবে, কারণ hash হিসাব হয় এই তথ্যগুলো থেকেই। History-টির commit graph এরকম — আট দিনে আটটি commit, মাঝে একটি ছোট branch আর একটি merge:

$ git log --oneline --graph --all --decorate
*   8ef86df (HEAD -> main) Merge branch 'discount'
|\  
| * 4663630 (discount) Apply eid discount to the total
| * 7830cb6 Add eid discount function
* | dbc8f82 Update onion price
* | 50032f9 Move notes into docs folder
|/  
* 058bc63 Fix price names and round the total
* daa3560 Add bill calculator
* 38690bb Add price list and README

এখনই সব চিহ্ন বোঝার দরকার নেই। অংশ ৬-এ এই ছবি লাইন ধরে পড়ব, আর অংশ ৭-এ এর ওপর হাঁটব।

২ · git status — এখন কোন ফাইল কোন অবস্থায়

ধরুন উপরের history-র পরে রহিম আরও কিছু কাজ করলেন, এখনো commit করেননি: prices.txt-এ চিনির দাম যোগ করে git add করলেন; bill.py-তে discount-এর হার বদলে git add করলেন, তারপর ফাইলের প্রথম লাইনের মন্তব্যও বদলালেন (সেটি add করেননি); README.md-তে একটি লাইন যোগ করলেন; নতুন test_bill.py বানিয়ে add করলেন; git mv দিয়ে docs/notes.txt-এর নাম বদলালেন; আর todo.txt নামে একটি নতুন ফাইল খুললেন, add করেননি। এবার git status:

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   bill.py
	renamed:    docs/notes.txt -> docs/meeting-notes.txt
	modified:   prices.txt
	new file:   test_bill.py

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:   README.md
	modified:   bill.py

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

আউটপুটের তিনটি অংশ তিনটি আলাদা তুলনা:

  • Changes to be committed — index আর শেষ commit-এর পার্থক্য। এখন git commit চালালে ঠিক এগুলোই যাবে। ফাইলের নাম বদল (renamed) আর নতুন ফাইল (new file)-ও এখানে।
  • Changes not staged for commit — working tree আর index-এর পার্থক্য। বদলেছেন, কিন্তু add করেননি; commit চালালে এগুলো যাবে না.
  • Untracked files — Git এই ফাইলগুলোকে এখনো চেনে না: এগুলো শেষ commit-এও নেই, index-এও নেই। এমন ফাইলকে বলে untracked file; বাকিগুলো tracked.

খেয়াল করুন, bill.py দুটি অংশেই আছে! কারণ এর তিনটি রূপ তিন রকম: শেষ commit-এ পুরোনো হার, index-এ নতুন হার, আর working tree-তে নতুন হার আর নতুন মন্তব্য। Git ফাইল নয়, ফাইলের অবস্থা stage করে — git add চালানোর মুহূর্তে যা ছিল, তা-ই। পরে বদলালে সেটা আবার আলাদা পরিবর্তন।

বন্ধনীর ভেতরের পরামর্শগুলো (use "git restore --staged ...") Git-এর version-ভেদে আলাদা। Git ২.২৩ (২০১৯)-এ git restore আর git switch আসার আগে এখানে লেখা থাকত git reset HEAD <file> আর git checkout -- <file>. এগুলো ঠিক কী করে আর কখন বিপজ্জনক, তা পর্ব ১১-এ (ভুল শোধরানো)। আর যে ফাইল কখনো track করবেন না (যেমন build-এর ফল), সেগুলো Untracked তালিকা থেকে সরানোর উপায় .gitignore — পর্ব ১০-এ (.gitignore আর git config).

ছোট রূপ: git status -s আর দুই কলামের কোড

লম্বা আউটপুট নতুনদের জন্য ভালো, কিন্তু অনেক ফাইলে দ্রুত চোখ বোলাতে ছোট রূপ (-s বা --short) সুবিধার। -b যোগ করলে প্রথম লাইনে branch-এর নামও দেখায়:

$ git status -s
 M README.md
MM bill.py
R  docs/notes.txt -> docs/meeting-notes.txt
M  prices.txt
A  test_bill.py
?? todo.txt
$ git status -sb
## main
 M README.md
MM bill.py
R  docs/notes.txt -> docs/meeting-notes.txt
M  prices.txt
A  test_bill.py
?? todo.txt

প্রতিটি লাইনের শুরুতে ঠিক দুটি অক্ষর, তারপর একটি ফাঁকা ঘর, তারপর path. প্রথম অক্ষর X = index বনাম শেষ commit (staged অংশ); দ্বিতীয় অক্ষর Y = working tree বনাম index (unstaged অংশ)। ফাঁকা ঘর মানে “এই তুলনায় পার্থক্য নেই”। তাই M␣ আর ␣M দেখতে প্রায় এক, অর্থ সম্পূর্ণ আলাদা — ফাঁকা ঘরটি কোন পাশে, সেটাই আসল খবর।

কোডমানেcommit চালালে কী যাবে
M␣বদলেছে, পুরোটা stage করাপুরো পরিবর্তন
␣Mবদলেছে, কিছুই stage করা নয়কিছুই না
MMStage করার পর আবার বদলেছেশুধু stage করা অংশ
A␣ / AMনতুন ফাইল index-এ যোগ হয়েছে / যোগের পর আবার বদলেছেAdd করার সময়ের রূপ
D␣ / ␣Dমুছে stage করা (git rm) / শুধু folder থেকে মুছেছেমোছা যাবে / কিছুই না
R␣নাম বদল stage করা — পুরোনো -> নতুননাম বদল
??Untracked — Git চেনে নাকিছুই না
!!Ignored — শুধু --ignored দিলে দেখায়কিছুই না
UU (এবং AA, DU …)Merge conflict, এখনো মেটানো হয়নিমেটানোর আগে commit হবে না

টেবিলে “␣” চিহ্ন একটি ফাঁকা ঘর বোঝাতে; আসল আউটপুটে সেখানে সাধারণ space থাকে।

নিচের ছোট program-টি ঠিক এই নিয়মে প্রতিটি লাইন ভেঙে ব্যাখ্যা করে, আর বলে কোন diff command দিয়ে পরিবর্তনটি দেখা যাবে। উপরের আসল আউটপুটের ছয়টি লাইনের সাথে আরও তিনটি সাধারণ অবস্থা যোগ করা হয়েছে:

// Explain "git status --short" lines. X = index vs HEAD, Y = working tree vs index.
const CODE = { " ": "unchanged", M: "modified", T: "type changed", A: "added", D: "deleted",
               R: "renamed", C: "copied", U: "unmerged" };
const lines = [" M README.md", "MM bill.py", "R  docs/notes.txt -> docs/meeting-notes.txt",
  "M  prices.txt", "A  test_bill.py", "?? todo.txt", "AM app.js", " D old.txt", "UU shop.py"];
function explain(line) {
  const X = line[0], Y = line[1], path = line.slice(3);
  if (X === "?" && Y === "?") return [path, "untracked: Git does not follow it yet", "-"];
  if (X === "!" && Y === "!") return [path, "ignored (shown only with --ignored)", "-"];
  if (X === "U" || Y === "U" || (X === "A" && Y === "A") || (X === "D" && Y === "D"))
    return [path, "merge conflict: fix the file, then git add", "git diff"];
  const cmds = [];
  if (X !== " ") cmds.push("git diff --staged");   // index differs from HEAD
  if (Y !== " ") cmds.push("git diff");            // working tree differs from index
  return [path, "index: " + CODE[X] + " | worktree: " + CODE[Y], cmds.join(" + ")];
}
const rows = lines.map((l) => [l.slice(0, 2).replace(/ /g, "_"), ...explain(l)]);
console.log("XY  " + "path".padEnd(24) + "meaning".padEnd(44) + "see it with");
for (const [xy, p, m, c] of rows)
  console.log(xy.padEnd(4) + (p.length > 23 ? p.slice(0, 20) + "..." : p).padEnd(24) + m.padEnd(44) + c);
const staged = lines.filter((l) => !"?! U".includes(l[0])).length;
const unstaged = lines.filter((l) => !"?! U".includes(l[1])).length;
console.log(`\n${staged} entries have something staged, ${unstaged} have unstaged changes.`);

আউটপুট:

XY  path                    meaning                                     see it with
_M  README.md               index: unchanged | worktree: modified       git diff
MM  bill.py                 index: modified | worktree: modified        git diff --staged + git diff
R_  docs/notes.txt -> do... index: renamed | worktree: unchanged        git diff --staged
M_  prices.txt              index: modified | worktree: unchanged       git diff --staged
A_  test_bill.py            index: added | worktree: unchanged          git diff --staged
??  todo.txt                untracked: Git does not follow it yet       -
AM  app.js                  index: added | worktree: modified           git diff --staged + git diff
_D  old.txt                 index: unchanged | worktree: deleted        git diff
UU  shop.py                 merge conflict: fix the file, then git add  git diff

5 entries have something staged, 4 have unstaged changes.

আউটপুটে ফাঁকা ঘর বোঝাতে _ ছাপা হয়েছে। নিয়ম একটাই: X ফাঁকা না হলে staged কিছু আছে, তা দেখায় git diff --staged; Y ফাঁকা না হলে unstaged কিছু আছে, তা দেখায় git diff. তাই MM bill.py আর AM app.js দুটি command-এই আসে। ?? ফাইলের কোনো diff নেই, কারণ তুলনার মতো আগের কোনো রূপ Git-এর কাছে নেই। শেষ লাইন: ৯টির মধ্যে ৫টিতে staged কিছু আছে (MM, R␣, M␣, A␣, AM), ৪টিতে unstaged (␣M, MM, AM, ␣D); conflict-এর লাইন দুই হিসাবেই বাদ।

Script বা CI-তে git status-এর আউটপুট parse করতে হলে -s নয়, git status --porcelain ব্যবহার করুন। মানুষের জন্য সাজানো আউটপুট (রং, ভাষা, পরামর্শ) Git-এর version আর সেটিং অনুযায়ী বদলাতে পারে; porcelain format ইচ্ছে করে স্থির রাখা হয়।

৩ · git diff — কোন দুটি জিনিসের তুলনা

git status বলে কোন ফাইল বদলেছে; diff বলে কোন লাইন. সবচেয়ে বড় ভুল বোঝাবুঝি হলো ভাবা যে git diff মানে “শেষ commit-এর পর যা বদলেছে”। না — শুধু git diff মেলায় working tree আর index. তাই যা add করে ফেলেছেন, তা git diff-এ আর দেখায় না। অনেকে git add . করে git diff চালিয়ে কিছু না দেখে ভাবেন “কিছুই বদলায়নি” — অথচ সব পরিবর্তন index-এ বসে আছে।

নিচের stepper-এ bill.py-এর তিনটি রূপের ওপর চারটি diff চালানো হয়েছে। প্রতিটি ধাপে দেখুন বাঁ দিক (−, a/) আর ডান দিক (+, b/) কোন এলাকা, আর index লাইনের দুটি hash কোথা থেকে আসছে:

তুলনা
Command
বাঁ দিক (−, a/)
ডান দিক (+, b/)
যা দেখায়
bill.py-এর তিনটি রূপশুধু বদলানো দুটি লাইন
HEAD     f5d75db  # bill.py - grocery bill for a corner shop
                  return amount * 0.9 if amount >= 1000 ...
Index    b9360c2  # bill.py - grocery bill for a corner shop
                  return amount * 0.95 if amount >= 1000 ...
Worktree b7a6d96  # bill.py - ... corner shop (prices in taka)
                  return amount * 0.95 if amount >= 1000 ...
Working tree বনাম indexGit ২.৫৩
$ git diff -- bill.py
diff --git a/bill.py b/bill.py
index b9360c2..b7a6d96 100644
--- a/bill.py
+++ b/bill.py
@@ -1,4 +1,4 @@
-# bill.py - grocery bill for a corner shop
+# bill.py - grocery bill for a corner shop (prices in taka)
 PRICES = {}
 
 def load_prices(path):
Index বনাম HEADGit ২.৫৩
$ git diff --staged -- bill.py
diff --git a/bill.py b/bill.py
index f5d75db..b9360c2 100644
--- a/bill.py
+++ b/bill.py
@@ -22,4 +22,4 @@ def show(cart):
     print("Total:", eid_discount(bill_total(cart)))
 
 def eid_discount(amount):
-    return amount * 0.9 if amount >= 1000 else amount
+    return amount * 0.95 if amount >= 1000 else amount
Working tree বনাম HEADGit ২.৫৩
$ git diff HEAD -- bill.py
diff --git a/bill.py b/bill.py
index f5d75db..b7a6d96 100644
--- a/bill.py
+++ b/bill.py
@@ -1,4 +1,4 @@
-# bill.py - grocery bill for a corner shop
+# bill.py - grocery bill for a corner shop (prices in taka)
 PRICES = {}
 
 def load_prices(path):
@@ -22,4 +22,4 @@ def show(cart):
     print("Total:", eid_discount(bill_total(cart)))
 
 def eid_discount(amount):
-    return amount * 0.9 if amount >= 1000 else amount
+    return amount * 0.95 if amount >= 1000 else amount
দুটি commitGit ২.৫৩
$ git diff HEAD~2 HEAD~1
diff --git a/prices.txt b/prices.txt
index f676f31..8fbde9b 100644
--- a/prices.txt
+++ b/prices.txt
@@ -1,5 +1,5 @@
 rice,72
 lentil,110
-onion,60
+onion,80
 potato,35
 oil,175
  1. একই ফাইল, তিনটি রূপ। শেষ commit-এ (HEAD) হার 0.9; index-এ 0.95, কারণ হার বদলে add করা হয়েছিল; working tree-তে 0.95 আর প্রথম লাইনে নতুন মন্তব্য, যা add করা হয়নি। পাশের ছোট hash-গুলো প্রতিটি রূপের বিষয়বস্তুর hash — Git-এর ভাষায় একটি blob-এর নাম (পর্ব ৫২-এ বিস্তারিত)।
  2. git diff — working tree বনাম index. শুধু মন্তব্যের লাইন দেখাচ্ছে, কারণ হারের পরিবর্তন index-এ আগেই আছে — দুই পাশে একই। index b9360c2..b7a6d96: বাঁয়ে index-এর blob, ডানে working tree-র বিষয়বস্তুর hash. অর্থাৎ “এখন add করলে যা যোগ হবে”।
  3. git diff --staged — index বনাম HEAD. এখন শুধু হারের পরিবর্তন: “এখন commit করলে যা যাবে”। --staged আর --cached একই option-এর দুই নাম; --cached পুরোনো, --staged পরে যোগ হয়েছে মনে রাখার সুবিধার জন্য। Commit-এর আগে এই command-টিই সবচেয়ে গুরুত্বপূর্ণ।
  4. git diff HEAD — working tree বনাম HEAD. Index-কে পাশ কাটিয়ে “শেষ commit-এর পর মোট কী বদলেছে” — staged আর unstaged দুটোই, তাই দুটি hunk. index f5d75db..b7a6d96: HEAD-এর blob থেকে সরাসরি working tree.
  5. git diff A B — দুটি commit. Working tree আর index এখানে কোনো ভূমিকা রাখে না; history-র দুটি snapshot মেলানো হয়। HEAD~2 আর HEAD~1 কী, তা অংশ ৭-এ — এখানে এরা দুটি পাশাপাশি commit, যার মধ্যে পেঁয়াজের দাম ৬০ থেকে ৮০ হয়েছে। git diff HEAD~2..HEAD~1 লিখলেও একই ফল।
মূল ধারণা

প্রতিটি diff দুটি জিনিসের তুলনা — আগে জিজ্ঞেস করুন “কোন দুটি?” কিছু না দিলে: working tree বনাম index. --staged: index বনাম HEAD. একটি commit দিলে: working tree বনাম সেই commit. দুটি commit দিলে: সেই দুটি। শেষে -- path দিলে শুধু সেই ফাইল বা folder.

সংক্ষেপে দেখা: --stat আর --name-only

বড় পরিবর্তনে প্রথমে পুরো diff নয়, একটি সারাংশ দেখা ভালো। --stat প্রতিটি ফাইলে কত লাইন যোগ (+) আর কত লাইন বাদ (-) হয়েছে তার একটি ছোট দণ্ড দেখায়; --name-only শুধু ফাইলের নাম:

$ git diff --stat
 README.md | 2 ++
 bill.py   | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)
$ git diff --cached --stat
 bill.py                               | 2 +-
 docs/{notes.txt => meeting-notes.txt} | 0
 prices.txt                            | 1 +
 test_bill.py                          | 3 +++
 4 files changed, 5 insertions(+), 1 deletion(-)
$ git diff HEAD --stat
 README.md                             | 2 ++
 bill.py                               | 4 ++--
 docs/{notes.txt => meeting-notes.txt} | 0
 prices.txt                            | 1 +
 test_bill.py                          | 3 +++
 5 files changed, 8 insertions(+), 2 deletions(-)
$ git diff --name-only
README.md
bill.py

যোগফল মিলিয়ে দেখুন: unstaged ৩টি যোগ আর ১টি বাদ, staged ৫টি যোগ আর ১টি বাদ, আর git diff HEAD-এ দুটো মিলে ৮ আর ২. bill.py-এর 2 +- (১টি বদল = ১ বাদ + ১ যোগ) দুই জায়গায়, আর HEAD-এর সাথে 4 ++--. খেয়াল করুন, Git একটি লাইন “বদলানো” বলে আলাদা কিছু জানে না — একটি বদল মানে একটি লাইন বাদ আর একটি নতুন লাইন যোগ। নাম বদলের লাইনে {notes.txt => meeting-notes.txt} আর 0: বিষয়বস্তু একটুও বদলায়নি।

Commit বনাম commit, আর দুই বিন্দু বনাম তিন বিন্দু

$ git diff --stat HEAD~3 HEAD
 bill.py                     | 5 ++++-
 notes.txt => docs/notes.txt | 1 +
 prices.txt                  | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)
$ git diff --name-only HEAD~3 HEAD
bill.py
docs/notes.txt
prices.txt
$ git diff --stat main~1..discount
 bill.py                     | 5 ++++-
 docs/notes.txt => notes.txt | 1 -
 prices.txt                  | 2 +-
 3 files changed, 5 insertions(+), 3 deletions(-)
$ git diff --stat main~1...discount
 bill.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

প্রথম দুটি সোজা: 058bc63 (HEAD~3) থেকে শেষ commit পর্যন্ত তিনটি ফাইল বদলেছে। মাঝের লাইনে notes.txt => docs/notes.txt — Git নিজে নাম বদল লিখে রাখে না; একটি ফাইল উধাও আর প্রায় একই বিষয়বস্তুর নতুন ফাইল দেখে অনুমান করে যে এটি নাম বদল। একে বলে rename detection.

শেষ দুটি ফাঁদের মতো। git diff A..B আসলে git diff A B-এর আরেক লেখা — দুই প্রান্তের সরাসরি তুলনা। main~1-এ notes ফাইলটি docs/-এ সরানো হয়েছিল, কিন্তু discount branch-এ তা হয়নি; তাই সরাসরি তুলনায় দেখায় যেন discount ফাইলটিকে ফেরত এনেছে (docs/notes.txt => notes.txt). অথচ discount branch-এ কেউ notes ছুঁয়েও দেখেননি! তিন বিন্দু A...B মেলায় দুই branch-এর শেষ সাধারণ পূর্বপুরুষ — merge base — থেকে B পর্যন্ত, অর্থাৎ “B-র দিকে কী কী করা হয়েছে”। তাই শুধু bill.py. Pull request-এর পাতায় দেখা diff সাধারণত এই তিন-বিন্দুর ধারণার কাছাকাছি (platform-ভেদে খুঁটিনাটি আলাদা), আর merge base-এর হিসাব পর্ব ৫৪-এ।

.. আর ...-এর মানে git diff আর git log-এ এক নয়। git log A..B মানে “B থেকে পৌঁছানো যায় কিন্তু A থেকে যায় না এমন commit” — অংশ ৬-এ দেখবেন। একই চিহ্ন, দুই command-এ দুই অর্থ; AI-ও প্রায়ই এখানে গুলিয়ে ফেলে।

শব্দ ধরে দেখা: --word-diff

$ git diff HEAD~2 HEAD~1
diff --git a/prices.txt b/prices.txt
index f676f31..8fbde9b 100644
--- a/prices.txt
+++ b/prices.txt
@@ -1,5 +1,5 @@
 rice,72
 lentil,110
-onion,60
+onion,80
 potato,35
 oil,175
$ git diff --word-diff HEAD~2 HEAD~1
diff --git a/prices.txt b/prices.txt
index f676f31..8fbde9b 100644
--- a/prices.txt
+++ b/prices.txt
@@ -1,5 +1,5 @@
rice,72
lentil,110
[-onion,60-]{+onion,80+}
potato,35
oil,175

সাধারণ diff পুরো লাইন বাদ আর পুরো লাইন যোগ দেখায়। --word-diff একই পরিবর্তন এক লাইনে দেখায়: [-…-] বাদ, {+…+} যোগ। লম্বা অনুচ্ছেদ বা documentation-এ একটি শব্দ বদলালে এটি খুব কাজের। কিন্তু খেয়াল করুন, পুরো onion,60 একটি “শব্দ” ধরা হয়েছে — Git ডিফল্টভাবে শব্দ আলাদা করে শুধু ফাঁকা জায়গা দিয়ে, কমা দিয়ে নয়। চাইলে --word-diff-regex দিয়ে শব্দের নিয়ম বদলানো যায়। আর শুধু space বা tab-এর পরিবর্তন উপেক্ষা করতে -w (--ignore-all-space) — formatting বদলানো কোড review-এ খুব কাজের।

৪ · Unified diff লাইন ধরে পড়া

Code review, pull request, AI-এর দেওয়া patch — সবখানেই আপনি একই বিন্যাস দেখবেন: unified diff. এটি পড়তে পারা আজকের developer-এর মৌলিক সাক্ষরতা। নিচে 058bc63 commit-টি — রহিম একসাথে দুটি ছোট ভুল ঠিক করেছিলেন: দামের তালিকার নামের আশপাশের ফাঁকা জায়গা ছাঁটা, আর মোট টাকা দুই দশমিক ঘরে গোল করা। দুটি পরিবর্তন ফাইলের দুই জায়গায়, তাই দুটি hunk:

$ git show HEAD~3
commit 058bc631869c00d5d9573647e4c74a49bde9446b
Author: Rahim <rahim@example.com>
Date:   Sat Jan 17 10:00:00 2026 +0600

    Fix price names and round the total

diff --git a/bill.py b/bill.py
index 187744d..22b830e 100644
--- a/bill.py
+++ b/bill.py
@@ -5,7 +5,7 @@ def load_prices(path):
     with open(path) as f:
         for line in f:
             name, price = line.split(",")
-            PRICES[name] = float(price)
+            PRICES[name.strip()] = float(price)
 
 def line_total(item, qty):
     return PRICES[item] * qty
@@ -14,7 +14,7 @@ def bill_total(cart):
     total = 0
     for item, qty in cart:
         total += line_total(item, qty)
-    return total
+    return round(total, 2)
 
 def show(cart):
     for item, qty in cart:

প্রথম hunk-টি লাইন ধরে পড়ি। “পরে” চাপুন আর ডানের হিসাবটি খেয়াল করুন — পুরোনো ফাইলের কোন লাইন, নতুন ফাইলের কোন লাইন, আর @@-এর সংখ্যাগুলো কীভাবে মেলে:

1diff --git a/bill.py b/bill.py2index 187744d..22b830e 1006443--- a/bill.py4+++ b/bill.py5@@ -5,7 +5,7 @@ def load_prices(path):6     with open(path) as f:7         for line in f:8             name, price = line.split(",")9-            PRICES[name] = float(price)10+            PRICES[name.strip()] = float(price)11 12 def line_total(item, qty):13     return PRICES[item] * qty
লাইনের হিসাব
পুরোনো ফাইল (a/)
নতুন ফাইল (b/)
গোনা (a / b)
  1. diff --git a/bill.py b/bill.py — একটি ফাইলের diff এখান থেকে শুরু। a/ মানে তুলনার বাঁ দিক (পুরোনো), b/ মানে ডান দিক (নতুন)। নাম বদল হলে দুটি path আলাদা হয়।
  2. index 187744d..22b830e 100644 — পুরোনো আর নতুন বিষয়বস্তুর ছোট hash, তারপর ফাইলের mode. 100644 মানে সাধারণ ফাইল; চালানো যায় এমন script হলে 100755. নতুন ফাইলে বাঁ দিকের hash সব শূন্য (0000000).
  3. --- আর +++ — এই দুটি লাইন ঠিক করে দেয় নিচে কোন চিহ্ন কোন দিকের: - দিয়ে শুরু লাইন শুধু পুরোনো ফাইলে, + দিয়ে শুরু লাইন শুধু নতুনটিতে। নতুন ফাইল হলে --- /dev/null, মুছে ফেলা ফাইল হলে +++ /dev/null.
  4. @@ -5,7 +5,7 @@ — hunk header. -5,7: পুরোনো ফাইলের লাইন ৫ থেকে শুরু করে ৭টি লাইন এই hunk-এ আছে (৫ থেকে ১১)। +5,7: নতুন ফাইলের লাইন ৫ থেকে ৭টি লাইন। দুই দিকের মোট সমান, কারণ একটি লাইন বাদ আর একটি যোগ হয়েছে। লাইন নম্বর গোনা হয় ১ থেকে।
  5. def load_prices(path): — শুধু ঠিকানা-চিহ্ন। দ্বিতীয় @@-এর পরের লেখাটি পরিবর্তনের অংশ নয়। Hunk শুরুর আগের সবচেয়ে কাছের একটি “function-এর মতো দেখতে” লাইন — Git ডিফল্টভাবে অক্ষর দিয়ে শুরু হওয়া লাইন খোঁজে — পাঠককে বোঝাতে যে পরিবর্তনটি কোন function-এর ভেতরে। এটা অনুমান, তাই অনেক সময় ভুল লাইনও আসে।
  6. তিনটি context line. শুরুতে একটি ফাঁকা ঘর মানে লাইনটি দুই ফাইলেই আছে, বদলায়নি। পুরোনোর লাইন ৫, ৬, ৭ আর নতুনের লাইন ৫, ৬, ৭ — দুই দিকের গোনা ৩ করে। Git ডিফল্টভাবে প্রতিটি পরিবর্তনের আগে-পরে ৩টি করে context দেখায় (-U দিয়ে বদলানো যায়, যেমন -U1).
  7. - লাইন — পুরোনোর লাইন ৮ বাদ। এটি শুধু পুরোনো দিকে গোনা হয়: পুরোনোর হিসাব ৩ থেকে ৪, নতুনের হিসাব ৩-ই থাকে।
  8. + লাইন — নতুনের লাইন ৮ যোগ। এটি শুধু নতুন দিকে গোনা: এখন ৪ / ৪. Git সবসময় একটি জায়গার বাদ যাওয়া লাইনগুলো আগে, যোগ হওয়া লাইনগুলো পরে দেখায়।
  9. শেষের তিনটি context. একটি ফাঁকা লাইন (শুধু একটি space), তারপর দুটি কোডের লাইন। মোট দাঁড়াল পুরোনো দিকে ৩ + ১ + ৩ = ৭, নতুন দিকে ৩ + ১ + ৩ = ৭ — header-এর -5,7 +5,7-এর সাথে হুবহু মিলল। Hunk-এর লাইন গুনে header মেলানো — patch সঠিক কি না যাচাইয়ের সবচেয়ে সরল উপায়।

দ্বিতীয় hunk @@ -14,7 +14,7 @@ def bill_total(cart): একইভাবে পড়ুন: পুরোনো লাইন ১৪ থেকে ২০, পরিবর্তনটি লাইন ১৭-এ। প্রথম hunk-এ লাইন সংখ্যা বাড়েনি-কমেনি, তাই দুই দিকের শুরু একই (১৪)। যদি প্রথম hunk-এ দুটি লাইন যোগ হতো, তাহলে দ্বিতীয়টি হতো -14,7 +16,7 — পরের প্রতিটি hunk-এর নতুন-দিকের শুরু আগের সব যোগ-বিয়োগের হিসাব বহন করে।

যা দেখবেনঅর্থ
@@ -0,0 +1,3 @@নতুন ফাইল: পুরোনো দিকে কিছু নেই, নতুন দিকে লাইন ১ থেকে ৩টি
@@ -1,3 +0,0 @@পুরো ফাইল মুছে ফেলা
@@ -3,0 +4,2 @@মোট ০ হলে শুরুর সংখ্যাটি “এই লাইনের পরে” — পুরোনোর লাইন ৩-এর পরে দুটি লাইন ঢুকেছে (context ছাড়া, -U0-এ দেখা যায়)
@@ -7 +7 @@মোট ১ হলে সংখ্যাটি বাদ দেওয়া যায় — একটিমাত্র লাইন
\ No newline at end of fileঠিক আগের লাইনটির শেষে newline নেই — diff-এর লাইন নয়, একটি সতর্ক-চিহ্ন

দুটি পরিবর্তন কখন একটি hunk-এ মিশে যায়? যখন তাদের মাঝে অপরিবর্তিত লাইন context-এর দ্বিগুণ বা তার কম — ডিফল্টে ৬ বা কম। কারণ দুই hunk-এর context তখন একে অপরকে ছুঁয়ে ফেলত। উপরের commit-এ দুই পরিবর্তনের মাঝে (লাইন ৮ আর ১৭) ৮টি লাইন, তাই আলাদা hunk. অনুশীলন ১-এ এটি নিজে যাচাই করবেন।

৫ · Diff কীভাবে হিসাব হয় — LCS থেকে Myers

Git কীভাবে ঠিক করে কোন লাইন “মিলেছে” আর কোনটি যোগ-বিয়োগ? মূল চিন্তা: দুই ফাইলের লাইনের তালিকায় একই ক্রমে থাকা সবচেয়ে বড় লাইন-সমষ্টি খুঁজুন — এগুলো অপরিবর্তিত; বাকি পুরোনো লাইন বাদ, বাকি নতুন লাইন যোগ। এই সমষ্টির নাম longest common subsequence (LCS). LCS যত বড়, যোগ-বিয়োগ তত কম: যোগ-বিয়োগের মোট = (পুরোনো লাইন − LCS) + (নতুন লাইন − LCS).

ঈদের আগে মা গতবারের বাজারের ফর্দ দেখে নতুন ফর্দ লিখলেন। দুটি পাশাপাশি রেখে আপনি দেখতে চান কী বদলেছে। স্বাভাবিকভাবে আপনি আগে মিলে যাওয়া জিনিসগুলো জোড়া দেন — চাল, ডাল, তেল — ক্রম না ভেঙে; যা জোড়া পেল না, সেগুলোই নতুন বা বাদ। LCS ঠিক এই “সবচেয়ে বেশি জোড়া, ক্রম না ভেঙে” কাজটি নিয়ম মেনে করে।

নিচের program-টি dynamic programming দিয়ে LCS বের করে (DSA কোর্সে LCS আর edit distance দেখুন), তারপর Git-এর নিয়মে ৩টি context লাইন রেখে hunk বানায় আর সঠিক @@ header ছাপে। ইনপুট ঈদের বাজারের দুটি ফর্দ, ১৪ আর ১৫ লাইনের:

// Line diff via LCS (dynamic programming) printed as unified-diff hunks (3 context lines).
// Git's default is Myers' O(ND) algorithm (1986); both find a shortest edit script.
const a = "rice 5kg|lentil 2kg|onion 3kg|garlic 500g|ginger 250g|potato 4kg|oil 2L|salt 1kg|sugar 2kg|milk 3L|semai 4pack|dates 1kg|chicken 3kg|beef 5kg".split("|");
const b = "rice 5kg|lentil 2kg|onion 4kg|garlic 500g|ginger 250g|potato 4kg|oil 2L|salt 1kg|sugar 2kg|milk 3L|semai 4pack|dates 1kg|cardamom 50g|chicken 3kg|beef 6kg".split("|");
function diffOps(a, b) {               // ops: [kind, text, oldIndex, newIndex]
  const n = a.length, m = b.length, L = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
  for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--)
    L[i][j] = a[i] === b[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
  const ops = []; let i = 0, j = 0;
  while (i < n || j < m) {
    if (i < n && j < m && a[i] === b[j]) { ops.push([" ", a[i], i, j]); i++; j++; }
    else if (i < n && (j === m || L[i + 1][j] >= L[i][j + 1])) { ops.push(["-", a[i], i, j]); i++; }
    else { ops.push(["+", b[j], i, j]); j++; }
  }
  console.log("LCS length:", L[0][0], "of", n, "old /", m, "new lines");
  return ops;
}
function unified(ops, C = 3) {
  const ch = ops.map((o, k) => (o[0] !== " " ? k : -1)).filter((k) => k >= 0);
  let k = 0;
  while (k < ch.length) {
    let last = k;                         // join changes whose gap is at most 2*C lines
    while (last + 1 < ch.length && ch[last + 1] - ch[last] - 1 <= 2 * C) last++;
    const from = Math.max(0, ch[k] - C), to = Math.min(ops.length - 1, ch[last] + C);
    const part = ops.slice(from, to + 1);
    const oc = part.filter((o) => o[0] !== "+").length, nc = part.filter((o) => o[0] !== "-").length;
    const os = oc ? part[0][2] + 1 : part[0][2], ns = nc ? part[0][3] + 1 : part[0][3];
    console.log(`@@ -${os},${oc} +${ns},${nc} @@`);
    for (const o of part) console.log(o[0] + o[1]);
    k = last + 1;
  }
}
const ops = diffOps(a, b);
console.log("--- a/eid-list.txt\n+++ b/eid-list.txt");
unified(ops);

আউটপুট:

LCS length: 12 of 14 old / 15 new lines
--- a/eid-list.txt
+++ b/eid-list.txt
@@ -1,6 +1,6 @@
 rice 5kg
 lentil 2kg
-onion 3kg
+onion 4kg
 garlic 500g
 ginger 250g
 potato 4kg
@@ -10,5 +10,6 @@
 milk 3L
 semai 4pack
 dates 1kg
+cardamom 50g
 chicken 3kg
-beef 5kg
+beef 6kg

LCS ১২: ১৪টি পুরোনো লাইনের ১২টি মিলেছে, তাই বাদ ১৪ − ১২ = ২টি (onion 3kg, beef 5kg) আর যোগ ১৫ − ১২ = ৩টি। প্রথম hunk -1,6 +1,6: পরিবর্তন লাইন ৩-এ; আগে ৩টি context চাইলেও আছে মাত্র ২টি, তাই শুরু লাইন ১; পরে ৩টি (লাইন ৪–৬)। দ্বিতীয় hunk -10,5 +10,6: দুই পরিবর্তনের মাঝে লাইন ৪–১১, অর্থাৎ ৮টি অপরিবর্তিত লাইন (৬-এর বেশি), তাই আলাদা hunk. পুরোনো দিকে লাইন ১০–১৪ (৫টি), নতুন দিকে ১০–১৫ (৬টি), কারণ cardamom যোগ হয়েছে। একই দুটি ফাইলে git diff --no-index চালালে Git ২.৫৩ হুবহু এই hunk দেয়, শুধু দ্বিতীয় header-এর শেষে sugar 2kg যোগ করে — অংশ ৪-এর সেই “function-এর মতো দেখতে” ঠিকানা-চিহ্ন, যা সাধারণ লেখার ফাইলে যেকোনো অক্ষর-দিয়ে-শুরু লাইন হতে পারে। তুলনার নিয়মে >= বাদ দেওয়াকে আগে রাখে, তাই - লাইন +-এর আগে আসে, Git-এর মতোই।

এই DP পদ্ধতিতে সময় আর memory লাগে পুরোনো লাইন × নতুন লাইনের সমানুপাতিক — ১০ হাজার লাইনের দুই ফাইলে ১০ কোটি ঘর! Git তাই ডিফল্টভাবে ব্যবহার করে Myers-এর algorithm: Eugene W. Myers-এর ১৯৮৬ সালের প্রবন্ধ “An O(ND) Difference Algorithm and Its Variations” (Algorithmica পত্রিকায়)। এখানে N মোট লাইন আর D যোগ-বিয়োগের সংখ্যা; ফাইল দুটি প্রায় একই হলে (D ছোট, যা বাস্তবে প্রায় সবসময়) এটি খুব দ্রুত। উভয় পদ্ধতিই সবচেয়ে কম যোগ-বিয়োগের একটি diff খোঁজে, কিন্তু সমান-ছোট একাধিক diff থাকলে কোনটি বেছে নেবে, তা আলাদা হতে পারে। তার আগে Unix-এর মূল diff program-এর পেছনে ছিল James W. Hunt আর M. Douglas McIlroy-র ১৯৭৬ সালের Bell Labs-এর প্রতিবেদন “An Algorithm for Differential File Comparison”.

Git-এ --diff-algorithm দিয়ে চারটি diff algorithm বেছে নেওয়া যায়: myers (ডিফল্ট), minimal (আরও সময় নিয়ে নিশ্চিতভাবে সবচেয়ে ছোট), patience আর histogram. Patience diff-এর ধারণা সাধারণত Bram Cohen-এর বলে ধরা হয়: আগে দুই ফাইলে একবারই আসে এমন লাইন (যেমন একটি function-এর নাম) জোড়া দেওয়া, তারপর মাঝের অংশ। Histogram তার একটি দ্রুত রূপ, JGit প্রকল্প থেকে Git-এ এসেছে বলে জানা যায়। কোড সরানো বা নতুন function ঢোকানোর ক্ষেত্রে এ দুটি অনেক সময় মানুষের কাছে বেশি স্বাভাবিক diff দেয় — যেমন } বা ফাঁকা লাইনের মতো সাধারণ লাইন ভুল জায়গায় জোড়া না লাগানো — তবে সবসময় নয়, আর কোনটি “ভালো” তা অনেকটা রুচি। এই পর্বের দুটি উদাহরণে চারটিই একই ফল দেয়। স্থায়ীভাবে বদলাতে git config diff.algorithm histogram; config-এর বিস্তারিত পর্ব ১০-এ।

মূল ধারণা

Diff মানুষের উদ্দেশ্য জানে না, শুধু লাইনের মিল জানে। সবচেয়ে ছোট diff সবসময় “কী আসলে হয়েছে” তার সঠিক গল্প নয়। Review করার সময় diff দেখে উপসংহার টানুন, কিন্তু সন্দেহ হলে পুরো ফাইলটিও খুলে দেখুন।

৬ · git log — history-তে খোঁজা

মুদি দোকানের হিসাবের খাতার কথা ভাবুন: প্রতিদিনের entry-তে তারিখ, কে লিখেছেন, কী লেনদেন। মাস শেষে হয়তো জানতে চান, “কোন দিন পেঁয়াজের দাম বদলেছিল?” বা “Nasrin আপা কোন কোন entry করেছেন?” git log হলো এই খাতা পড়ার যন্ত্র — আর তার অসংখ্য option হলো খাতায় দ্রুত খোঁজার কৌশল। কিছু না দিলে এটি HEAD থেকে পেছনে হেঁটে প্রতিটি commit-এর পূর্ণ hash, লেখক, তারিখ আর বার্তা দেখায় (-n 2 বা -2 = শুধু প্রথম দুটি):

$ git log -n 2
commit 8ef86df964edcab96ca448ed6627d790f6b901d6
Merge: dbc8f82 4663630
Author: Rahim <rahim@example.com>
Date:   Thu Jan 22 10:00:00 2026 +0600

    Merge branch 'discount'

commit dbc8f8272bfed101c82d5364113b6cbadb8912e3
Author: rahim <rahim@home.example.com>
Date:   Wed Jan 21 10:00:00 2026 +0600

    Update onion price

প্রথম commit-টি একটি merge commit — তাই একটি বাড়তি লাইন Merge: dbc8f82 4663630, অর্থাৎ এর দুটি parent. লম্বা আউটপুট টার্মিনালে সাধারণত একটি pager-এ (প্রায়ই less) খোলে: স্পেস চাপলে পরের পাতা, /শব্দ দিয়ে খোঁজা, q চাপলে বেরিয়ে আসা। অনেক নতুন ব্যবহারকারী এখানে আটকে যান — শুধু q.

$ git log --oneline
8ef86df Merge branch 'discount'
dbc8f82 Update onion price
50032f9 Move notes into docs folder
4663630 Apply eid discount to the total
7830cb6 Add eid discount function
058bc63 Fix price names and round the total
daa3560 Add bill calculator
38690bb Add price list and README

--oneline প্রতিটি commit এক লাইনে: ছোট hash আর বার্তার প্রথম লাইন। ক্রমটি খেয়াল করুন: 50032f9 (২০ জানুয়ারি) এর পরে 4663630 (১৯ জানুয়ারি) — ডিফল্টে git log commit-গুলো তারিখের উল্টো ক্রমে সাজায়, ফলে দুই branch-এর commit মিশে যায়। তাই অংশ ১-এর --graph ছবিটি অনেক বেশি তথ্য দেয়: * একটি commit, | একটি ধারা, |\ যেখানে merge commit-এর দ্বিতীয় parent-এর দিকে ধারা ভাগ হয়েছে, |/ যেখানে ধারাটি branch তৈরির জায়গায় মিলেছে। --all সব branch থেকে history দেখায় (শুধু HEAD থেকে নয়), আর --decorate বন্ধনীতে branch-এর নাম দেখায় — (HEAD -> main) মানে HEAD এখন main branch-এ। নতুন Git-এ টার্মিনালে --decorate সাধারণত নিজেই চালু থাকে। --graph চালু থাকলে Git সাজানোর ক্রমও বদলায়, যাতে একটি ধারার commit পাশাপাশি থাকে।

খোঁজার filter

$ git log --oneline --author=Nasrin
4663630 Apply eid discount to the total
7830cb6 Add eid discount function
$ git log --oneline --author=Rahim
8ef86df Merge branch 'discount'
50032f9 Move notes into docs folder
058bc63 Fix price names and round the total
daa3560 Add bill calculator
38690bb Add price list and README
$ git log --oneline -i --author=rahim
8ef86df Merge branch 'discount'
dbc8f82 Update onion price
50032f9 Move notes into docs folder
058bc63 Fix price names and round the total
daa3560 Add bill calculator
38690bb Add price list and README
$ git log --oneline --since="2026-01-18T00:00+06:00" --until="2026-01-20T23:59+06:00"
50032f9 Move notes into docs folder
4663630 Apply eid discount to the total
7830cb6 Add eid discount function
$ git log --oneline --grep=discount
8ef86df Merge branch 'discount'
4663630 Apply eid discount to the total
7830cb6 Add eid discount function
  • --author=Nasrin — লেখকের নাম বা email-এ লেখাটি আছে এমন commit (আসলে একটি regular expression). খেয়াল করুন, --author=Rahim dbc8f82 দেখায়নি, কারণ সেই commit-এ নাম ছোট হাতের rahim. -i দিলে বড়-ছোট হাতের পার্থক্য উপেক্ষা হয়। একই মানুষের দুই রকম নাম — অংশ ৮-এ এর সমাধান।
  • --since / --until — সময়ের সীমা; "2 weeks ago" বা "yesterday"-এর মতো লেখাও চলে। সীমা মাপা হয় committer-এর তারিখে (লেখকের তারিখে নয়, যদিও এখানে দুটো একই)। সময় অঞ্চল (time zone) না লিখলে আপনার কম্পিউটারের অঞ্চল ধরা হয় — তাই এখানে +06:00 লিখে দেওয়া, যাতে যেকোনো দেশে একই ফল আসে।
  • --grep=discount — commit-বার্তায় খোঁজা। Merge commit-এর বার্তাতেও শব্দটি আছে, তাই সেটিও এসেছে। কোডের ভেতরে কোনো লেখা কখন ঢুকল বা বেরোল, তা খুঁজতে git log -S — সেটি পর্ব ৫৫-এ, git blame আর git bisect-এর সাথে।

একটি ফাইলের history: -- path আর --follow

$ git log --oneline -- prices.txt
dbc8f82 Update onion price
38690bb Add price list and README
$ git log --oneline -- docs/notes.txt
50032f9 Move notes into docs folder
$ git log --oneline --follow -- docs/notes.txt
50032f9 Move notes into docs folder
38690bb Add price list and README

---এর পরে path দিলে শুধু সেই ফাইল বা folder ছুঁয়েছে এমন commit. -- চিহ্নটি Git-কে বলে “এর পরে সব path, revision নয়” — ফাইলের নাম কোনো branch-এর নামের মতো হলে এটি বিভ্রান্তি এড়ায়। docs/notes.txt-এর history মাত্র একটি commit দেখাচ্ছে, কারণ এই path-এ ফাইলটি এসেছে 50032f9-এ। --follow দিলে Git নাম বদল পেরিয়ে পেছনে যায় আর পুরোনো নাম notes.txt-এর সময়কার 38690bb-ও খুঁজে পায়। --follow শুধু একটিমাত্র ফাইলের সাথে কাজ করে, আর নাম বদল চেনে অংশ ৩-এর মতো মিল দেখে অনুমানে — তাই নাম বদলের সাথে বিষয়বস্তু অনেক বদলালে ধারাটি হারাতে পারে। পরামর্শ: নাম বদল আর বড় সম্পাদনা আলাদা commit-এ করুন।

প্রতিটি commit-এর পরিবর্তনসহ: --stat আর -p

$ git log --stat -n 1 HEAD~3
commit 058bc631869c00d5d9573647e4c74a49bde9446b
Author: Rahim <rahim@example.com>
Date:   Sat Jan 17 10:00:00 2026 +0600

    Fix price names and round the total

 bill.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

--stat প্রতিটি commit-এর নিচে সারাংশ জুড়ে দেয়; -p (patch) জুড়ে দেয় পুরো diff — অংশ ৪-এর মতো। git log -p -- bill.py হলো একটি ফাইলের পুরো বিবর্তনের গল্প, commit ধরে ধরে।

নিজের মতো format

$ git log -3 --format='%h %an %ar %s'
8ef86df Rahim 9 months ago Merge branch 'discount'
dbc8f82 rahim 9 months ago Update onion price
50032f9 Rahim 9 months ago Move notes into docs folder
$ git log -3 --format='%h %an %ad %s' --date=short
8ef86df Rahim 2026-01-22 Merge branch 'discount'
dbc8f82 rahim 2026-01-21 Update onion price
50032f9 Rahim 2026-01-20 Move notes into docs folder

--format (বা --pretty=format:)-এ % দিয়ে শুরু চিহ্নগুলো তথ্যের জায়গা: %h ছোট hash, %H পূর্ণ hash, %an লেখকের নাম, %ae email, %ad লেখার তারিখ, %ar আপেক্ষিক তারিখ, %s বার্তার প্রথম লাইন, %p parent-দের ছোট hash. সাবধান: %ar-এর “9 months ago” নির্ভর করে আপনি কবে চালাচ্ছেন তার ওপর — এই আউটপুট নেওয়া হয়েছিল ২০২৬ সালের অক্টোবরের শেষে। আপনার স্ক্রিনে অন্য কিছু আসবে, কিন্তু --date=short-এর 2026-01-22 সবখানে একই। Script-এ তাই সবসময় নির্দিষ্ট তারিখের format.

Optionকাজ
--onelineপ্রতি commit এক লাইন
-n 5 / -5প্রথম ৫টি commit
--graph --all --decorateসব branch-এর ছবি, নামসহ
-p / --statপ্রতিটির diff / সারাংশ
--author= / --grep=লেখক / বার্তায় খোঁজা (-i: বড়-ছোট হাত উপেক্ষা)
--since= / --until=সময়ের সীমা
-- path / --followএকটি ফাইল বা folder / নাম বদল পেরিয়ে
--no-merges / --first-parentMerge বাদ / শুধু প্রথম parent-এর পথ (অংশ ৭)
A..BB-তে আছে কিন্তু A-তে নেই এমন commit

শেষ সারিটি অংশ ৩-এর সতর্কতার সাথে মিলিয়ে নিন। এই repository-তে git log --oneline discount..main দেখায় তিনটি commit — 8ef86df, dbc8f82, 50032f9 — যেগুলো main-এ আছে কিন্তু discount-এ নেই; আর main..discount কিছুই দেখায় না, কারণ discount-এর সব commit ইতিমধ্যে merge হয়ে main-এর অংশ।

৭ · git show আর revision-এর নাম — HEAD~ বনাম HEAD^

git show <commit> একটি commit-এর তথ্য আর তার আগের অবস্থার সাথে diff দেখায় — অংশ ৪-এ git show HEAD~3 ঠিক এটাই ছিল। আর git show <commit>:<path> দেখায় সেই commit-এর সময় ফাইলটি হুবহু কেমন ছিল, working tree না ছুঁয়ে:

$ git show HEAD~2:prices.txt
rice,72
lentil,110
onion,60
potato,35
oil,175
$ git show HEAD:prices.txt
rice,72
lentil,110
onion,80
potato,35
oil,175

পুরোনো দাম দেখতে ফাইল বদলাতে হলো না, কোনো branch-ও বদলাতে হলো না। কোলনের পরের path সবসময় repository-র মূল folder থেকে গোনা। পুরোনো ফাইলটি আলাদা নামে রাখতে চাইলে: git show HEAD~2:prices.txt > old-prices.txt.

Merge commit-এ git show একটি চমক দেয়:

$ git show HEAD
commit 8ef86df964edcab96ca448ed6627d790f6b901d6
Merge: dbc8f82 4663630
Author: Rahim <rahim@example.com>
Date:   Thu Jan 22 10:00:00 2026 +0600

    Merge branch 'discount'
$ git show --stat --format=%h HEAD
8ef86df

 bill.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

কোনো diff নেই! Merge commit-এ ডিফল্টভাবে Git একটি “মিশ্র diff” (combined diff) দেখায়, যেখানে শুধু সেই লাইনগুলো আসে যা কোনো parent-এর সাথেই মেলে না — অর্থাৎ conflict মেটাতে গিয়ে হাতে লেখা অংশ। এই merge-এ conflict ছিল না, তাই কিছুই নেই। কিন্তু --stat দিলে Git ২.৫৩ প্রথম parent-এর সাথে তুলনার সারাংশ দেখায়: merge-এর ফলে main-এ bill.py-তে ৪টি লাইন যোগ, ১টি বাদ। দুই parent-এর সাথে আলাদা আলাদা তুলনা চাইলে git show -m. Merge-এর খুঁটিনাটি পর্ব ১৩-এ (Merge আর conflict).

Commit-কে নাম দেওয়ার ভাষা

উপরে বারবার HEAD~3, HEAD~2 লিখেছি। এগুলোকে বলে revision-এর নাম: commit-কে চেনানোর লেখা, যা Git একটি hash-এ রূপান্তর করে। কয়েকটি মৌলিক রূপ:

  • পূর্ণ বা ছোট hash — 8ef86df, এমনকি 8ef8; শর্ত: অন্তত ৪টি অক্ষর আর repository-তে একটিমাত্র object-এর সাথে মেলা। বড় project-এ ৭ অক্ষরও কখনো দ্ব্যর্থক হয়, তাই Git প্রয়োজনে নিজেই লম্বা ছোট-hash দেখায়।
  • Branch-এর নাম আর HEAD — main, discount, HEAD.
  • X~n — X থেকে প্রথম parent ধরে n ধাপ পেছনে। X~ = X~1.
  • X^n — X-এর n-তম parent, এক ধাপেই। X^ = X^1 = X~1. সাধারণ commit-এর parent একটি, তাই সেখানে ^2-এর অর্থ নেই।

বংশতালিকার মতো ভাবুন। Merge commit-এর দুই parent — প্রথম parent “বাবা” (যে branch-এ বসে merge করা হয়েছিল, এখানে main), দ্বিতীয় parent “মা” (যে branch মেশানো হয়েছে, এখানে discount). HEAD~1 বাবা, HEAD~2 দাদা — বাবার বাবা, সবসময় বাবার দিক ধরে। HEAD^2 মা, আর HEAD^2~1 নানা — মায়ের বাবা। ~ শুধু বংশের একটি ধারা ধরে ওপরে ওঠে; ^n ঠিক করে দেয় এই ধাপে কোন parent.

^1 ^2 8ef86df dbc8f82 50032f9 058bc63 daa3560 38690bb 4663630 7830cb6 HEAD HEAD~1 HEAD~2 HEAD~3 HEAD~4 HEAD~5 main HEAD^2 discount HEAD^2~1 merge base
Revision → commit
লেখা
পথ
Commit
  1. HEAD — HEAD এখন main branch-এ, আর main-এর শেষ commit 8ef86df, একটি merge commit. এর দুটি parent: প্রথম dbc8f82 (merge-এর আগে main যেখানে ছিল), দ্বিতীয় 4663630 (discount-এর শেষ commit).
  2. HEAD~1 — প্রথম parent ধরে এক ধাপ: dbc8f82. HEAD^ আর HEAD^1-ও একই, কারণ এক ধাপের ক্ষেত্রে “প্রথম parent” দুই চিহ্নেই একই অর্থ।
  3. HEAD~2 — প্রথম parent ধরে দুই ধাপ: dbc8f82-এর parent 50032f9. লেখা যায় HEAD^^-ও (দুবার “প্রথম parent”). কিন্তু HEAD^2 লিখলে সম্পূর্ণ অন্য commit — পরের ধাপ দেখুন।
  4. HEAD^2 — merge commit-এর দ্বিতীয় parent, এক ধাপেই অন্য ধারায়: 4663630, Nasrin-এর discount branch-এর শেষ commit. সংখ্যাটি ^-এর পরে থাকলে মানে “কোন parent”, ~-এর পরে থাকলে মানে “কত ধাপ”।
  5. HEAD^2~1 — বাঁ থেকে ডানে পড়ুন: আগে দ্বিতীয় parent-এ (4663630), তারপর সেখান থেকে প্রথম parent ধরে এক ধাপ: 7830cb6, discount function যোগ করার commit.
  6. HEAD~3 আর HEAD^2~2 — দুই পথ একই 058bc63-এ মেলে, যেখান থেকে discount branch শুরু হয়েছিল: দুই ধারার merge base. Graph-এ একই commit-এ পৌঁছানোর একাধিক নাম থাকতেই পারে; Git-এর কাছে আসল পরিচয় শুধু hash.

এবার আসল Git দিয়ে যাচাই। git rev-parse একটি revision-এর নামকে hash-এ রূপান্তর করে; --short দিলে ছোট hash:

$ git rev-parse --short HEAD
8ef86df
$ git rev-parse --short HEAD~1
dbc8f82
$ git rev-parse --short HEAD^
dbc8f82
$ git rev-parse --short HEAD~2
50032f9
$ git rev-parse --short HEAD^2
4663630
$ git rev-parse --short HEAD^2~1
7830cb6
$ git rev-parse --short HEAD~3
058bc63
$ git rev-parse --short HEAD^2~2
058bc63
$ git log -1 --format='%h parents: %p' HEAD
8ef86df parents: dbc8f82 4663630
$ git rev-parse --short 8ef8
8ef86df
$ git log --oneline -1 HEAD~1^2
fatal: ambiguous argument 'HEAD~1^2': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

শেষ লাইনটি ইচ্ছে করে ভুল: HEAD~1 (dbc8f82) সাধারণ commit, তার দ্বিতীয় parent নেই, তাই Git “unknown revision” বলে থামে। নিচের program-টি এই নিয়মগুলো নিজে প্রয়োগ করে — একই আটটি commit, একই parent-এর তালিকা, প্রথম parent আগে:

// Resolve revision names like HEAD~2, HEAD^2, main^2~1 on a tiny commit DAG.
// parents[0] is the FIRST parent (the branch you were on when you merged).
const commits = {
  "8ef86df": { msg: "Merge branch 'discount'", parents: ["dbc8f82", "4663630"] },
  "dbc8f82": { msg: "Update onion price", parents: ["50032f9"] },
  "50032f9": { msg: "Move notes into docs folder", parents: ["058bc63"] },
  "4663630": { msg: "Apply eid discount to the total", parents: ["7830cb6"] },
  "7830cb6": { msg: "Add eid discount function", parents: ["058bc63"] },
  "058bc63": { msg: "Fix price names and round the total", parents: ["daa3560"] },
  "daa3560": { msg: "Add bill calculator", parents: ["38690bb"] },
  "38690bb": { msg: "Add price list and README", parents: [] },
};
const refs = { main: "8ef86df", discount: "4663630", HEAD: "main" };   // HEAD -> main
function base(name) {
  if (refs[name]) return refs[refs[name]] || refs[name];             // follow HEAD -> main
  const hits = Object.keys(commits).filter((h) => h.startsWith(name));
  if (name.length < 4 || hits.length !== 1) throw new Error(`'${name}': unknown or ambiguous`);
  return hits[0];
}
function resolve(rev) {
  const m = rev.match(/^([^~^]+)((?:[~^]\d*)*)$/);
  let c = base(m[1]); const path = [c];
  for (const [, op, num] of m[2].matchAll(/([~^])(\d*)/g)) {
    const k = num === "" ? 1 : Number(num);
    if (op === "^") {                                 // ^k = k-th parent (^0 = itself)
      if (k > 0) { c = commits[c].parents[k - 1]; if (!c) throw new Error(`${rev}: no parent ${k}`); }
    } else for (let s = 0; s < k; s++) {              // ~k = follow FIRST parent k times
      c = commits[c].parents[0]; if (!c) throw new Error(`${rev}: history too short`);
    }
    path.push(op + k + " " + c);
  }
  return path.join("  ");
}
for (const r of ["HEAD~1", "HEAD^", "HEAD~2", "HEAD^2", "HEAD^2~1", "HEAD~3", "HEAD^^2", "discount~2", "7830"])
  try { console.log(r.padEnd(11), resolve(r)); } catch (e) { console.log(r.padEnd(11), "fatal:", e.message); }

আউটপুট:

HEAD~1      8ef86df  ~1 dbc8f82
HEAD^       8ef86df  ^1 dbc8f82
HEAD~2      8ef86df  ~2 50032f9
HEAD^2      8ef86df  ^2 4663630
HEAD^2~1    8ef86df  ^2 4663630  ~1 7830cb6
HEAD~3      8ef86df  ~3 058bc63
HEAD^^2     fatal: HEAD^^2: no parent 2
discount~2  4663630  ~2 058bc63
7830        7830cb6

প্রতিটি লাইনে প্রথমে শুরুর commit, তারপর প্রতিটি ধাপ আর তার পরে কোথায় পৌঁছানো গেল। ~3 একটি ধাপ হিসেবে লেখা, কিন্তু ভেতরে তিনবার প্রথম parent ধরা হয়েছে। ফলগুলো উপরের আসল git rev-parse-এর সাথে হুবহু মেলে: HEAD~2 = 50032f9, HEAD^2 = 4663630, HEAD^2~1 = 7830cb6. HEAD^^2 মানে “প্রথম parent-এর দ্বিতীয় parent” — dbc8f82-এর একটিই parent, তাই ব্যর্থ; আসল Git-ও একই কারণে error দেয়। discount~2 branch-এর নাম থেকে শুরু করে merge base-এ পৌঁছায়, আর 7830 ছোট hash হিসেবে চেনা গেছে, কারণ আর কোনো hash এই চারটি অক্ষর দিয়ে শুরু হয়নি। চেষ্টা করে দেখুন: তালিকায় "HEAD^0" আর "d" যোগ করুন — প্রথমটি নিজেকেই দেয়, দ্বিতীয়টি এক অক্ষরের, তাই নাকচ।

Windows-এর পুরোনো cmd.exe-তে ^ একটি বিশেষ চিহ্ন (escape), তাই git show HEAD^ সেখানে অন্যভাবে পড়া হতে পারে; উদ্ধৃতিচিহ্ন দিন ("HEAD^") বা HEAD~1 লিখুন। zsh-এর কিছু সেটিংয়েও ^ glob চিহ্ন হিসেবে ধরা হয়। ~ সাধারণত সব shell-এ নিরাপদ।

--first-parent option ঠিক “বাবার ধারা” ধরে হাঁটে: git log --oneline --first-parent এই repository-তে দেখায় 8ef86df, dbc8f82, 50032f9, 058bc63, daa3560, 38690bb — main-এর নিজের গল্প, যেখানে পুরো discount-এর কাজ একটি merge commit হিসেবে আসে। দল যদি নিয়ম মেনে প্রতিটি feature merge করে, তাহলে এই দৃশ্য release-এর সারাংশের মতো পড়া যায়। Rebase দিয়ে history সোজা রাখার বিকল্প পথ পর্ব ১৫-এ।

৮ · দলের হিসাব — git shortlog আর .mailmap

git shortlog commit-গুলোকে লেখক অনুযায়ী ভাগ করে। -s শুধু সংখ্যা, -n বেশি থেকে কম ক্রমে, -e email-ও দেখায়:

$ git shortlog -sn HEAD
     5	Rahim
     2	Nasrin
     1	rahim
$ git shortlog -sne HEAD
     5	Rahim <rahim@example.com>
     2	Nasrin <nasrin@example.com>
     1	rahim <rahim@home.example.com>

রহিম দুবার এসেছেন! একটি commit তিনি বাড়ির ল্যাপটপ থেকে করেছিলেন, যেখানে Git-এ নাম ছোট হাতের rahim আর অন্য email দেওয়া ছিল। Git-এর কাছে এরা দুজন আলাদা মানুষ। (টার্মিনালে শুধু git shortlog -sn লিখলেই HEAD থেকে হিসাব হয়; কিন্তু script বা pipe-এর ভেতরে Git তখন commit-এর তালিকা ইনপুট থেকে পড়ার অপেক্ষা করে, তাই সেখানে HEAD লিখে দেওয়া নিরাপদ।)

পুরোনো commit বদলানো যায় না — বদলালে hash বদলে যায় (পর্ব ৫২)। সমাধান হলো repository-র মূল folder-এ একটি .mailmap ফাইল, যা বলে “এই নাম/email আসলে ওই মানুষ”:

$ cat .mailmap
Rahim <rahim@example.com> rahim <rahim@home.example.com>
$ git shortlog -sn HEAD
     6	Rahim
     2	Nasrin
$ git log -1 --format='%h %an / %aN' HEAD~1
dbc8f82 rahim / Rahim

প্রতিটি লাইনের গঠন: আসল-নাম <আসল-email> commit-এ-লেখা-নাম <commit-এ-লেখা-email>. এখন shortlog-এ রহিম ৬. Commit নিজে বদলায়নি — %an এখনো rahim দেখায়, কিন্তু %aN (বড় হাতের N) mailmap মেনে Rahim. Git ২.৫৩-এ সাধারণ git log-এর “Author:” লাইনও ডিফল্টভাবে mailmap মানে। .mailmap সাধারণত commit করে রাখা হয়, যাতে দলের সবাই একই হিসাব পান। আর ভবিষ্যতে এমন ভাঙা নাম এড়াতে প্রতিটি কম্পিউটারে user.name আর user.email একই রাখুন — পর্ব ১০-এর বিষয়।

আরও গভীরে: Git প্রতিটি commit-এ পুরো project-এর snapshot রাখে, diff রাখে না; আপনি যত diff দেখেন, সব চাহিদামতো তখনই হিসাব হয়। তাই git diff --histogram লিখে পুরোনো commit-এর diff-ও নতুন algorithm-এ দেখা যায়। Git-এর diff-এর কোড xdiff নামের একটি অংশে, যা Davide Libenzi-র LibXDiff library থেকে নেওয়া বলে জানা যায়। Myers-এর প্রবন্ধটি পড়তে কঠিন নয় — মূল চিন্তা হলো “edit graph”-এ সবচেয়ে কম তির্যক-নয় এমন ধাপে এক কোণ থেকে অন্য কোণে যাওয়া। আর সংরক্ষণের জায়গা বাঁচাতে Git ভেতরে packfile-এ object-গুলোর মধ্যে delta রাখে, যা আপনার দেখা diff থেকে সম্পূর্ণ আলাদা ব্যাপার (পর্ব ৫২)।

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

  • git diff আর git diff --staged-এর পার্থক্য? — প্রথমটি working tree বনাম index (unstaged পরিবর্তন), দ্বিতীয়টি index বনাম HEAD (পরের commit-এ যা যাবে)।
  • HEAD~2 আর HEAD^2 কি এক? — না। ~2 প্রথম parent ধরে দুই ধাপ; ^2 merge commit-এর দ্বিতীয় parent. সাধারণ commit-এ ^2 error দেয়।
  • @@ -10,4 +10,6 @@-এর মানে? — পুরোনো ফাইলের লাইন ১০ থেকে ৪টি, নতুনের লাইন ১০ থেকে ৬টি; নিট ২টি লাইন বেশি।
  • নাম বদলানো ফাইলের পুরো history কীভাবে দেখবেন? — git log --follow -- <নতুন path>.
  • git status -s-এ MM কী বোঝায়? — কিছু পরিবর্তন stage করা, তার পরে আবার বদলানো; commit করলে শুধু stage করা অংশ যাবে।

৯ · AI-এর যুগে — diff পড়তে পারাই আপনার রক্ষাকবচ

AI এখন কোড লেখে, commit-বার্তা লেখে, এমনকি পুরো patch বানিয়ে দেয়। এর মানে আপনার কাজ বদলে যাচ্ছে: লেখার চেয়ে বেশি যাচাই — আর যাচাইয়ের প্রধান যন্ত্র diff. AI-এর দেওয়া পরিবর্তন commit করার আগে git diff আর git diff --staged লাইন ধরে পড়া, --stat দিয়ে দেখা যে AI এমন কোনো ফাইল ছোঁয়নি যা ছোঁয়ার কথা ছিল না — এটুকু অভ্যাস অনেক বড় ভুল আটকায়। এই বিষয়ে AI-এর কিছু পরিচিত ভুল:

AI-এর ভুলকীভাবে ধরবেন
“Status পরিষ্কার করতে” git reset --hard বা git clean -fd পরামর্শ, কোনো সতর্কতা ছাড়া — commit না করা কাজ চিরতরে মুছে যেতে পারেযেকোনো command চালানোর আগে জিজ্ঞেস করুন: এটি কি শুধু দেখায়, না কিছু বদলায়? বদলালে আগে একটি পরীক্ষার clone-এ (পর্ব ১১)
বানানো flag, যেমন git diff --staged-only বা git log --author-emailGit সাথে সাথে বলে invalid option বা unrecognized argument; git help diff দেখুন
git diff মানে “শেষ commit-এর পরের সব পরিবর্তন” বলাঅংশ ৩-এর stepper: শুধু git diff index-এর সাথে মেলায়
diff A..B আর log A..B-কে একই অর্থে ব্যাখ্যা করা, বা HEAD^2-কে “দুই commit আগে” বলাছোট repository-তে git rev-parse আর --stat দিয়ে নিজে মিলিয়ে দেখুন
Hunk header-এর সংখ্যা ভুল এমন patch — git apply ব্যর্থ হয়, বা ভুল জায়গায় বসেHunk-এর লাইন গুনে @@-এর সাথে মেলান; git apply --check দিয়ে আগে যাচাই করুন
পুরো diff বা git log -p prompt-এ পেস্ট করতে বলা — তাতে পাসওয়ার্ড, token বা গ্রাহকের তথ্য থাকতে পারেপাঠানোর আগে পড়ুন, গোপন অংশ মুছুন; ফাঁস হলে কী করবেন, পর্ব ৫৭-এ

নিচের prompt-এ AI-কে review করতে বলা হয়েছে, কিন্তু শর্তসহ: শুধু দেখানো diff-এর ভিত্তিতে, প্রতিটি মন্তব্যে hunk আর লাইনের উল্লেখসহ, আর কোনো command সুপারিশ করলে সেটি কিছু বদলায় কি না তা বলে দিয়ে।

আমি commit করার আগে নিচের পরিবর্তনটি review করাতে চাই। এটি `git diff --staged` আর `git diff --staged --stat`-এর আউটপুট (গোপন তথ্য মুছে দিয়েছি)। [এখানে --stat-এর আউটপুট] [এখানে diff] ১. প্রতিটি hunk-এর @@ header পড়ে বলো কোন ফাইলের কোন লাইনগুলোতে কী বদলেছে। মন্তব্য করলে "ফাইল:লাইন নম্বর" উল্লেখ করো। ২. সম্ভাব্য bug, বাদ পড়া test, আর এমন কোনো পরিবর্তন যা এই commit-এর উদ্দেশ্যের সাথে মেলে না, আলাদা তালিকায় দাও। ৩. diff-এ যা নেই তা অনুমান করবে না; বাকি কোড দেখা দরকার হলে বলো কোন ফাইল দেখতে চাও। ৪. কোনো git command সুপারিশ করলে বলো সেটি শুধু দেখায়, নাকি working tree, index বা history বদলায়।

AI-এর পরিবর্তন commit করার আগে — চেকলিস্ট

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

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

প্র ০১Git তো প্রতিটি commit-এ পুরো snapshot রাখে — তাহলে diff আসে কোথা থেকে? Diff সংরক্ষণ না করার সিদ্ধান্তের লাভ-ক্ষতি কী?

পুরোনো অনেক version control (যেমন RCS আর তার ওপর গড়া CVS) প্রতিটি ফাইলের ইতিহাস রাখত পরিবর্তনের তালিকা হিসেবে — একটি version থেকে পরেরটিতে যেতে কী যোগ-বিয়োগ করতে হবে। Git ২০০৫ সালে Linus Torvalds শুরু করেন ভিন্ন চিন্তায়: প্রতিটি commit পুরো project-এর একটি snapshot-এর দিকে নির্দেশ করে, আর একই বিষয়বস্তু একবারই রাখা হয় (তার hash দিয়ে চেনা)। Diff তাই কোথাও লেখা থাকে না — আপনি যখন git diff বা git show চালান, Git দুটি snapshot বের করে তখনই হিসাব করে।

লাভ অনেক। এক, যেকোনো দুটি commit — পাশাপাশি হোক বা বছরের ব্যবধানে — একই খরচে তুলনা করা যায়, মাঝের সব পরিবর্তন জোড়া লাগাতে হয় না। দুই, diff-এর algorithm বা option বদলানো যায় পরে: আজ --histogram বা -w দিয়ে দশ বছর আগের commit-ও নতুনভাবে দেখা সম্ভব। তিন, নাম বদল আলাদা করে লিখতে হয় না; Git পরে মিল দেখে অনুমান করে। চার, একটি commit-এর hash তার পুরো অবস্থার hash, তাই ইতিহাস গোপনে বদলানো কঠিন।

ক্ষতিও আছে। নাম বদল অনুমাননির্ভর বলে কখনো ভুল হয় — ফাইলের নাম বদলের সাথে অনেক সম্পাদনা করলে Git সেটিকে মুছে ফেলা আর নতুন ফাইল ভাবতে পারে, আর --follow ধারা হারায়। জায়গার দিক থেকে প্রতিটি সংস্করণ পুরো রাখলে repository ফুলে যেত; Git তাই ভেতরে packfile-এ object-গুলোকে একে অপরের সাপেক্ষে delta হিসেবে চাপ দিয়ে রাখে — কিন্তু সেটি সংরক্ষণের কৌশল, ইতিহাসের মডেল নয় (পর্ব ৫২)।

Diff-এর বিন্যাসেরও একটি ইতিহাস আছে। Unix-এর diff program-এর মূল algorithm নিয়ে Hunt আর McIlroy ১৯৭৬ সালে Bell Labs থেকে প্রতিবেদন প্রকাশ করেন। Unified format আসে পরে, ১৯৯০ সালের দিকে GNU diff-এ — সাধারণত Wayne Davison-এর অবদান বলে উল্লেখ করা হয়। Larry Wall-এর patch program (১৯৮৫) এই diff ফাইল নিয়ে অন্যের কপিতে পরিবর্তন বসাতে পারত; email-এ patch পাঠানোর সেই সংস্কৃতি Linux kernel-এ আজও চলে।

মূল উপলব্ধি: Git ইতিহাস হিসেবে রাখে অবস্থা, পরিবর্তন নয়; diff একটি দেখার পদ্ধতি, যা চাইলে বদলানো যায়। এ কারণেই একই history নানা দৃষ্টিতে দেখা সম্ভব।

প্র ০২সবচেয়ে ছোট diff কি সবসময় সবচেয়ে ভালো diff? Code review-এর মানুষের কাছে “ভালো diff” মানে কী?

Myers-এর algorithm-এর লক্ষ্য গাণিতিক: সবচেয়ে কম যোগ-বিয়োগ। কিন্তু একই সংখ্যক যোগ-বিয়োগের অনেকগুলো diff থাকতে পারে, আর তাদের কোনোটি মানুষের চোখে স্বাভাবিক, কোনোটি বিভ্রান্তিকর। ধরুন একটি C ফাইলে total() function-এর আগে একটি নতুন discount() function যোগ করলেন, আর total()-এর ভেতরে একটি লাইন বদলালেন। দুটি function-এর শরীরে প্রায় একই লাইন — return a;, }, ফাঁকা লাইন — থাকলে diff দেখাতে পারে যেন total-এর নাম বদলে discount হয়েছে আর নিচে একটি নতুন total এসেছে। যোগ-বিয়োগের সংখ্যা দুই ব্যাখ্যাতেই সমান, কিন্তু reviewer-এর কাছে প্রথমটির অর্থ সম্পূর্ণ ভুল।

এ কারণেই বিকল্প algorithm এসেছে। Patience diff (ধারণাটি Bram Cohen-এর বলে পরিচিত, যিনি BitTorrent-এর স্রষ্টা হিসেবেও খ্যাত) আগে দুই ফাইলে ঠিক একবার আসে এমন লাইন জোড়া দেয় — সেগুলো সাধারণত অর্থবহ (function-এর নাম), আর }-এর মতো বারবার আসা লাইন পরে। Histogram একই চিন্তার দ্রুত রূপ। Git-এ আরও আছে --indent-heuristic-এর মতো কৌশল, যা hunk-এর সীমা এমনভাবে সরায় যাতে একটি পুরো block একসাথে দেখায় — নতুন version-এ এটি ডিফল্ট। কোনো algorithm সব ক্ষেত্রে সেরা নয়।

তবে algorithm-এর চেয়ে বড় ভূমিকা আপনার। Review-যোগ্য diff বানানোর কয়েকটি চর্চা: formatting-এর পরিবর্তন (indentation, space) আর logic-এর পরিবর্তন আলাদা commit-এ রাখা, যাতে logic-এর diff ছোট আর পরিষ্কার থাকে; ফাইলের নাম বদল আলাদা commit-এ, যাতে rename detection কাজ করে; এক commit-এ এক উদ্দেশ্য। Reviewer-এর দিক থেকে: -w দিয়ে whitespace বাদ দেওয়া, --word-diff দিয়ে লেখার পরিবর্তন দেখা, আর সন্দেহ হলে diff-এর বাইরে পুরো ফাইল খোলা। Code review-এর পূর্ণ আলোচনা পর্ব ৪৪-এ, আর ভালো commit-এর নিয়ম পর্ব ১৮-এ।

মূল উপলব্ধি: “সবচেয়ে ছোট” একটি গাণিতিক মাপ, “সবচেয়ে বোধগম্য” একটি মানবিক মাপ। Algorithm প্রথমটি দেয়; দ্বিতীয়টি আসে ভালো algorithm বেছে নেওয়া আর পরিবর্তনগুলো সুশৃঙ্খলভাবে ভাগ করা থেকে।

প্র ০৩git log-এ লেখা তারিখ আর লেখকের নাম কতটা বিশ্বাসযোগ্য? History-কে কি প্রমাণ হিসেবে ধরা যায়?

এই পর্বের সব commit-এর তারিখ ১৫ থেকে ২২ জানুয়ারি ২০২৬, অথচ commit-গুলো আসলে বানানো হয়েছে অন্য সময়ে — শুধু দুটি environment variable (GIT_AUTHOR_DATE, GIT_COMMITTER_DATE) দিয়ে। লেখকের নামও user.name-এ যা খুশি লেখা যায়। অর্থাৎ commit-এর ভেতরের তথ্য লেখক নিজে যা দাবি করেন, তা-ই; Git নিজে তা যাচাই করে না।

প্রতিটি commit-এ দুটি সময় থাকে: author date (কাজটি প্রথম কখন লেখা হয়েছিল) আর committer date (commit-টি শেষবার কখন তৈরি হলো)। সাধারণত দুটি একই, কিন্তু rebase বা cherry-pick করলে (পর্ব ১৫, ৫৬) committer date বদলায়, author date থাকে। --since/--until committer date দেখে; git log-এর “Date:” লাইন দেখায় author date. তাই “৩ তারিখের পরের সব commit” খুঁজতে গিয়ে অবাক হওয়ার সুযোগ আছে।

তাহলে history কি মূল্যহীন? না। প্রথমত, commit-এর hash তার parent-এর hash-সহ পুরো তথ্য থেকে তৈরি, তাই একবার কেউ আপনার history নিজের কপিতে নিয়ে গেলে পরে পুরোনো commit গোপনে বদলানো যায় না — বদলালেই hash আলাদা হয়, আর সবার কপির সাথে অমিল ধরা পড়ে। দ্বিতীয়ত, কে লিখেছেন তা যাচাইয়ের জন্য আছে signed commit: লেখক নিজের গোপন চাবি দিয়ে commit-এ স্বাক্ষর দেন, আর অন্যরা তা যাচাই করতে পারেন (পর্ব ৫৭)। তৃতীয়ত, hosting platform-গুলো সাধারণত push-এর নিজস্ব লগ রাখে — কে কখন push করেছেন — যা commit-এর দাবির চেয়ে বেশি বিশ্বাসযোগ্য, যদিও তার বিস্তারিত platform-ভেদে আলাদা।

মূল উপলব্ধি: Git-এর history বদলানো থেকে সুরক্ষিত, কিন্তু প্রথমবার লেখার সময় যা দাবি করা হয়েছে তার সত্যতা যাচাই করে না। প্রমাণ চাইলে স্বাক্ষর আর বাইরের লগ লাগে।

প্র ০৪Git-এ “পেছনে যাওয়ার” জন্য ~ আর ^ দুটি আলাদা চিহ্ন কেন? প্রথম parent-এর ধারণা দলের কাজের ধরনে কী প্রভাব ফেলে?

একটি সরলরেখার history-তে একটি চিহ্নই যথেষ্ট হতো — প্রতিটি commit-এর একটিই parent. কিন্তু Git শুরু থেকেই Linux kernel-এর মতো project-এর জন্য বানানো, যেখানে শত শত মানুষের আলাদা ধারা নিয়মিত merge হয়। Merge commit-এর একাধিক parent থাকে — এমনকি দুইয়ের বেশিও (“octopus merge”, যা kernel-এর history-তে মাঝে মাঝে দেখা যায়)। তখন দুটি আলাদা প্রশ্ন ওঠে: “কোন parent?” আর “কত প্রজন্ম পেছনে?” ^n প্রথম প্রশ্নের উত্তর, ~n দ্বিতীয়টির — সবসময় প্রথম parent ধরে। দুটো জুড়ে যেকোনো commit-এ পৌঁছানো যায়: HEAD~3^2~1.

প্রথম parent-এর বিশেষ মর্যাদা একটি প্রথা থেকে আসে: git merge feature চালালে আপনি যে branch-এ বসে আছেন তার আগের commit প্রথম parent হয়। তাই একটি দল যদি সবসময় main-এ বসে feature merge করে, main-এর প্রথম parent-এর ধারা হয়ে ওঠে “main-এর নিজের গল্প” — প্রতিটি feature একটি merge commit. git log --first-parent তখন একটি পরিষ্কার সারাংশ দেয়, আর git bisect --first-parent (পর্ব ৫৫) কোন feature merge-এ bug ঢুকেছে তা খুঁজতে পারে, প্রতিটি ছোট commit পরীক্ষা না করেই।

কিন্তু এই সুবিধা ভাঙে যখন কেউ উল্টো দিকে merge করেন — যেমন feature branch-এ বসে main-কে merge করে তারপর সেটিকে main-এ fast-forward করে দেওয়া। তখন main-এর প্রথম parent-এর ধারা feature-এর ভেতর দিয়ে চলে যায়, আর “main-এর গল্প” এলোমেলো হয়। অনেক দল তাই pull request-এর মাধ্যমে নিয়মিত পথে merge করে (পর্ব ১৬), অথবা rebase করে history সোজা রাখে (পর্ব ১৫), যেখানে merge commit-ই থাকে না। কোনটি ভালো — দীর্ঘ বিতর্কের বিষয়; পর্ব ১৭-এ branching strategy-তে আবার আসবে।

মূল উপলব্ধি: ~ আর ^ একটি DAG-এ ঠিকানা লেখার দুটি মাত্রা — প্রজন্ম আর শাখা। আর “প্রথম parent” একটি প্রথা, যা দল নিয়ম মেনে চললেই অর্থবহ থাকে।

প্র ০৫git shortlog -sn দিয়ে কি developer-দের কাজের মূল্যায়ন করা উচিত? Commit-সংখ্যা আসলে কী মাপে?

সংখ্যাটি দেখতে লোভনীয়: এক command-এ প্রত্যেকের commit গোনা হয়ে যায়। কিন্তু এটি কী মাপে, তা ভাবুন। একজন দিনে বিশটি ছোট commit করেন (“typo ঠিক”, “আবার typo”), আরেকজন এক সপ্তাহ ধরে একটি কঠিন concurrency bug খুঁজে এক লাইনের পরিবর্তনের একটি commit দেন। দ্বিতীয়জনের কাজ হয়তো অনেক বেশি মূল্যবান। লাইনের সংখ্যাও (--stat) একই ফাঁদে পড়ে: কোড মুছে সরল করা প্রায়ই সবচেয়ে ভালো কাজ, অথচ তা “নেতিবাচক” দেখায়।

তার ওপর সংখ্যাটি প্রায়ই প্রযুক্তিগতভাবেও ভুল। এই পর্বেই দেখেছেন, এক মানুষ দুই নামে ভাগ হয়ে যান, যতক্ষণ না .mailmap ঠিক করা হয়। Squash merge করলে (অনেক platform-এ pull request-এর একটি বিকল্প) একাধিক মানুষের কাজ একজনের নামে একটি commit হয়ে যেতে পারে। Pair programming-এ দুজনের কাজ একজনের নামে যায়; অনেক দল তাই বার্তার শেষে Co-authored-by: লাইন যোগ করে, যা কিছু platform চিনে নেয়, কিন্তু git shortlog ডিফল্টভাবে গোনে না। আর review, নকশা, documentation, নতুনদের সাহায্য — দলের অনেক জরুরি কাজ commit-ই তৈরি করে না।

সবচেয়ে বড় বিপদ আচরণগত। পর্ব ৬-এ দেখেছেন Goodhart-এর সূত্র: কোনো মাপ লক্ষ্য হয়ে গেলে সেটি আর ভালো মাপ থাকে না। Commit-সংখ্যা দিয়ে মূল্যায়ন শুরু হলে মানুষ কাজ কৃত্রিমভাবে ভেঙে অনেক commit বানাবেন, আর history-র মান কমবে। DORA গবেষণা (পর্ব ৫৯) ব্যক্তির নয়, দলের delivery-র মাপ দেখতে বলে।

তাহলে shortlog-এর ভালো ব্যবহার কী? Release-এর সময় অবদানকারীদের ধন্যবাদের তালিকা, open-source project-এ কারা সক্রিয় তার মোটা ধারণা, বা একটি পুরোনো module সম্পর্কে কাকে জিজ্ঞেস করা যায় তা খোঁজা (git shortlog -sn -- path).

মূল উপলব্ধি: Commit-সংখ্যা কার্যকলাপের একটি মোটা চিহ্ন, মূল্যের মাপ নয়। একে প্রশ্ন তোলার জন্য ব্যবহার করুন, মানুষকে বিচারের জন্য নয়।

অনুশীলন

আউটপুট অনুমান, command লেখা আর একটি অভ্যাসের সমালোচনা — প্রতিটির পূর্ণ উত্তর দেওয়া আছে, আর প্রতিটি উত্তর Git ২.৫৩-এ চালিয়ে যাচাই করা।

  1. Hunk header অনুমান করুন: menu.txt-এর ১০টি লাইন: tea 10, coffee 30, singara 10, samosa 10, puri 5, piyaju 5, chop 15, cake 20, biscuit 10, juice 40. নতুন রূপে coffee 30 মুছে ফেলা হলো, biscuit 10 হলো biscuit 15, আর শেষে lassi 50 যোগ হলো। (ক) ডিফল্ট ৩টি context লাইনে কয়টি hunk হবে, আর header কী? (খ) -U1 (১টি context লাইন) দিলে?

    (ক) একটি hunk: @@ -1,10 +1,10 @@. প্রথম পরিবর্তন লাইন ২-এ, পরেরটি লাইন ৯-এ; মাঝে অপরিবর্তিত লাইন ৩–৮, অর্থাৎ ৬টি — context-এর দ্বিগুণ (৬)-এর বেশি নয়, তাই দুটি মিশে এক hunk. শুরু লাইন ১ (লাইন ২-এর আগে মাত্র ১টি লাইন আছে)। পুরোনো দিকে লাইন ১–১০ সব, ১০টি; নতুন দিকে ১০ − ১ (coffee) + ১ (lassi) = ১০টি।

    (খ) দুটি hunk: এবার ফাঁক ৬ > ২, তাই আলাদা।

    $ git diff --no-index -U1 old.txt new.txt
    diff --git a/old.txt b/new.txt
    index e8f9491..7fa924a 100644
    --- a/old.txt
    +++ b/new.txt
    @@ -1,3 +1,2 @@
     tea 10
    -coffee 30
     singara 10
    @@ -8,3 +7,4 @@ chop 15
     cake 20
    -biscuit 10
    +biscuit 15
     juice 40
    +lassi 50

    দ্বিতীয় hunk-এ পুরোনো দিকে লাইন ৮–১০ (৩টি: cake, biscuit, juice), নতুন দিকে শুরু লাইন ৭, কারণ coffee মুছে যাওয়ায় সবকিছু এক লাইন ওপরে উঠেছে; নতুন দিকে ৪টি (cake, biscuit 15, juice, lassi). chop 15 শুধু ঠিকানা-চিহ্ন। নিজে চালাতে: দুটি ফাইল বানিয়ে git diff --no-index -U1 old.txt new.txt.

  2. Command-এর ধারা থেকে status অনুমান করুন: একটি পরিষ্কার repository-তে তিনটি tracked ফাইল a.txt, b.txt, d.txt. তারপর এই ধারায়: echo x >> a.txt; git add a.txt; echo y >> a.txt; echo z >> b.txt; echo new > c.txt; git add c.txt; echo more >> c.txt; rm d.txt; echo hi > e.txt. (ক) git status -s কী দেখাবে? (খ) git diff --staged --name-only? (গ) এখন git commit -m "wip" চালালে কোন ফাইলের কোন রূপ যাবে?
    $ git status -s
    MM a.txt
     M b.txt
    AM c.txt
     D d.txt
    ?? e.txt
    $ git diff --staged --name-only
    a.txt
    c.txt

    (ক) a.txt: add-এর পরে আবার বদলানো — MM. b.txt: বদলানো, add করা হয়নি — ␣M. c.txt: নতুন, add করা, তারপর বদলানো — AM. d.txt: শুধু folder থেকে মোছা, git rm নয় — ␣D. e.txt: untracked — ??. Git ফাইলগুলো নাম অনুযায়ী সাজিয়ে দেখায়।

    (খ) শুধু a.txt আর c.txt — যাদের X কলাম ফাঁকা নয়।

    (গ) a.txt-এ x যোগ করা রূপ (কিন্তু y নয়), আর c.txt-এর শুধু new লাইন। b.txt-এর পরিবর্তন, d.txt-এর মোছা আর e.txt — কিছুই যাবে না। মোছাও commit-এ নিতে চাইলে git rm d.txt বা git add d.txt.

  3. Command লিখুন: এই পর্বের bazar repository-তে (HEAD = merge commit 8ef86df) প্রতিটি কাজের জন্য একটি command লিখুন: (ক) Nasrin-এর যেসব commit bill.py ছুঁয়েছে, এক লাইনে; (খ) commit-বার্তায় “price” শব্দ আছে এমন commit, বড়-ছোট হাত না মেনে; (গ) তিন commit আগের (প্রথম parent ধরে) bill.py working tree না বদলে দেখা; (ঘ) পরের commit-এ যাবে এমন ফাইলগুলোর শুধু নাম; (ঙ) merge commit-এর দ্বিতীয় parent-এর ছোট hash.
    $ git log --oneline --author=Nasrin -- bill.py
    4663630 Apply eid discount to the total
    7830cb6 Add eid discount function
    $ git log --oneline -i --grep=PRICE
    dbc8f82 Update onion price
    058bc63 Fix price names and round the total
    38690bb Add price list and README
    $ git show HEAD~3:bill.py
    # bill.py - grocery bill for a corner shop
    PRICES = {}
    
    …
    $ git diff --staged --name-only
    bill.py
    docs/meeting-notes.txt
    prices.txt
    test_bill.py
    $ git rev-parse --short HEAD^2
    4663630

    (ক)-তে --author আর -- bill.py একসাথে দুটো শর্তই মানে। (খ)-তে -i ছাড়া PRICE কিছুই পেত না; তিনটি commit এসেছে, কারণ price শব্দাংশ “prices”-এর ভেতরেও আছে। (গ)-তে HEAD~3 = 058bc63; আউটপুট লম্বা বলে প্রথম তিন লাইন দেখানো। (ঘ)-তে উত্তর অংশ ২-এর অবস্থার ওপর চালানো — নাম বদলানো ফাইল নতুন নামে আসে। (ঙ)-এর বিকল্প: git log -1 --format=%p HEAD, যা দুটি parent-ই দেখায়।

  4. অভ্যাসের সমালোচনা: একজন নতুন সহকর্মীর commit করার নিয়ম: কাজ শেষে git add ., তারপর git diff চালিয়ে কিছু না দেখে ভাবেন “সব ঠিক”, তারপর git commit -m "update". তাঁর একটি commit-এ ৪০টি ফাইল — পুরো project-এর indentation বদল আর একটি payment-এর হিসাবের পরিবর্তন একসাথে। History দেখেন শুধু git log --oneline দিয়ে। কোথায় কোথায় ঝুঁকি, আর কীভাবে বদলাবেন?
    • ভুল command-এ যাচাই: git add .-এর পর সব পরিবর্তন index-এ, তাই git diff (working tree বনাম index) খালি — এটি “কোনো পরিবর্তন নেই”-এর প্রমাণ নয়। দেখা উচিত git diff --staged. আর git add .-এ অচেনা ফাইল (log, .env, build-এর ফল) ঢুকে যেতে পারে — আগে git status, প্রয়োজনে .gitignore (পর্ব ১০)।
    • দুই উদ্দেশ্য এক commit-এ: Indentation-এর হাজার লাইনের diff-এর ভিড়ে payment-এর এক লাইনের পরিবর্তন reviewer-এর চোখ এড়িয়ে যাবে — যে অংশে ভুল সবচেয়ে ব্যয়বহুল, ঠিক সেটিই লুকানো। সমাধান: দুটি commit — আগে শুধু formatting (বার্তায় সেটি বলা), পরে logic. Review-এ git diff -w বা git show -w দিয়ে whitespace বাদ দিয়ে দেখা যায়।
    • অর্থহীন বার্তা: “update” ছয় মাস পরে কিছুই বলবে না, আর git log --grep দিয়ে খোঁজা অসম্ভব হবে। বার্তায় কী আর কেন বদলেছে — নিয়ম পর্ব ১৮-এ।
    • History-র অর্ধেক দেখা: শুধু --oneline-এ কোন commit কী ছুঁয়েছে বোঝা যায় না। --stat, --graph --all আর প্রয়োজনে -p -- path অভ্যাস করা।
    • প্রস্তাবিত নিয়ম: git status → git diff (বাকি কী আছে) → বেছে বেছে git add <ফাইল> → git diff --staged --stat → git diff --staged পড়া → test চালানো → অর্থবহ বার্তায় commit.

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

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

ক্রমGit-এর একটি ফাইলের unified diff-এ লাইনগুলো যে ক্রমে আসে, সেভাবে সাজান (▲▼ চেপে সরান)

  1. diff --git a/bill.py b/bill.py — কোন ফাইলের তুলনা
  2. index 187744d..22b830e 100644 — দুই রূপের hash আর mode
  3. --- a/bill.py — পুরোনো দিক
  4. +++ b/bill.py — নতুন দিক
  5. @@ -5,7 +5,7 @@ — hunk header
  6. Context, - আর + লাইন

একটি repository-তে git status -s চালালে একটি লাইন আসে MM bill.py. এর সঠিক অর্থ কোনটি?

  • bill.py দুটি commit-এ বদলানো হয়েছে
  • bill.py-তে merge conflict আছে
  • কিছু পরিবর্তন stage করা হয়েছে, তার পরে ফাইলটি আবার বদলানো হয়েছে, যা stage করা নয়
  • bill.py নতুন ফাইল, এইমাত্র add করা হয়েছে

একজন developer কয়েকটি ফাইল বদলে সবগুলো git add করেছেন, এখনো commit করেননি। পরের commit-এ ঠিক কী যাবে, লাইন ধরে দেখতে কোন command?

  • git diff
  • git diff --staged
  • git log -p
  • git show HEAD

একটি diff-এ hunk header @@ -12,7 +12,9 @@. এ থেকে কোনটি নিশ্চিতভাবে বলা যায়?

  • পুরোনো ফাইলের লাইন ১২ থেকে ৭টি লাইন আর নতুন ফাইলের লাইন ১২ থেকে ৯টি লাইন এই hunk-এ; নিট ২টি লাইন যোগ হয়েছে
  • ঠিক ৭টি লাইন বাদ আর ৯টি লাইন যোগ হয়েছে
  • পরিবর্তন লাইন ১২ থেকে লাইন ৭ পর্যন্ত
  • ঠিক ২টি লাইন যোগ হয়েছে আর কোনো লাইন বাদ যায়নি

একটি merge commit M-এর প্রথম parent P1 আর দ্বিতীয় parent P2. P1-এর parent Q1, আর P2-এর parent Q2; P1 ও P2 দুটিই সাধারণ (এক parent-এর) commit. HEAD = M হলে HEAD^2~1 কোন commit?

  • Q1
  • P2
  • P1
  • Q2

notes.txt ফাইলটি একটি commit-এ docs/notes.txt-এ সরানো হয়েছিল। এখন git log --oneline -- docs/notes.txt শুধু সেই সরানোর commit-টি দেখায়। নাম বদলের আগের history-ও দেখতে কোনটি যোগ করবেন?

  • --all
  • --graph
  • --follow
  • --stat

দুটি branch main আর feature একটি সাধারণ commit থেকে আলাদা হয়েছে, আর দুটিতেই নতুন commit আছে। কোন বক্তব্যটি সঠিক?

  • git diff main..feature merge base থেকে feature পর্যন্ত পরিবর্তন দেখায়
  • git diff main..feature আর git diff main feature একই, কিন্তু git log main..feature দেখায় শুধু feature-এ থাকা commit
  • git log main..feature দুই branch-এর সব নতুন commit দেখায়
  • git diff main...feature আর git diff main feature সবসময় একই ফল দেয়

একটি দলের git shortlog -sn-এ একই মানুষ Rahim আর rahim দুই নামে আলাদা সারিতে আসছেন, কারণ দুটি কম্পিউটারে নাম আর email আলাদা ছিল। পুরোনো commit না বদলে হিসাব ঠিক করার প্রচলিত উপায় কোনটি?

  • Repository-র মূল folder-এ একটি .mailmap ফাইল যোগ করা
  • এখন থেকে git config user.name Rahim ঠিক রাখা
  • সব পুরোনো commit নতুন করে লিখে নাম বদলে দেওয়া
  • git shortlog -sn -i চালানো

শব্দকোষ

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

মনে রাখুন

  1. status, diff, log, show — চারটিই শুধু দেখায়; ভয় ছাড়া যতবার খুশি চালান।
  2. git status -s-এ বাঁ অক্ষর index (staged), ডান অক্ষর working tree (unstaged); ফাঁকা ঘরের জায়গাটাই খবর।
  3. git diff = working tree বনাম index, --staged = index বনাম HEAD, HEAD = working tree বনাম HEAD, A B = দুটি commit. Commit-এর আগে git diff --staged.
  4. @@ -l,s +l,s @@: শুরুর লাইন আর লাইনের সংখ্যা, context-সহ; hunk-এর লাইন গুনে header মেলানো যায়।
  5. ~n = প্রথম parent ধরে n ধাপ, ^n = n-তম parent; HEAD~2 ≠ HEAD^2. পুরোনো ফাইল দেখতে git show <commit>:<path>.

পরের পর্বে: আজ todo.txt “Untracked” তালিকায় ঝুলে ছিল, আর একই মানুষ দুই নামে এসেছিলেন। পর্ব ১০-এ এই দুই সমস্যার মূলে যাব: .gitignore দিয়ে কোন ফাইল কখনো track হবে না তা ঠিক করা, .gitattributes দিয়ে line ending আর diff-এর নিয়ম, আর git config-এর স্তর — নাম, email, pager আর diff algorithm-সহ।

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

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