এই পর্বে যা শিখবেন
git bisect-এর পুরো session — start, good, bad, skip, log, replay, reset, নিজের পরিভাষা আর--first-parent- Bisect-এর পেছনের গণিত: কেন ⌈log₂ n⌉ ধাপ, আর skip করা commit কখন উত্তর অস্পষ্ট করে
git bisect run-এর জন্য নির্ভরযোগ্য test script লেখা আর exit code-এর নিয়মgit blame-এর-L,-w,-M,-C,-e, porcelain আর ignore-revs — আর blame-এর ফল ঠিকভাবে পড়াgit log -S,-G,-L,--follow,git grepআরshortlogজুড়ে একটি পূর্ণ bug-তদন্ত
১ · কবে, কোথায়, কেন — একটি bug-এর তিনটি প্রশ্ন
একটি কাল্পনিক মুদি দোকানের হিসাবের program দিয়ে শুরু করি। নাম bazar: bill.py ফাইলে দামের যোগফল, ঈদের ছাড় আর রসিদ ছাপা। নিয়ম ছিল সহজ — বিল ১,০০০ টাকা বা তার বেশি হলে ৫% ছাড়। মাসখানেক পরে এক ক্রেতা অভিযোগ করলেন: ঠিক ১,০০০ টাকার বাজার করলেন, কিন্তু ছাড় পেলেন না। অথচ ছয় নম্বর commit-এ, যেটিতে v1.0 tag লাগানো, সব ঠিক ছিল — তখন test করে দেখা হয়েছিল।
যে আচরণ আগে ঠিক ছিল আর পরে কোনো পরিবর্তনের ফলে ভেঙেছে, তাকে বলে regression. Regression-এর একটি বড় সুবিধা আছে: কোথাও না কোথাও একটি “ভালো” অবস্থা আর একটি “খারাপ” অবস্থা জানা, আর মাঝের প্রতিটি পরিবর্তন Git-এ লেখা। তাই প্রশ্নগুলো সাজানো যায়:
- কবে ঢুকল? কোন commit-এ প্রথমবার ভুল আচরণ শুরু হলো —
git bisect. - কোথায়, কোন লাইনে? সন্দেহের লাইনটি শেষবার কোন commit বদলেছে —
git blame; একটি লেখা কবে এল বা গেল —git log -S/-G; একটি function-এর পুরো ইতিহাস —git log -L. - কেন? সেই commit-এর বার্তা, তার সাথে যুক্ত issue আর review — যেখানে লেখা থাকে পরিবর্তনটি কী উদ্দেশ্যে করা হয়েছিল।
মুদি দোকানের হিসাবের খাতার কথা ভাবুন। মাসের শেষে নগদ টাকা আর খাতার যোগফল মিলছে না। মাসের ১ তারিখে মিলেছিল — সেটি জানা। ৩০টি পাতা একে একে যাচাই না করে বুদ্ধিমান দোকানি ১৫ তারিখের যোগফল মেলান। মিললে ভুল ১৫ তারিখের পরে, না মিললে আগে। তারপর অর্ধেকের অর্ধেক — ৮ বা ২৩ তারিখ। পাঁচবারের মধ্যেই ভুলের দিনটি ধরা পড়ে। এটিই bisect. তারপর সেই দিনের পাতায় প্রতিটি লাইনের পাশে কার হাতের লেখা, কোন দিনের — সেটি blame. আর “চিনির দামের লাইনটি খাতায় প্রথম কবে উঠল?” — সেটি log -S.
এই পর্বের সব Git আউটপুট আসল: Git ২.৫৩ দিয়ে একটি খালি folder-এ bazar repository বানিয়ে চালানো। লেখক Rahim (rahim@example.com) আর Nasrin (nasrin@example.com), দুজনই কাল্পনিক। Commit-এর সময় ইচ্ছে করে স্থির রাখা — ১৫ জানুয়ারি ২০২৬, সকাল ১০:০০ (+06:00) থেকে শুরু করে প্রতি commit-এ এক মিনিট করে। একই ফাইল, নাম আর সময় দিলে আপনার hash-ও হুবহু এক আসবে। মোট ২০টি commit, যার একটিতে ভুলটি ইচ্ছে করে বসানো — আমরা খুঁজে বের করব।
$ git log --oneline --decorate
2cd69ab (HEAD -> main) Add a thank-you line to the receipt
b8fcb04 Describe version 1.1 in README
22367c8 Update flour price
05792ca Move format_taka next to line_total
3c65833 Add a test for line_total
1853d66 Move eid discount into discount.py
87f8469 Format code: spaces around operators, double quotes
7331e00 Add delivery charge
70c0892 Use a constant for the discount threshold (#17)
c71b112 Add sugar and flour prices
3765778 Finish receipt printing
a2aba58 WIP: start receipt printing
392acc2 Add usage notes to README
3198235 Update onion price
a122fb6 (tag: v1.0) Add format_taka helper
7bd5ea2 Apply eid discount to the total
86bd46b Add eid discount function
0a0b885 Round the total to two decimals
40f5f49 Add bill calculator
90e700e Add price list and READMEলেখার সুবিধার জন্য commit-গুলোকে নিচ থেকে নম্বর দেব: 90e700e = commit ১, a122fb6 (v1.0) = commit ৬, আর HEAD 2cd69ab = commit ২০। বার্তার (#17) একটি issue-এর নম্বর — commit-এর নম্বর নয়; issue tracker-এ ১৭ নম্বর কাজের অনুরোধ।
Bug ধরার প্রথম কাজ হলো ভুলটি একটি স্বয়ংক্রিয় যাচাই-এ রূপ দেওয়া — যাতে যেকোনো commit-এ চালিয়ে হ্যাঁ/না উত্তর পাওয়া যায়। আমাদের যাচাই check.py, যা repository-র বাইরে (এক ধাপ ওপরের folder-এ) রাখা, যাতে পুরোনো commit-এ গেলেও ফাইলটি না হারায়:
import sys
sys.path.insert(0, ".")
try:
import bill
except SyntaxError:
print("cannot import bill.py - skip")
sys.exit(125)
bill.load_prices("prices.txt")
got = bill.final_total([("oil", 5)]) # 5 x 200 = 1000 taka
print("total for 1000 taka:", got)
sys.exit(0 if got == 950.0 else 1)ঠিক ১,০০০ টাকার বাজার (৫ লিটার তেল, প্রতি লিটার ২০০ টাকা) দিলে ছাড়ের পর ৯৫০ হওয়ার কথা। মিললে exit code 0, না মিললে 1; আর bill.py যদি এমন অবস্থায় থাকে যে import-ই করা যায় না, তাহলে 125 — এর মানে অংশ ৪-এ বুঝব। দুই প্রান্তে চালিয়ে দেখি:
$ python ../check.py
total for 1000 taka: 1000.0
$ echo $?
1
$ git switch --detach v1.0
HEAD is now at a122fb6 Add format_taka helper
$ python ../check.py
total for 1000 taka: 950.0
$ git switch main
Previous HEAD position was a122fb6 Add format_taka helper
Switched to branch 'main'HEAD-এ ভুল (১,০০০.০), v1.0-তে ঠিক (৯৫০.০)। মাঝে ১৪টি commit — যেকোনোটি দোষী হতে পারে। (Linux বা macOS-এ python-এর বদলে সাধারণত python3 লিখতে হয়।)
২ · Bisect-এর গণিত — history-র ওপর binary search
সাজানো তালিকায় একটি সংখ্যা খোঁজার binary search আপনি হয়তো DSA-তে দেখেছেন (Linear আর binary search): প্রতিবার মাঝখানের উপাদান দেখে অর্ধেক তালিকা বাদ। Bisect একই কৌশল commit-এর সারির ওপর চালায়। শর্ত একটিই — একমুখিতা (monotonicity): কোনো commit খারাপ হলে তার পরের সব commit-ও খারাপ, আর ভালো হলে তার আগের সবও ভালো। তখন সারিটি দেখতে এরকম: ভালো, ভালো, …, ভালো, খারাপ, খারাপ, …, খারাপ — আর আমরা খুঁজছি প্রথম খারাপটি।
১৬টি সন্দেহভাজন commit-এ bisect — প্রতি ধাপে পরিসর অর্ধেক
Known good (#0) and known bad (#16); each test halves the suspect range
কত ধাপ লাগবে — ⌈log₂ n⌉
ধরুন n টি commit সন্দেহভাজন — অর্থাৎ প্রথম খারাপ commit এই n টির যেকোনোটি হতে পারে (জানা খারাপ প্রান্তটিও এর মধ্যে, কারণ হয়তো সেটিই প্রথম খারাপ)। প্রতিটি পরীক্ষা দুটি উত্তরের একটি দেয়, তাই k টি পরীক্ষায় সর্বোচ্চ 2kটি আলাদা সম্ভাবনা আলাদা করা যায়। সব n সম্ভাবনা আলাদা করতে লাগে 2k ≥ n, অর্থাৎ k = ⌈log₂ n⌉. এখানে ⌈ ⌉ মানে “ওপরের দিকের পূর্ণসংখ্যা”।
| সন্দেহভাজন commit (n) | ⌈log₂ n⌉ পরীক্ষা | একে একে দেখলে সবচেয়ে খারাপ ক্ষেত্রে | Git ২.৫৩-এর প্রথম বার্তা |
|---|---|---|---|
| ১৪ (আমাদের bazar) | ৪ | ১৩ | 6 revisions left … (roughly 3 steps) |
| ৫০০ | ৯ (২⁹ = ৫১২) | ৪৯৯ | 249 revisions left … (roughly 8 steps) |
| ১,০০০ | ১০ (২¹⁰ = ১,০২৪) | ৯৯৯ | 499 revisions left … (roughly 9 steps) |
| ৩,০০০ | ১২ (২¹² = ৪,০৯৬) | ২,৯৯৯ | 1499 revisions left … (roughly 11 steps) |
| ১০ লাখের মতো | ২০ (২²⁰ = ১০,৪৮,৫৭৬) | প্রায় ১০ লাখ | — |
শেষ কলামটি একটি সরল সারির repository-তে আসলেই চালিয়ে দেখা। Git-এর “roughly N steps” মানে এই checkout-টির পরীক্ষার পরে আরও কত ধাপ — তাই ১ যোগ করলে আমাদের হিসাবের সাথে মেলে: ৩ + ১ = ৪, ৮ + ১ = ৯, ৯ + ১ = ১০, ১১ + ১ = ১২। একে একে দেখতে যেখানে হাজার ধাপ, bisect-এ সেখানে দশ — একে একে দেখার খরচ বাড়ে n-এর সমান হারে, bisect-এর বাড়ে শুধু log₂ n হারে।
Bisect কাজ করে কারণ history একটি ক্রম, আর bug একবার ঢুকলে থেকে যায়। এই অনুমান ভাঙলে — bug কখনো আসে কখনো যায় (flaky), বা দুটি আলাদা bug একই লক্ষণ দেখায় — bisect একটি ভুল commit-কে আত্মবিশ্বাসের সাথে দোষী দেখাতে পারে।
Merge থাকলে history আর সরল সারি থাকে না, একটি commit graph হয়ে যায়। তখন Git সহজ “মাঝখান” বেছে নেয় না; সে এমন commit খোঁজে যার পূর্বপুরুষ (ancestor) সন্দেহভাজনের সংখ্যা মোটের অর্ধেকের যত কাছে সম্ভব — যাতে উত্তর যা-ই হোক, প্রায় অর্ধেক বাদ যায়। নিচের simulator-এ সরল সারির ১,০০০টি commit, যার কোথাও প্রথম খারাপ commit লুকানো (seed দেওয়া PRNG দিয়ে, তাই প্রতিবার একই ফল), আর প্রায় ৫% commit এমন যে build-ই হয় না — সেগুলো skip করতে হয়:
// Bisect as binary search: commit 0 is known good, commit 999 is known bad.
function mulberry32(a) { return function () { a |= 0; 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 N = 1000;
function bisect(seed) {
const rand = mulberry32(seed);
const firstBad = 1 + Math.floor(rand() * (N - 1)); // commits >= firstBad are bad
const broken = new Set(); // about 5% cannot be built
for (let c = 1; c < N - 1; c++) if (rand() < 0.05) broken.add(c);
let good = 0, bad = N - 1, runs = 0, skips = 0; const skipped = new Set();
while (bad - good > 1) {
const mid = Math.floor((good + bad) / 2);
let pick = -1; // nearest commit not yet skipped
for (let d = 0; pick < 0 && d < bad - good; d++)
for (const c of [mid + d, mid - d])
if (pick < 0 && c > good && c < bad && !skipped.has(c)) pick = c;
if (pick < 0) return { seed, firstBad, result: "only skipped commits left", runs, skips };
runs++;
if (broken.has(pick)) { skipped.add(pick); skips++; continue; } // exit 125
if (pick >= firstBad) bad = pick; else good = pick;
}
return { seed, firstBad, result: bad === firstBad ? "found " + bad : "WRONG " + bad, runs, skips };
}
console.log("candidates =", N - 1, "| ceil(log2) =", Math.ceil(Math.log2(N - 1)));
for (const seed of [55, 24, 10]) {
const r = bisect(seed);
console.log(`seed ${String(seed).padEnd(3)} first bad ${r.firstBad} -> ${r.result}, runs ${r.runs}, skips ${r.skips}`);
}
let found = 0, unsure = 0, runs = 0, maxVerdicts = 0;
for (let seed = 1; seed <= 1000; seed++) {
const r = bisect(seed); if (r.result.startsWith("found")) found++; else unsure++;
runs += r.runs; maxVerdicts = Math.max(maxVerdicts, r.runs - r.skips);
}
console.log(`1000 sessions: found ${found}, only-skipped ${unsure}, mean runs ${(runs / 1000).toFixed(2)}, max verdicts ${maxVerdicts}`);আউটপুট:
candidates = 999 | ceil(log2) = 10
seed 55 first bad 446 -> found 446, runs 10, skips 0
seed 24 first bad 332 -> found 332, runs 12, skips 2
seed 10 first bad 502 -> only skipped commits left, runs 10, skips 1
1000 sessions: found 903, only-skipped 97, mean runs 10.40, max verdicts 10
Commit ০ জানা ভালো আর ৯৯৯ জানা খারাপ, তাই সন্দেহভাজন ৯৯৯টি, আর ⌈log₂ 999⌉ = ১০। Seed 55: কোনো ভাঙা commit পথে পড়েনি — ঠিক ১০টি পরীক্ষায় ৪৪৬ পাওয়া গেছে। Seed 24: দুবার মাঝের commit build হয়নি; পাশের commit বেছে নিতে হয়েছে, তাই মোট ১২বার চালানো, কিন্তু ভালো/খারাপ রায় তখনও ১০টি। Seed 10: শেষে দুই সীমার মাঝে শুধু একটি skip করা commit বাকি — সেটিই প্রথম খারাপ, নাকি তার পরেরটি, বলা অসম্ভব। ১,০০০ বারের হিসাবে এমন অস্পষ্ট ফল ৯৭ বার, প্রায় ১০%: প্রথম খারাপ commit নিজে, বা ঠিক তার আগেরটি ভাঙা হলে (প্রতিটির সম্ভাবনা প্রায় ৫%) উত্তর নিশ্চিত হয় না। গড়ে ১০.৪০ বার চালাতে হয়েছে, আর কোনো session-এ রায় ১০-এর বেশি লাগেনি — skip খরচ বাড়ায়, কিন্তু binary search-এর ভিত্তি ভাঙে না। Git নিজে skip করা commit-এর পাশের commit বাছে কিছুটা ভিন্ন, ছদ্ম-এলোমেলো নিয়মে; এখানে সরলতার জন্য সবচেয়ে কাছেরটি নেওয়া হয়েছে।
৩ · হাতে-কলমে bisect — start, bad, good, skip আর reset
একটি bisect session-এর চারটি অংশ: শুরু (git bisect start), দুই প্রান্ত জানানো (bad আর good), প্রতিটি checkout-এ পরীক্ষা আর রায় (good, bad বা skip), আর শেষে ফেরা (git bisect reset). প্রতিটি রায়ের পর Git নিজেই পরের commit checkout করে — আপনি তখন detached HEAD অবস্থায় থাকেন, কোনো branch-এ নয়। পুরো session, ঠিক যেভাবে চলেছিল:
$ git bisect start
status: waiting for both good and bad commits
$ git bisect bad
status: waiting for good commit(s), bad commit known
$ git bisect good v1.0
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[7331e0026c8ab498791f4f3f8c62dfd007b1da66] Add delivery charge
$ python ../check.py
total for 1000 taka: 1000.0
$ git bisect bad
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[a2aba5872252155ccead4e4e1a8e6301a597184d] WIP: start receipt printing
$ python ../check.py
cannot import bill.py - skip
$ git bisect skip
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[376577801e539bcc8b96af38db80a06871369509] Finish receipt printing
$ python ../check.py
total for 1000 taka: 950.0
$ git bisect good
Bisecting: 0 revisions left to test after this (roughly 1 step)
[70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6] Use a constant for the discount threshold (#17)
$ python ../check.py
total for 1000 taka: 1000.0
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[c71b11297237a8405fd2077019e1cdef10312046] Add sugar and flour prices
$ python ../check.py
total for 1000 taka: 950.0
$ git bisect good
70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6 is the first bad commit
commit 70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6
Author: Rahim <rahim@example.com>
Date: Thu Jan 15 10:11:00 2026 +0600
Use a constant for the discount threshold (#17)
Issue #17: keep the eid discount threshold in one place
so that it can be changed before each Eid.
bill.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)git bisect bad-এ commit না বললে বর্তমান HEAD ধরা হয়; git bisect good v1.0-এ একটি revision-এর নাম। দুটি একসাথেও দেওয়া যায়: git bisect start HEAD v1.0 (আগে খারাপ, পরে ভালো)। নিচের stepper-এ একই session ধাপে ধাপে — বাঁয়ে v1.0..HEAD-এর commit-গুলো (বাঁয়ের সংখ্যা commit-এর নম্বর), ডানে পরিসর কীভাবে ছোট হয়:
202cd69ab thank-you line (HEAD, bad)19b8fcb04 README v1.11822367c8 flour price1705792ca move format_taka163c65833 test line_total151853d66 move to discount.py1487f8469 format code137331e00 delivery charge1270c0892 constant (#17)11c71b112 sugar, flour103765778 finish receipt9a2aba58 WIP receipt8392acc2 README usage73198235 onion price6a122fb6 format_taka (v1.0, good)
| ভালো সীমা | |
| খারাপ সীমা | |
| সন্দেহভাজন | |
| পরীক্ষার ফল | |
| পরের checkout |
- শুরু: HEAD (২০) খারাপ, v1.0 (৬) ভালো। প্রথম খারাপ commit ৭ থেকে ২০-এর মধ্যে, ১৪টি সম্ভাবনা, তাই ⌈log₂ 14⌉ = ৪টি পরীক্ষা লাগার কথা। Git মাঝামাঝি commit ১৩ (
7331e00) checkout করে বলে “6 revisions left … roughly 3 steps” — এই পরীক্ষার পরে আরও প্রায় ৩টি। - Commit ১৩ খারাপ (ফল 1000.0):
git bisect bad. এখন ১৪ থেকে ২০ বাদ — ওগুলো ১৩-র পরে, তাই খারাপ হবেই। সন্দেহ ৭–১৩, ৭টি। Git পরের মাঝের commit ৯ (a2aba58) দেয়। - Commit ৯ পরীক্ষাই করা যায় না: এটি একটি অসম্পূর্ণ “WIP” commit,
bill.py-তে syntax error, তাই import ব্যর্থ। একে ভালো বা খারাপ বললে মিথ্যা বলা হবে —git bisect skip. পরিসর বদলায় না, শুধু ৯ বাদ দিয়ে Git পাশের commit ১০ বেছে নেয়। - Commit ১০ ভালো (950.0):
git bisect good. তাহলে ১০ আর তার আগের সব (৯-সহ) ভালো ধরা হয় — skip করা ৯-এর প্রশ্ন আর ওঠে না। সন্দেহ ১১, ১২, ১৩। Git দেয় ১২। - Commit ১২ খারাপ (1000.0): সন্দেহ এখন ১১ আর ১২। “0 revisions left … roughly 0 steps” — একটি শেষ পরীক্ষা বাকি: commit ১১।
- Commit ১১ ভালো (950.0): তাই প্রথম খারাপ commit ১২,
70c0892— “Use a constant for the discount threshold (#17)”. মোট ৫বার চালানো: ৪টি রায় (⌈log₂ 14⌉-এর সমান) আর ১টি skip.
দোষী commit পাওয়া গেল; এখন তার diff দেখি — git show 70c0892:
$ git show 70c0892 -- bill.py
…
@@ -16,8 +16,10 @@ def bill_total(cart):
total += line_total(item, qty)
return round(total, 2)
+DISCOUNT_MIN = 1000
+
def eid_discount(amount):
- if amount >= 1000:
+ if amount > DISCOUNT_MIN:
return amount*0.95
return amount
একটি চিহ্নের ভুল: >= হয়ে গেছে >. Issue #17 চেয়েছিল শুধু সংখ্যাটি একটি নামে (constant) রাখতে — একটি নিরীহ refactoring. কিন্তু লেখার সময় তুলনার চিহ্নটি বদলে গেছে, আর ঠিক ১,০০০ টাকার সীমানার ক্ষেত্রটি আর ছাড় পায় না। এটি একটি ক্লাসিক off-by-one error — আর ঠিক এ কারণেই boundary value-এর test (পর্ব ৪৭, Test case নকশা) এত জরুরি।
শেষে ফেরা, আর session-এর খাতা: reset, log, replay
খোঁজ শেষ হলেও আপনি তখনও detached HEAD-এ, আর Git bisect-এর অবস্থা .git-এর ভেতরে রেখে দিয়েছে। git bisect reset সেই অবস্থা মুছে আপনাকে যে branch থেকে শুরু করেছিলেন সেখানে ফেরায় — git bisect reset <commit> দিলে সেই commit-এ। Reset-এর আগে খাতাটি রেখে দেওয়া ভালো অভ্যাস:
$ git bisect log
git bisect start
# status: waiting for both good and bad commits
# bad: [2cd69ab26193187ec54dd3d945bf624c8ef132b7] Add a thank-you line to the receipt
git bisect bad 2cd69ab26193187ec54dd3d945bf624c8ef132b7
# status: waiting for good commit(s), bad commit known
# good: [a122fb65eb5929fb827238c12eb660f083eb7e4f] Add format_taka helper
git bisect good a122fb65eb5929fb827238c12eb660f083eb7e4f
# bad: [7331e0026c8ab498791f4f3f8c62dfd007b1da66] Add delivery charge
git bisect bad 7331e0026c8ab498791f4f3f8c62dfd007b1da66
# skip: [a2aba5872252155ccead4e4e1a8e6301a597184d] WIP: start receipt printing
git bisect skip a2aba5872252155ccead4e4e1a8e6301a597184d
# good: [376577801e539bcc8b96af38db80a06871369509] Finish receipt printing
git bisect good 376577801e539bcc8b96af38db80a06871369509
# bad: [70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6] Use a constant for the discount threshold (#17)
git bisect bad 70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6
…
$ git bisect log > ../bisect.log
$ git bisect reset
Previous HEAD position was c71b112 Add sugar and flour prices
Switched to branch 'main'খাতাটি আসলে একটি shell script: # দিয়ে শুরু লাইন মন্তব্য, বাকিগুলো command, আর প্রতিটি রায় পুরো ৪০ অক্ষরের hash দিয়ে লেখা। git bisect replay <file> সেটি আবার চালিয়ে ঠিক একই অবস্থায় ফেরায়। দুটি বড় কাজে লাগে: (১) কোনো ধাপে ভুল রায় দিলে — খাতা বের করে ভুল লাইনটি মুছে, reset করে, replay; অনুশীলন ৩-এ পুরো ঘটনা আছে। (২) সহকর্মী বা bug report-এ খাতাটি পাঠালে অন্যজন আপনার খোঁজ যাচাই করতে বা এগিয়ে নিতে পারেন।
বাকি সন্দেহভাজন দেখা: git bisect visualize
মাঝপথে জানতে ইচ্ছে হয়, কোন commit-গুলো এখনো সন্দেহের তালিকায়? git bisect visualize (বা view) সেগুলো দেখায় — graphical পরিবেশ ধরা পড়লে (নথি অনুযায়ী DISPLAY, MSYSTEM-এর মতো environment variable দেখে) gitk খোলে, নইলে git log. Commit ১১ পরীক্ষার ঠিক আগে:
$ git bisect visualize --oneline
70c0892 Use a constant for the discount threshold (#17)
c71b112 Add sugar and flour prices“ভালো-খারাপ” ছাড়া অন্য প্রশ্ন: নিজের পরিভাষা
সব প্রশ্ন bug নিয়ে নয়। “কোন commit-এ page লোড ধীর হলো?” বা উল্টো, “কোন commit-এ bug-টি ঠিক হয়েছিল?” — দ্বিতীয় ক্ষেত্রে পুরোনো অবস্থা খারাপ আর নতুনটি ভালো, তাই good/bad শব্দ উল্টো শোনায়। Git সাধারণ শব্দ হিসেবে old/new মানে, আর --term-old/--term-new দিয়ে নিজের শব্দও দেওয়া যায় — এগুলোকে বলে bisect terms:
$ git bisect start --term-old=fast --term-new=slow
status: waiting for both good and bad commits
$ git bisect slow HEAD
status: waiting for good commit(s), bad commit known
$ git bisect fast v1.0
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[7331e0026c8ab498791f4f3f8c62dfd007b1da66] Add delivery charge
$ git bisect terms
Your current terms are fast for the old state
and slow for the new state.
$ git bisect good
error: Invalid command: you're currently in a slow/fast bisect
fatal: unknown command: 'good'
…নিজের শব্দ বেছে নিলে good/bad আর চলে না। নিয়ম: “old” history-র আগের অবস্থা, “new” পরের; Git খোঁজে প্রথম “new” commit.
Merge-ভরা history আর --first-parent: একটি feature branch-এর ভেতরে মাঝের কিছু commit হয়তো build-ই হয় না, অথচ merge-এর পরে সব ঠিক। সাধারণ bisect সেই ভেতরের commit-এও নেমে যায়। git bisect start --first-parent merge commit-এ শুধু first parent ধরে চলে — অর্থাৎ main-এর নিজের ধারা — আর দোষী হিসেবে একটি পুরো merge commit দেখায় (“কোন feature merge-এ bug এল”)। এরপর চাইলে সেই branch-এর ভেতরে আলাদা bisect চালানো যায়। Git-এর release notes অনুযায়ী option-টি Git ২.২৯ (২০২০)-এ এসেছে বলে জানা যায়; পুরোনো Git-এ এটি পাবেন না, তাই git help bisect-এ মিলিয়ে নিন। প্রথম parent-এর ধারণা পর্ব ৯-এ (History আর পার্থক্য দেখা) দেখেছেন।
৪ · git bisect run — রায় দেবে আপনার script
হাতে রায় দেওয়া ধীর আর ভুলপ্রবণ — ক্লান্ত হয়ে একবার “good”-এর জায়গায় “bad” টাইপ করলেই খোঁজ ভুল পথে যায়। git bisect run প্রতিটি checkout-এ একটি command চালায় আর তার exit code দেখে নিজেই রায় দেয়। আমাদের check.py শুরু থেকেই এই নিয়মে লেখা, তাই পুরো session এক লাইনে:
$ git bisect start HEAD v1.0
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[7331e0026c8ab498791f4f3f8c62dfd007b1da66] Add delivery charge
$ git bisect run python ../check.py
running 'python' '../check.py'
total for 1000 taka: 1000.0
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[a2aba5872252155ccead4e4e1a8e6301a597184d] WIP: start receipt printing
running 'python' '../check.py'
cannot import bill.py - skip
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[376577801e539bcc8b96af38db80a06871369509] Finish receipt printing
running 'python' '../check.py'
total for 1000 taka: 950.0
Bisecting: 0 revisions left to test after this (roughly 1 step)
[70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6] Use a constant for the discount threshold (#17)
running 'python' '../check.py'
total for 1000 taka: 1000.0
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[c71b11297237a8405fd2077019e1cdef10312046] Add sugar and flour prices
running 'python' '../check.py'
total for 1000 taka: 950.0
70c0892dbff8ce3bd88d0b4c0a628b00a7a9d5e6 is the first bad commit
…
bisect found first bad commit
$ git bisect resetএকই পথ, একই উত্তর, আর মানুষের কোনো হাত নেই। এখন দেখি নিয়মগুলো ঠিক কী — git help bisect-এর ভাষায়: script ভালো (old) অবস্থায় 0 দিয়ে, খারাপ (new) অবস্থায় 1 থেকে 127-এর মধ্যে, 125 বাদে, যেকোনো code দিয়ে বের হবে। 125 মানে “এই commit পরীক্ষা করা যায় না” — skip. অন্য যেকোনো code (অর্থাৎ 128 থেকে 255) পুরো bisect থামিয়ে দেয়। নথিতে আরও আছে: 125 বেছে নেওয়া হয়েছে কারণ 126 আর 127 POSIX shell নিজে ব্যবহার করে (127 = command পাওয়া যায়নি, 126 = পাওয়া গেছে কিন্তু চালানো যায় না)।
// How "git bisect run" reads the exit code of your test script (rules from git help bisect).
function verdict(code, firstRun) {
const c = ((code % 256) + 256) % 256; // the shell keeps only 8 bits: exit(-1) -> 255
if (c === 0) return [c, "good (old)"];
if (c === 125) return [c, "skip - this commit cannot be tested"];
if (c === 126 || c === 127) return [c, firstRun
? "bad? Git first re-runs the script on the good commit; same code there -> abort"
: "bad (new)"];
if (c >= 1 && c <= 127) return [c, "bad (new)"];
return [c, "ABORT the whole bisect (code >= 128, e.g. killed by a signal)"];
}
const cases = [
[0, "tests passed"], [1, "a test failed"], [2, "pytest: usage error"],
[124, "timeout(1) gave up"], [125, "build failed, script said skip"],
[127, "command not found"], [139, "crash: 128 + 11 (SIGSEGV)"], [-1, "exit(-1)"],
];
for (const [code, why] of cases) {
const [c, v] = verdict(code, code === 127);
console.log(`${String(code).padStart(4)} -> $?=${String(c).padEnd(4)} ${why.padEnd(31)} ${v}`);
}
// A crash should usually mean "bad", so wrap the program: ./run_tests || exit 1
const wrapped = (code) => (code === 0 ? 0 : code === 125 ? 125 : 1);
console.log("\nwrapped 139 ->", verdict(wrapped(139), false)[1]);আউটপুট:
0 -> $?=0 tests passed good (old)
1 -> $?=1 a test failed bad (new)
2 -> $?=2 pytest: usage error bad (new)
124 -> $?=124 timeout(1) gave up bad (new)
125 -> $?=125 build failed, script said skip skip - this commit cannot be tested
127 -> $?=127 command not found bad? Git first re-runs the script on the good commit; same code there -> abort
139 -> $?=139 crash: 128 + 11 (SIGSEGV) ABORT the whole bisect (code >= 128, e.g. killed by a signal)
-1 -> $?=255 exit(-1) ABORT the whole bisect (code >= 128, e.g. killed by a signal)
wrapped 139 -> bad (new)
কয়েকটি সারি আলাদা করে পড়ুন। 2 আর 124: test framework-এর “usage error” বা timeout command-এর সময় শেষ হওয়াও “খারাপ” ধরা হয় — অথচ এগুলো হয়তো bug নয়, script-এর সমস্যা। তাই script-কে নিজেই ঠিক করতে হবে কোন ব্যর্থতা আসল। 127: Git ২.৫৩-এ প্রথম চালানোয় 126 বা 127 এলে Git সন্দেহ করে যে command-টিই ভুল; সে ভালো commit-এ script চালিয়ে দেখে, সেখানেও একই code এলে থেমে যায় — নিচের transcript দেখুন। এটি নতুন Git-এর আচরণ; পুরোনো version-এ 127 সরাসরি “bad” হতো বলে মনে করা হয়, তাই আপনার version-এ git help bisect দেখুন। 139: program crash করলে (segmentation fault, signal 11) shell 128 + 11 = 139 দেয় — যা bisect থামিয়ে দেয়। −1: exit(-1) আসলে 255, কারণ exit code-এর শুধু নিচের ৮টি bit থাকে। শেষ লাইন: crash-কেও “খারাপ” গণ্য করতে চাইলে program-কে মুড়ে দিন — ./run_tests || exit 1.
$ git bisect start HEAD v1.0
…
$ git bisect run no-such-tester
running 'no-such-tester'
'no-such-tester': line 1: no-such-tester: command not found
[a122fb65eb5929fb827238c12eb660f083eb7e4f] Add format_taka helper
running 'no-such-tester'
'no-such-tester': line 1: no-such-tester: command not found
error: bogus exit code 127 for good revision
…
$ git bisect reset
…
$ git bisect start HEAD v1.0
…
$ git bisect run sh -c 'exit 200'
running 'sh' '-c' 'exit 200'
error: bisect run failed: exit code 200 from 'sh' '-c' 'exit 200' is < 0 or >= 128প্রথম ক্ষেত্রে command-এর নামেই ভুল; Git ভালো commit a122fb6-এ (v1.0) গিয়ে আবার চালিয়ে একই 127 পায়, তাই “bogus” বলে থামে — নইলে ভুল command-কে প্রতিটি commit-এ “খারাপ” ধরে একটি অর্থহীন উত্তর দিত। “command not found” লাইনের হুবহু লেখা shell আর operating system-ভেদে আলাদা হবে। দ্বিতীয় ক্ষেত্রে 200 সরাসরি পুরো session থামায়।
একটি ভালো bisect script-এর গুণ
- Repository-র বাইরে রাখুন, যাতে পুরোনো checkout-এ script বদলে বা হারিয়ে না যায়।
- শুধু একটি প্রশ্ন: “ঠিক এই bug আছে কি?” পুরো test suite চালালে সম্পর্কহীন কোনো ব্যর্থতা ভুল পথে নেয়। Bug report থেকে বানানো একটি ছোট regression test আদর্শ (পর্ব ৪৬, Unit test).
- “পরীক্ষা করা যায় না” আর “খারাপ” আলাদা: build ব্যর্থ বা test-ফাইলই নেই —
exit 125. যেমন:git bisect run sh -c "make || exit 125; ./check". - নির্ধারিত (deterministic): network, সময় বা random-নির্ভর flaky test একমুখিতার শর্ত ভাঙে (পর্ব ৫১, Test pyramid আর integration test).
এই একই script পরে CI-তে স্থায়ী test হয়ে যেতে পারে, যাতে bug আর ফিরে না আসে — CI-র pipeline নিয়ে বিস্তারিত পর্ব ৫৮-এ (Continuous integration).
৫ · git blame — প্রতিটি লাইনের শেষ পরিবর্তন
অনেক সময় শুরুটা উল্টো দিক থেকে — একটি সন্দেহজনক লাইন চোখে পড়েছে, জানতে চান সেটি কবে, কেন এমন হলো। git blame একটি ফাইলের প্রতিটি লাইনের পাশে লেখে: শেষ কোন commit সেই লাইনটিকে বর্তমান রূপ দিয়েছে, লেখক কে, কখন। অনেক tool-এ একে “annotate” বলে; Git-এও git annotate প্রায় একই কাজ করে, শুধু আউটপুটের বিন্যাস একটু আলাদা।
$ git blame -L :print_receipt bill.py
a2aba587 (Rahim 2026-01-15 10:08:00 +0600 27) def print_receipt(cart):
37657780 (Rahim 2026-01-15 10:09:00 +0600 28) for item, qty in cart:
37657780 (Rahim 2026-01-15 10:09:00 +0600 29) print(item, qty, format_taka(line_total(item, qty)))
87f84699 (Rahim 2026-01-15 10:13:00 +0600 30) print("Total:", format_taka(final_total(cart)))
2cd69ab2 (Rahim 2026-01-15 10:19:00 +0600 31) print("Thank you!")
7331e002 (Nasrin 2026-01-15 10:12:00 +0600 32) প্রতিটি লাইনের গঠন: commit-এর ছোট hash (blame ডিফল্টে ৮ অক্ষর দেখায়), বন্ধনীর ভেতরে লেখক, author date, সময়-অঞ্চল আর বর্তমান ফাইলে লাইনের নম্বর, তারপর লাইনের লেখা। শেষ লাইনটি ফাঁকা, আর সেটি Nasrin-এর — কারণ print_receipt-এর পরে delivery_charge যোগ করার সময় তিনি মাঝে একটি ফাঁকা লাইন রেখেছিলেন। প্রথম commit থেকে আসা লাইনের hash-এর আগে ^ থাকে — মানে খোঁজার সীমানার (boundary) commit, যার আগে আর কিছু নেই:
$ git blame -L 3,5 README.md
^90e700e (Rahim 2026-01-15 10:00:00 +0600 3) A tiny grocery bill calculator for a corner shop.
392acc2c (Nasrin 2026-01-15 10:07:00 +0600 4)
392acc2c (Nasrin 2026-01-15 10:07:00 +0600 5) ## Usageকোন লাইন: -L-এর রূপগুলো
| লেখা | অর্থ |
|---|---|
-L 12,16 | লাইন ১২ থেকে ১৬ |
-L 12,+5 | লাইন ১২ থেকে ৫টি লাইন (১২–১৬) |
-L '/def bill_total/,+6' | যে লাইনে regex মেলে, সেখান থেকে ৬টি |
-L :print_receipt | যে function-এর নামের সাথে মেলে তার পুরো শরীর (পরের function-এর শুরুর আগ পর্যন্ত) |
-L 12,13 -L 27,31 | একাধিক পরিসর একসাথে |
খেয়াল করুন, blame-এ লেখা -L :funcname (শেষে কোনো ফাইল নেই, ফাইল আলাদা argument); কিন্তু git log-এ লেখা -L :funcname:file — অংশ ৭-এ দেখবেন। Function কোথায় শুরু, তা Git অনুমান করে একটি “funcname” নিয়ম দিয়ে: ডিফল্টে অক্ষর, _ বা $ দিয়ে শুরু হওয়া লাইনকে সে function-এর শিরোনাম ভাবে। Python-এ .gitattributes-এ *.py diff=python দিলে নির্ভুল নিয়ম কাজ করে (পর্ব ১০, .gitignore, .gitattributes আর git config).
আরও কয়েকটি দরকারি option: -e নামের বদলে email দেখায়; -s লেখক-সময় বাদ দিয়ে ছোট আউটপুট; blame <commit> -- file পুরোনো একটি অবস্থায় blame চালায় — যেমন “bug ঢোকার ঠিক আগে এই লাইন কার ছিল?” জানতে git blame 70c0892^ -- bill.py.
$ git blame -e -L 12,13 bill.py
40f5f498 (<rahim@example.com> 2026-01-15 10:01:00 +0600 12) def line_total(item, qty):
87f84699 (<rahim@example.com> 2026-01-15 10:13:00 +0600 13) return PRICES[item] * qty
$ git blame -s -L 12,13 bill.py
40f5f498 12) def line_total(item, qty):
87f84699 13) return PRICES[item] * qtyScript-এর জন্য: --porcelain
Editor-এর plugin বা নিজের tool blame পড়তে চাইলে মানুষের জন্য সাজানো আউটপুট নয়, porcelain format ব্যবহার করুন — প্রতিটি অংশ একটি নির্দিষ্ট শিরোনামসহ আলাদা লাইনে, আর পুরো hash:
$ git blame --porcelain -L 13,13 bill.py
87f846999929544ad01f1b16c309301298516ca3 11 13 1
author Rahim
author-mail <rahim@example.com>
author-time 1768450380
author-tz +0600
committer Rahim
committer-mail <rahim@example.com>
committer-time 1768450380
committer-tz +0600
summary Format code: spaces around operators, double quotes
previous 7331e0026c8ab498791f4f3f8c62dfd007b1da66 bill.py
filename bill.py
return PRICES[item] * qtyপ্রথম লাইনের তিনটি সংখ্যা: সেই commit-এ লাইনটি ছিল ১১ নম্বরে, এখন ১৩ নম্বরে, আর দলে ১টি লাইন। author-time Unix সময় (১৯৭০ থেকে সেকেন্ড)। previous বলে “এর আগে কী ছিল” দেখতে কোন commit আর ফাইলে যেতে হবে।
Blame কীভাবে হিসাব হয় — একটি ছোট মডেল
Blame-এর মূল চিন্তা সরল: সবচেয়ে পুরোনো রূপ থেকে শুরু করুন, সব লাইন প্রথম commit-এর নামে। প্রতিটি পরের রূপে আগের রূপের সাথে diff করুন: যে লাইন অপরিবর্তিত, সে আগের মালিকানা রাখে; যে লাইন নতুন বা বদলানো, সে এই commit-এর নামে যায়। (Git আসলে উল্টো দিকে — নতুন থেকে পুরোনোয় — হাঁটে, আর প্রতিটি লাইনের দায় parent-এ “ঠেলে” দেয় যতক্ষণ সম্ভব; ফল একই।) নিচে eid_discount-এর তিনটি রূপ — commit ৪, ১২ আর ১৪-এর bill.py থেকে নেওয়া:
# A minimal "git blame": each line remembers the commit that last introduced it.
import difflib
versions = [
("86bd46b", ["def eid_discount(amount):", " if amount >= 1000:",
" return amount*0.95", " return amount"]),
("70c0892", ["DISCOUNT_MIN = 1000", "",
"def eid_discount(amount):",
" if amount > DISCOUNT_MIN:",
" return amount*0.95",
" return amount"]),
("87f8469", ["DISCOUNT_MIN = 1000", "",
"def eid_discount(amount):",
" if amount > DISCOUNT_MIN:",
" return amount * 0.95",
" return amount"]),
]
def blame(versions, ignore_space=False):
key = (lambda s: "".join(s.split())) if ignore_space else (lambda s: s)
owners, old = [], []
for commit, new in versions:
sm = difflib.SequenceMatcher(None, [key(x) for x in old], [key(x) for x in new], autojunk=False)
fresh = [commit] * len(new) # unmatched lines belong to this commit
for tag, i1, i2, j1, j2 in sm.get_opcodes():
if tag == "equal": # unchanged lines keep their old owner
fresh[j1:j2] = owners[i1:i2]
owners, old = fresh, new
return list(zip(owners, old))
for title, flag in (("plain", False), ("ignore whitespace (-w)", True)):
print(title)
for n, (c, line) in enumerate(blame(versions, flag), start=2):
print(f" {c} {n}) {line}")আউটপুট:
plain
70c0892 2) DISCOUNT_MIN = 1000
70c0892 3)
86bd46b 4) def eid_discount(amount):
70c0892 5) if amount > DISCOUNT_MIN:
87f8469 6) return amount * 0.95
86bd46b 7) return amount
ignore whitespace (-w)
70c0892 2) DISCOUNT_MIN = 1000
70c0892 3)
86bd46b 4) def eid_discount(amount):
70c0892 5) if amount > DISCOUNT_MIN:
86bd46b 6) return amount * 0.95
86bd46b 7) return amount
লাইন নম্বর ২ থেকে শুরু, যাতে আজকের discount.py-এর সাথে মেলে (সেখানে লাইন ১ একটি নতুন মন্তব্য)। difflib.SequenceMatcher দুই রূপের মিলে যাওয়া লাইন-খণ্ড (equal) দেয়; শুধু সেগুলো আগের মালিক রাখে। plain: if-এর লাইন commit ১২-র (70c0892) — bug ঠিক এখানেই; return amount * 0.95 commit ১৪-র (formatting), কারণ লেখা বদলেছে। -w: তুলনার আগে সব ফাঁকা জায়গা মুছলে amount*0.95 আর amount * 0.95 একই, তাই লাইনটি Nasrin-এর commit ৪-এ (86bd46b) ফেরে। পরের অংশে আসল git blame -C আর -C -w-এর আউটপুট এর সাথে হুবহু মিলবে।
Blame দেখায় “শেষ পরিবর্তন”, “bug-এর লেখক” নয়। উপরের bill.py-এর লাইন ১৩-এ blame বলছে 87f8469 — অথচ সেই commit শুধু *-এর দুপাশে space দিয়েছে; লাইনটি লিখেছিলেন অন্য কেউ, অনেক আগে। আবার যে লাইন বদলায়নি, তার কারণেও bug হতে পারে — যেমন অন্য ফাইলের একটি function বদলে যাওয়ায়। Blame একটি সূত্র ধরিয়ে দেয়: “এই commit-টি পড়ুন”। কাউকে দোষী সাব্যস্ত করার যন্ত্র এটি নয় — অংশ ৮-এ blameless সংস্কৃতি নিয়ে আবার আসব।
৬ · পর্দা সরানো — -w, -M, -C আর .git-blame-ignore-revs
বাস্তব project-এ blame-এর সবচেয়ে বড় শত্রু দুটি: formatting commit (কেউ formatter চালিয়ে হাজার লাইনের space আর উদ্ধৃতিচিহ্ন বদলেছেন) আর কোড সরানো (function এক জায়গা থেকে আরেক জায়গায়, বা এক ফাইল থেকে আরেক ফাইলে)। দুটিতেই আসল ইতিহাস ঢাকা পড়ে। আমাদের repository-তে দুটিই আছে: commit ১৪ (87f8469) পুরো bill.py-এ space আর double quote বসিয়েছে, আর commit ১৫ (1853d66) eid_discount-কে নতুন ফাইল discount.py-তে সরিয়েছে।
অন্য ফাইল থেকে আসা কোড: -C
$ git blame discount.py
1853d668 (Rahim 2026-01-15 10:14:00 +0600 1) # discount.py - eid discount rules
1853d668 (Rahim 2026-01-15 10:14:00 +0600 2) DISCOUNT_MIN = 1000
1853d668 (Rahim 2026-01-15 10:14:00 +0600 3)
1853d668 (Rahim 2026-01-15 10:14:00 +0600 4) def eid_discount(amount):
1853d668 (Rahim 2026-01-15 10:14:00 +0600 5) if amount > DISCOUNT_MIN:
1853d668 (Rahim 2026-01-15 10:14:00 +0600 6) return amount * 0.95
1853d668 (Rahim 2026-01-15 10:14:00 +0600 7) return amount
$ git blame -C discount.py
1853d668 discount.py (Rahim 2026-01-15 10:14:00 +0600 1) # discount.py - eid discount rules
70c0892d bill.py (Rahim 2026-01-15 10:11:00 +0600 2) DISCOUNT_MIN = 1000
70c0892d bill.py (Rahim 2026-01-15 10:11:00 +0600 3)
86bd46bc bill.py (Nasrin 2026-01-15 10:03:00 +0600 4) def eid_discount(amount):
70c0892d bill.py (Rahim 2026-01-15 10:11:00 +0600 5) if amount > DISCOUNT_MIN:
87f84699 bill.py (Rahim 2026-01-15 10:13:00 +0600 6) return amount * 0.95
86bd46bc bill.py (Nasrin 2026-01-15 10:03:00 +0600 7) return amount
$ git blame -C -w discount.py
…
86bd46bc bill.py (Nasrin 2026-01-15 10:03:00 +0600 6) return amount * 0.95
…সাধারণ blame-এর কাছে discount.py নতুন ফাইল, তাই সব লাইন commit ১৫-র — সম্পূর্ণ অকেজো তথ্য। -C খোঁজে: এই commit-এ বদলানো অন্য ফাইল থেকে কি লাইনগুলো সরে বা কপি হয়ে এসেছে? এসেছে — bill.py থেকে। তাই আউটপুটে একটি বাড়তি কলাম, মূল ফাইলের নাম, আর আসল ইতিহাস: bug-এর if লাইন commit ১২-র, যা bisect-ও বলেছিল। -C -w যোগ করলে লাইন ৬ formatting পেরিয়ে commit ৪-এ যায় — ঠিক আগের Python মডেলের মতো। এই খোঁজকে বলে copy detection.
| Option | কী খোঁজে | খরচ |
|---|---|---|
-M | একই ফাইলের ভেতরে সরানো বা কপি করা লাইন | কম |
-C | -M + একই commit-এ বদলানো অন্য ফাইল থেকে | মাঝারি |
-C -C | + ফাইলটি যে commit-এ তৈরি হয়েছে, সেখানে থাকা যেকোনো ফাইল থেকে | বেশি |
-C -C -C | + যেকোনো commit-এর যেকোনো ফাইল থেকে | অনেক বেশি (বড় repository-তে ধীর) |
একটি সূক্ষ্ম শর্ত: Git তখনই লাইনগুলোকে “সরানো” মানে, যখন একসাথে অন্তত একটি নির্দিষ্ট পরিমাণ অক্ষর মেলে — নথি অনুযায়ী ডিফল্টে -M-এ ২০টি আর -C-এ ৪০টি alphanumeric অক্ষর (-C30-এর মতো করে বদলানো যায়)। তাই খুব ছোট পরিসরে চালালে ফল বদলে যায়:
$ git blame -C -L 5,5 discount.py
1853d668 (Rahim 2026-01-15 10:14:00 +0600 5) if amount > DISCOUNT_MIN:
$ git blame -C -L :eid_discount discount.py
86bd46bc bill.py (Nasrin 2026-01-15 10:03:00 +0600 4) def eid_discount(amount):
70c0892d bill.py (Rahim 2026-01-15 10:11:00 +0600 5) if amount > DISCOUNT_MIN:
87f84699 bill.py (Rahim 2026-01-15 10:13:00 +0600 6) return amount * 0.95
86bd46bc bill.py (Nasrin 2026-01-15 10:03:00 +0600 7) return amountশুধু লাইন ৫ চাইলে Git-এর হাতে মাত্র একটি লাইন — ৪০টি অক্ষরের কম — তাই সে সরানো চিনতে পারে না। পুরো function দিলে চেনে। শিক্ষা: -C-এর সাথে পরিসর একটু বড় রাখুন।
একই ফাইলের ভেতরে সরানো: -M
Commit ১৭ (05792ca) format_taka-কে একই ফাইলের নিচ থেকে ওপরে এনেছে:
$ git blame -L 12,16 bill.py
40f5f498 (Rahim 2026-01-15 10:01:00 +0600 12) def line_total(item, qty):
87f84699 (Rahim 2026-01-15 10:13:00 +0600 13) return PRICES[item] * qty
40f5f498 (Rahim 2026-01-15 10:01:00 +0600 14)
05792cac (Rahim 2026-01-15 10:16:00 +0600 15) def format_taka(amount):
05792cac (Rahim 2026-01-15 10:16:00 +0600 16) return "Tk " + format(amount, ",.2f")
$ git blame -M -L 12,16 bill.py
40f5f498 (Rahim 2026-01-15 10:01:00 +0600 12) def line_total(item, qty):
87f84699 (Rahim 2026-01-15 10:13:00 +0600 13) return PRICES[item] * qty
40f5f498 (Rahim 2026-01-15 10:01:00 +0600 14)
a122fb65 (Rahim 2026-01-15 10:05:00 +0600 15) def format_taka(amount):
87f84699 (Rahim 2026-01-15 10:13:00 +0600 16) return "Tk " + format(amount, ",.2f")-M ছাড়া লাইন ১৫–১৬ “সরানোর commit”-এর; -M-এ লাইন ১৫ ফেরে যেখানে function-টি প্রথম লেখা হয়েছিল (commit ৬, a122fb6), আর লাইন ১৬ formatting commit-এ (single quote থেকে double quote — whitespace নয়, তাই -w-ও এখানে কাজে আসত না)।
Formatting commit উপেক্ষা: --ignore-rev আর .git-blame-ignore-revs
-w শুধু ফাঁকা জায়গার পরিবর্তন উপেক্ষা করে। কিন্তু formatter প্রায়ই আরও বদলায় — উদ্ধৃতিচিহ্ন, লাইন ভাঙা, কমা। তখন উপায়: Git-কে বলা, “এই commit-টি এমনভাবে উপেক্ষা করুন, যেন এটি ঘটেইনি।” Commit একটি হলে --ignore-rev <commit>; স্থায়ী তালিকা চাইলে repository-তে একটি ফাইল — প্রচলিত নাম .git-blame-ignore-revs — যেটি দলের সবাই ব্যবহার করবেন। নিচের stepper-এ ধাপে ধাপে:
$ git blame -L 12,13 bill.py
40f5f498 (Rahim ... 12) def line_total(item, qty):
87f84699 (Rahim ... 13) return PRICES[item] * qty
@@ -4,11 +4,11 @@ PRICES = {}
...
- name, price = line.strip().split(',')
+ name, price = line.strip().split(",")
...
def line_total(item, qty):
- return PRICES[item]*qty
+ return PRICES[item] * qty
$ git blame -w -L 12,13 bill.py 40f5f498 (Rahim ... 12) def line_total(item, qty): 40f5f498 (Rahim ... 13) return PRICES[item] * qty $ git blame -w -L :print_receipt bill.py ... 87f84699 (Rahim ... 30) print("Total:", ...)
$ git blame --ignore-rev 87f8469 -L :print_receipt bill.py
...
37657780 (Rahim ... 30) print("Total:", ...)
...
$ echo 87f84699 > .git-blame-ignore-revs $ git blame --ignore-revs-file .git-blame-ignore-revs ... fatal: invalid object name: 87f84699 $ git rev-parse 87f8469 > .git-blame-ignore-revs $ git config blame.ignoreRevsFile .git-blame-ignore-revs $ git blame -L 12,13 bill.py 40f5f498 (Rahim ... 12) def line_total(item, qty): 40f5f498 (Rahim ... 13) return PRICES[item] * qty
$ git -c blame.markIgnoredLines=true \
blame --ignore-rev 87f8469 -L 12,13 bill.py
40f5f498 (Rahim ... 12) def line_total(item, qty):
?40f5f49 (Rahim ... 13) return PRICES[item] * qty
$ git blame --ignore-revs-file "" -L 12,13 bill.py
...
87f84699 (Rahim ... 13) return PRICES[item] * qty
- প্রশ্ন:
line_total-এর হিসাবের লাইনটি কে লিখেছিলেন? সাধারণ blame বলে87f84699— commit ১৪, “Format code”. (জায়গা বাঁচাতে তারিখ-সময় “...” দিয়ে ছাঁটা।) - Commit-টি খুলে দেখা:
git show 87f8469— এটি শুধু চেহারা বদলেছে:*-এর দুপাশে space, single quote থেকে double quote. অর্থ একই। লাইনের আসল জন্ম আরও আগে। -w: লাইন ১৩-র পরিবর্তন শুধু space, তাই-wসেটি পেরিয়ে commit ২ (40f5f498) দেখায়। কিন্তুprint("Total:", …)লাইনে পরিবর্তন quote-এর — whitespace নয় — তাই-w-এও সেটি commit ১৪-তে আটকে থাকে।--ignore-rev 87f8469: Git commit ১৪-কে “ঘটেনি” ধরে তার বদলানো লাইনের দায় আগের commit-এ দেয়। এবারprint("Total:"লাইন commit ১০-এর (37657780, “Finish receipt printing”) — আসল উৎস।- দলের তালিকা: ফাইলে প্রতি লাইনে একটি পুরো ৪০ অক্ষরের hash লাগে — ছোট hash দিলে Git “invalid object name” বলে থামে (তাই
git rev-parseদিয়ে লেখা)।blame.ignoreRevsFileconfig দিলে প্রতিবার option লিখতে হয় না। ফাইলটি commit করে রাখলে পুরো দল একই তালিকা পায়; কিছু hosting platform-এর blame পাতাও লেখার সময়ে এই নামের ফাইলটি মানে, তবে তা platform-ভেদে আলাদা। - সততা রাখা:
blame.markIgnoredLinesচালু করলে যে লাইনের দায় উপেক্ষার কারণে সরানো হয়েছে, তার আগে?বসে — পাঠক জানেন এটি অনুমান। আর কোনো মুহূর্তে তালিকা ছাড়া আসল ছবি দেখতে--ignore-revs-file ""— খালি নাম আগের সব তালিকা মুছে দেয়।
ভালো অভ্যাস: পুরো codebase-এ formatter চালানোর commit আলাদা রাখুন, বার্তায় স্পষ্ট লিখুন (“Format code with …, no behaviour change”), তারপর একটি দ্বিতীয় commit-এ তার পুরো hash .git-blame-ignore-revs-এ যোগ করুন — একই commit-এ সম্ভব নয়, কারণ commit-এর hash তৈরি হয় তার বিষয়বস্তু থেকেই। পর্ব ৪১-এ (Clean code) এই ফাইলের কথা সংক্ষেপে এসেছিল; Git-এর নথি অনুযায়ী --ignore-rev-এর সুবিধা Git ২.২৩ (২০১৯)-এ যোগ হয়েছিল বলে release notes-এ উল্লেখ আছে।
৭ · Pickaxe আর function-এর ইতিহাস — log -S, -G আর -L
Blame চলে আজকের ফাইল থেকে পেছনে। কিন্তু যে লেখা আজ আর নেই — একটি মুছে ফেলা function, পুরোনো একটি সংখ্যা — তার খোঁজ blame দিতে পারে না। তখন pickaxe (গাঁইতি — মাটি খুঁড়ে পুরোনো জিনিস বের করা): git log -S'লেখা' সেই সব commit দেখায় যেখানে কোনো ফাইলে লেখাটি কতবার আছে, সেই সংখ্যা বদলেছে — অর্থাৎ লেখাটি যোগ হয়েছে বা মুছেছে।
$ git log --oneline -S'>= 1000'
70c0892 Use a constant for the discount threshold (#17)
86bd46b Add eid discount function
$ git log --oneline -S'DISCOUNT_MIN'
1853d66 Move eid discount into discount.py
70c0892 Use a constant for the discount threshold (#17)প্রথম প্রশ্ন: “>= 1000 শর্তটি কোথায় এল আর কোথায় গেল?” দুটি commit — এসেছিল commit ৪-এ, চলে গেছে commit ১২-তে। Bisect না চালিয়েও সন্দেহ সরাসরি commit ১২-তে পৌঁছায়। দ্বিতীয় প্রশ্নে commit ১৫-ও আসে, কারণ সেখানে bill.py-তে DISCOUNT_MIN-এর সংখ্যা ২ থেকে ০ হয়েছে আর discount.py-তে ০ থেকে ২ — গোনা হয় প্রতিটি ফাইলে আলাদা করে।
-S গোনে, -G মেলায়
-G'regex' একটি ভিন্ন প্রশ্ন করে: commit-এর patch-এ কি এমন কোনো যোগ হওয়া বা বাদ যাওয়া লাইন আছে, যা এই regular expression-এর সাথে মেলে? বেশির ভাগ সময় দুটির উত্তর এক। পার্থক্য দেখা দেয় যখন লাইন বদলায় কিন্তু লেখাটির সংখ্যা বদলায় না — যেমন লাইন সরানো বা চেহারা বদলানো:
$ git log --oneline -S'def format_taka'
a122fb6 Add format_taka helper
$ git log --oneline -G'def format_taka'
05792ca Move format_taka next to line_total
a122fb6 Add format_taka helper
$ git log --oneline -S'0.95'
1853d66 Move eid discount into discount.py
86bd46b Add eid discount function
$ git log --oneline -G'0\.95'
1853d66 Move eid discount into discount.py
87f8469 Format code: spaces around operators, double quotes
86bd46b Add eid discount functionCommit ১৭ format_taka-কে একই ফাইলের ভেতরে সরিয়েছে — def format_taka আগেও একবার, পরেও একবার; সংখ্যা বদলায়নি, তাই -S চুপ। কিন্তু diff-এ লাইনটি একবার - আর একবার + হিসেবে আছে, তাই -G ধরে। একইভাবে commit ১৪ amount*0.95-কে amount * 0.95 করেছে: “0.95” আগেও একবার, পরেও একবার — -S-এ নেই, -G-তে আছে। আর commit ১৫ ফাইল বদলেছে, তাই সেখানে দুটিতেই আসে। নিচের মডেলটি ঠিক এই নিয়ম দুটি চালায়:
// Model of git log -S (count of a string changes) vs -G (a changed line matches a regex).
const history = [
["c1 add helper", ["def line_total():", "def format_taka():", "def bill_total():"]],
["c2 move helper", ["def format_taka():", "def line_total():", "def bill_total():"]],
["c3 discount", ["def format_taka():", "def line_total():", "def bill_total():", "x = a*0.95"]],
["c4 reformat", ["def format_taka():", "def line_total():", "def bill_total():", "x = a * 0.95"]],
];
function diffLines(a, b) { // LCS table, then walk it: returns changed lines
const L = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
for (let i = a.length - 1; i >= 0; i--)
for (let j = b.length - 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 out = []; let i = 0, j = 0;
while (i < a.length || j < b.length) {
if (i < a.length && j < b.length && a[i] === b[j]) { i++; j++; }
else if (j < b.length && (i === a.length || L[i][j + 1] >= L[i + 1][j])) out.push("+" + b[j++]);
else out.push("-" + a[i++]);
}
return out.sort((x, y) => (x[0] === "-" ? 0 : 1) - (y[0] === "-" ? 0 : 1)); // "-" first, like Git
}
const count = (lines, s) => lines.join("\n").split(s).length - 1;
function search(s, re) {
let prev = [];
for (const [name, lines] of history) {
const changed = diffLines(prev, lines);
const S = count(prev, s) !== count(lines, s);
const G = changed.some((l) => re.test(l.slice(1)));
console.log(`${name.padEnd(15)} count ${count(prev, s)}->${count(lines, s)} -S:${S ? "yes" : "no "}` +
` -G:${G ? "yes" : "no "} diff: ${changed.join(" ")}`);
prev = lines;
}
}
console.log('-S"format_taka" vs -G"format_taka"'); search("format_taka", /format_taka/);
console.log('\n-S"0.95" vs -G"0\\.95"'); search("0.95", /0\.95/);আউটপুট:
-S"format_taka" vs -G"format_taka"
c1 add helper count 0->1 -S:yes -G:yes diff: +def line_total(): +def format_taka(): +def bill_total():
c2 move helper count 1->1 -S:no -G:yes diff: -def format_taka(): +def format_taka():
c3 discount count 1->1 -S:no -G:no diff: +x = a*0.95
c4 reformat count 1->1 -S:no -G:no diff: -x = a*0.95 +x = a * 0.95
-S"0.95" vs -G"0\.95"
c1 add helper count 0->0 -S:no -G:no diff: +def line_total(): +def format_taka(): +def bill_total():
c2 move helper count 0->0 -S:no -G:no diff: -def format_taka(): +def format_taka():
c3 discount count 0->1 -S:yes -G:yes diff: +x = a*0.95
c4 reformat count 1->1 -S:no -G:yes diff: -x = a*0.95 +x = a * 0.95
মডেলটি পড়ার নিয়ম: count হলো আগের আর পরের পুরো ফাইলে লেখাটি কতবার আছে; -S “yes” হয় শুধু সংখ্যা বদলালে। diff হলো LCS দিয়ে পাওয়া বদলানো লাইন; -G “yes” হয় যদি তার কোনোটি regex মেলে। c2: সংখ্যা ১→১, কিন্তু সরানো লাইনটি diff-এ আছে — শুধু -G. c4: “0.95” ১→১, কিন্তু বদলানো লাইনে আছে — আবার শুধু -G. একটি সূক্ষ্ম কথা: c2-তে diff চাইলে উল্টোভাবেও দেখাতে পারত — যেন line_total নিচে নেমেছে আর format_taka জায়গায় আছে। তখন -G"format_taka" c2 ধরত না! -G diff-এর লেখা দেখে, তাই এর ফল diff algorithm-এর ওপর নির্ভর করে; -S-এর ফল করে না।
আরও দুটি option: --pickaxe-regex দিলে -S-এর লেখাটিও regex হিসেবে পড়া হয় (তবু গোনার নিয়মে)। আর --pickaxe-all: ডিফল্টে -S/-G commit-এর শুধু সেই ফাইলগুলো দেখায় যেখানে মিল পাওয়া গেছে; এটি দিলে সেই commit-এর সব ফাইল দেখায় — পরিবর্তনের পুরো প্রসঙ্গ বুঝতে কাজের:
$ git log --oneline --stat -S'from discount'
1853d66 Move eid discount into discount.py
bill.py | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
$ git log --oneline --stat -S'from discount' --pickaxe-all
1853d66 Move eid discount into discount.py
bill.py | 9 ++-------
discount.py | 7 +++++++
2 files changed, 9 insertions(+), 7 deletions(-)একটি function-এর পুরো জীবন: git log -L
git log -L একটি নির্দিষ্ট লাইন-পরিসর বা function ধরে history-তে পেছনে হাঁটে, আর শুধু সেই commit দেখায় যেগুলো ঐ অংশ বদলেছে — প্রতিটির সাথে শুধু ঐ অংশের diff. লেখার রূপ -L :funcname:file বা -L 27,32:file. print_receipt-এর জীবনী:
$ git log --oneline -L :print_receipt:bill.py
2cd69ab Add a thank-you line to the receipt
…
87f8469 Format code: spaces around operators, double quotes
…
7331e00 Add delivery charge
…
3765778 Finish receipt printing
…
a2aba58 WIP: start receipt printing
…@@ -29,0 +30,3 @@
+def print_receipt(cart):
+ for item, qty in cart
+ print(item, qty)
@@ -30,3 +30,4 @@
def print_receipt(cart):
- for item, qty in cart
- print(item, qty)
+ for item, qty in cart:
+ print(item, qty, format_taka(line_total(item, qty)))
+ print('Total:', format_taka(final_total(cart)))
@@ -32,4 +32,5 @@
def print_receipt(cart):
for item, qty in cart:
print(item, qty, format_taka(line_total(item, qty)))
print('Total:', format_taka(final_total(cart)))
+
@@ -32,5 +32,5 @@ def print_receipt(cart): ... - print('Total:', format_taka(final_total(cart))) + print("Total:", format_taka(final_total(cart)))
@@ -27,5 +27,6 @@
def print_receipt(cart):
...
print("Total:", format_taka(final_total(cart)))
+ print("Thank you!")
1853d66 Move eid discount into discount.py
05792ca Move format_taka next to line_total
↑ দুটিই print_receipt-এর লাইন ছোঁয়নি,
শুধু ওপরের কোড সরিয়েছে:
লাইন 32 থেকে 27-এ নেমেছে, লেখা একই
- জন্ম, অসম্পূর্ণ অবস্থায়:
@@ -29,0 +30,3 @@— আগে কিছু ছিল না, লাইন ৩০ থেকে ৩টি নতুন লাইন।for-এর শেষে colon নেই — bisect-এ এটিই skip হয়েছিল। - পরের মিনিটেই ঠিক: colon এল, দাম আর মোট ছাপার লাইন যোগ হলো; ৩টি লাইন থেকে ৪টি।
- Nasrin-এর একটি ফাঁকা লাইন:
delivery_chargeযোগের সময় শেষে একটি ফাঁকা লাইন — পরিসর চলে পরের function শুরুর আগ পর্যন্ত, তাই এটিওprint_receipt-এর অংশ। অংশ ৫-এর blame-এর শেষ লাইনের রহস্য এটাই। - শুধু চেহারা: formatting commit single quote-কে double quote করেছে; লেখা বদলেছে, তাই
log -Lএটিও দেখায়। - সর্বশেষ: “Thank you!” যোগ। Hunk এখন লাইন ২৭-এ, আগে ছিল ৩২ — ওপরের কোড সরে যাওয়ায় function-টি নড়েছে।
- যা নেই, সেটিও তথ্য: commit ১৫ আর ১৭ শুধু লাইন নম্বর বদলেছে, লেখা নয়;
log -Lসেই সরে যাওয়া অনুসরণ করে আর এমন commit বাদ দেয় — ২০টির মধ্যে মাত্র ৫টি পড়তে হলো।
--follow আর log -L-এর সীমা: git log --follow -- file একটি পুরো ফাইলের নাম বদল পেরিয়ে history দেখায় (Git-এর rename detection দিয়ে)। কিন্তু ফাইলের একটি অংশ অন্য ফাইলে সরালে সেটি নাম বদল নয় — git log --oneline --follow -- discount.py শুধু 1853d66 দেখায়, আগের কিছু নয়; log -L :eid_discount:discount.py-ও সেখানে থেমে যায়। তখন উপায় git blame -C, git log -S, অথবা পুরোনো ফাইলে আগের commit থেকে log -L: git log -L :eid_discount:bill.py 87f8469.
৮ · git grep, shortlog আর একটি পূর্ণ তদন্ত
তদন্তের আরেকটি হাতিয়ার git grep — সাধারণ grep-এর মতো লেখা খোঁজে, কিন্তু শুধু Git-এর চেনা ফাইলে (build-এর ফল বা untracked জঞ্জাল বাদ), আর যেকোনো পুরোনো revision-এ, checkout না করেই:
$ git grep -n 1000
discount.py:2:DISCOUNT_MIN = 1000
$ git grep -n 1000 v1.0
v1.0:bill.py:20: if amount >= 1000:
$ git grep -n -p 0.95
discount.py=4=def eid_discount(amount):
discount.py:6: return amount * 0.95
$ git grep -n -e amount --and -e 0.95
discount.py:6: return amount * 0.95
$ git grep -c "def " v1.0 HEAD
v1.0:bill.py:6
HEAD:bill.py:7
HEAD:discount.py:1
HEAD:test_bill.py:1-n লাইন নম্বর দেয়। একটি revision দিলে (v1.0) সেই snapshot-এ খোঁজে, আর ফলের আগে revision-এর নাম বসায় — এক নজরে দেখা গেল v1.0-তে শর্ত ছিল >= 1000. -p মিলের আগে সেই function-এর শিরোনাম দেখায় (= চিহ্ন দিয়ে আলাদা করা)। --and দিয়ে একই লাইনে দুটি শর্ত। আর একাধিক revision একসাথে দিয়ে তুলনা করা যায় — v1.0-তে def ছিল ৬টি, এখন সব মিলিয়ে ৯টি।
সময়রেখা আর মানুষ: shortlog আর --since
কখনো প্রশ্ন হয় “ঐ সপ্তাহে bill.py-তে কী কী বদলেছিল?” বা “এই ফাইল সম্পর্কে কাকে জিজ্ঞেস করা যায়?”:
$ git log --oneline --since=2026-01-15T10:10 --until=2026-01-15T10:14 -- bill.py
1853d66 Move eid discount into discount.py
87f8469 Format code: spaces around operators, double quotes
7331e00 Add delivery charge
70c0892 Use a constant for the discount threshold (#17)
$ git shortlog -sn HEAD -- bill.py
10 Rahim
3 Nasrinবাস্তবে সময়ের সীমা দিন বা সপ্তাহের হবে (--since="2 weeks ago"); এখানে commit-গুলো এক মিনিট পরপর, তাই মিনিট দিয়ে। --since committer date দেখে, আর shortlog-এর সংখ্যা মানুষ বিচারের মাপ নয় — শুধু “কে এই ফাইল চেনেন” তার মোটা ইঙ্গিত (পর্ব ৯-এর আলোচনা দেখুন)। কোন ফাইল সবচেয়ে বেশি বদলায় — churn আর hotspot — সেই হিসাব পর্ব ৪৩-এ (Technical debt, legacy code আর maintenance).
সব জুড়ে: কবে → কোথায় → কেন → সমাধান
- পুনরুৎপাদন: অভিযোগকে একটি ছোট স্বয়ংক্রিয় যাচাইয়ে রূপ দেওয়া —
check.py, ১,০০০ টাকায় ৯৫০ আশা। - কবে:
git bisect start HEAD v1.0→git bisect run python ../check.py→ প্রথম খারাপ70c0892. বিকল্প দ্রুত পথ:git log -S'>= 1000'. - কোথায়:
git show 70c0892—>=থেকে>. আজকের কোডে লাইনটি কোথায়?git blame -C discount.py— লাইন ৫, তখনও commit ১২-র। - কেন: commit-বার্তা বলছে উদ্দেশ্য ছিল শুধু সংখ্যাটিকে constant করা — issue #17. Issue-তে গিয়ে নিশ্চিত হওয়া যায় যে নিয়ম বদলানোর কোনো সিদ্ধান্ত হয়নি; চিহ্নটি দুর্ঘটনা। ভালো বার্তা আর issue-র নম্বর এখানে সোনার মতো দামি — requirement থেকে কোড পর্যন্ত এই traceability-র কথা পর্ব ২৪-এ, আর ভালো commit-বার্তার নিয়ম পর্ব ১৮-এ।
- সমাধান:
>=ফেরানো, আরcheck.py-এর প্রশ্নটি একটি স্থায়ী regression test হিসেবে test suite-এ যোগ — যাতে এই bug আর কখনো নিঃশব্দে ফিরে না আসে। বার্তায় দোষী commit-এর উল্লেখ: “Fix discount boundary broken in 70c0892”.
Blameless সংস্কৃতি: উপরের তদন্তে একবারও প্রশ্ন ওঠেনি “Rahim কেন এমন করলেন?” — প্রশ্ন ছিল “আমাদের ব্যবস্থা কেন এই ভুল ধরতে পারল না?” সীমানার মানের কোনো test ছিল না; review-এ diff-এর এক চিহ্নের পরিবর্তন চোখ এড়িয়েছে; refactoring আর আচরণ বদল আলাদা commit-এ ছিল না। এই দৃষ্টিভঙ্গির নাম blameless culture. John Allspaw-এর ২০১২ সালের লেখা “Blameless PostMortems and a Just Culture” আর Google-এর Site Reliability Engineering বইয়ের (২০১৬) “Postmortem Culture” অধ্যায় এই ধারণাকে সফটওয়্যার জগতে জনপ্রিয় করেছে বলে ব্যাপকভাবে উল্লেখ করা হয়। মানুষ শাস্তির ভয় পেলে ভুল লুকায় — psychological safety ছাড়া ভালো postmortem হয় না। তাই blame-এর ব্যবহার “কাকে জিজ্ঞেস করলে প্রসঙ্গ জানা যাবে” খুঁজতে।
আরও গভীরে: Git-এর bisect-এর algorithm নিয়ে বিস্তারিত লিখেছেন Christian Couder — “Fighting regressions with git bisect” (Linux-Kongress ২০০৯), যা Git-এর নথির সাথেই (git-bisect-lk2009) বিতরণ হয়; সেখানে DAG-এ “সবচেয়ে ভালো bisection point” বাছাই আর skip-এর পাশের commit বাছার কৌশল ব্যাখ্যা করা আছে। Linux kernel-এর নথিতে bug report-কারীদের জন্য bisect করার আলাদা নির্দেশিকা আছে — বড় project-এ “bisect করে দোষী commit বলুন” প্রায় একটি সামাজিক নিয়ম। Pickaxe-এর কাজের নিয়ম gitdiffcore(7)-এ, আর blame-এর ভেতরের diff ও copy detection-এর বিবরণ git help blame-এ।
পরীক্ষা ও ইন্টারভিউতে:
- ২,০০০টি commit-এর মধ্যে regression খুঁজতে bisect-এ সর্বোচ্চ কয়টি পরীক্ষা? — ⌈log₂ 2000⌉ = ১১ (২¹¹ = ২,০৪৮)।
git bisect run-এ exit code 125-এর মানে? — এই commit পরীক্ষা করা যায় না, skip; 0 ভালো, 1–127 (125 বাদে) খারাপ, 128+ হলে bisect থামে।git log -Sআর-G-এর পার্থক্য? —-Sলেখার সংখ্যা বদলালে দেখায়;-Gবদলানো লাইন regex মেলালে — সরানো লাইন শুধু-G-তে।- Formatter চালানোর পর blame অকেজো হলে? — সেই commit-এর পুরো hash
.git-blame-ignore-revs-এ রেখেblame.ignoreRevsFileconfig; space-মাত্র হলে-w. - Function অন্য ফাইলে সরানোর পর তার আসল লেখক দেখতে? —
git blame -C(দরকারে-C -C), অথবাgit log -S.
৯ · AI-এর যুগে — দোষী commit খুঁজতে AI, যাচাই করতে আপনি
AI bisect-এর script লিখতে, বড় diff-এর সারাংশ দিতে বা blame থেকে সময়রেখা বানাতে সত্যিই সাহায্য করে। কিন্তু এখানে তার কিছু পরিচিত ভুল আছে:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Bisect-এর মাঝে “পরিষ্কার করতে” git reset --hard বা git clean -fdx — commit না করা কাজ চিরতরে মুছে যেতে পারে; ফেরার সঠিক পথ git bisect reset | Bisect শুরুর আগে git status পরিষ্কার রাখুন বা git stash; কিছু বদলায় এমন command আগে একটি পরীক্ষার clone-এ |
Script-এর exit code ভুল: build ব্যর্থ হলে exit 1 (ফলে “খারাপ”), বা test framework-এর কাঁচা code সরাসরি ফেরত — কোনো test খুঁজে না পেলে pytest ৫ দেয়, যা bisect “খারাপ” ধরবে | অংশ ৪-এর নিয়মে মিলিয়ে দেখুন; জানা ভালো আর জানা খারাপ commit-এ script আগে হাতে চালান |
বানানো flag: git bisect --auto, git blame --ignore-formatting, git log -S --regex | git help bisect, git help blame; Git সাথে সাথে “unknown option” বলে |
.git-blame-ignore-revs-এ ছোট hash বসানো | Git “invalid object name” দেয়; git rev-parse দিয়ে পুরো hash লিখুন |
| Blame-এর ফল দেখে “এই developer bug ঢুকিয়েছেন” সিদ্ধান্ত | Commit খুলে দেখুন সেটি formatting বা সরানো কি না; -w -C, ignore-revs; আর দোষ নয়, কারণ খুঁজুন |
| Version-এর দাবি বানানো (“--first-parent Git ২.১০ থেকে আছে”) | Git-এর release notes আর আপনার git --version-এর নথি |
পুরো git log -p বা blame prompt-এ পেস্ট করতে বলা — তাতে secret, গ্রাহকের তথ্য বা সহকর্মীর email থাকতে পারে | শুধু দরকারি hunk পাঠান, গোপন অংশ মুছুন; ফাঁস হলে কী করবেন — পর্ব ৫৭ (Git-এ নিরাপত্তা) |
নিচের prompt-এ AI-কে script লিখতে বলা হয়েছে, কিন্তু Git-এর exit code-এর চুক্তি আর “শুধু একটি প্রশ্ন” নীতিসহ — আর কোনো command সুপারিশ করলে সেটি কী বদলায় তা বলে দেওয়ার শর্তে।
AI-এর দেওয়া debug-পরিকল্পনা চালানোর আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Bisect হাজার গুণ দ্রুত হলে সবাই সবসময় এটি ব্যবহার করে না কেন? কোন পরিস্থিতিতে bisect ভুল উত্তর দেয়?
Bisect-এর গতি আসে একটি অনুমান থেকে: history একটি সারি, আর bug একবার ঢুকলে প্রতিটি পরের commit-এ থাকে। এই একমুখিতা যেখানে সত্য, সেখানে ⌈log₂ n⌉ পরীক্ষাই যথেষ্ট — Linux kernel-এর মতো project-এ, যেখানে দুটি release-এর মাঝে হাজার হাজার commit, এটি দিন বাঁচায়। কারণেই kernel-এর নথিতে bug report-কারীদের জন্য bisect-এর আলাদা নির্দেশিকা আছে।
কিন্তু অনুমান অনেকভাবে ভাঙে। Flaky আচরণ: test কখনো পাস, কখনো ফেল — একটি ভালো commit একবার “খারাপ” বললে পুরো অর্ধেক ভুলভাবে বাদ যায়, আর শেষ উত্তর আত্মবিশ্বাসের সাথে ভুল। একাধিক কারণ: দুটি আলাদা commit একই লক্ষণ তৈরি করলে, বা একটি bug মাঝে ঠিক হয়ে আবার ফিরলে, “প্রথম খারাপ” আর একক থাকে না। পরীক্ষা করা যায় না এমন commit: অনেক WIP বা ভাঙা build থাকলে skip জমে, আর দোষী যদি ঠিক তাদের পাশে থাকে, Git শুধু বলতে পারে “এদের যেকোনোটি” — এই পর্বের simulator-এ প্রায় ১০% session এমন হয়েছে। পরিবেশ-নির্ভর bug: সমস্যা কোডে নয়, database-এর তথ্য, configuration বা বাইরের service-এ — তখন সব commit-ই খারাপ বা সব ভালো দেখায়।
আরেকটি বাস্তব বাধা খরচ: প্রতিটি পরীক্ষায় যদি এক ঘণ্টার build লাগে, ১২টি ধাপও একদিনের কাজ। আর পুরোনো commit-এ ফিরে গেলে প্রায়ই পুরোনো dependency আর পুরোনো tool লাগে, যা আর পাওয়া যায় না। এ কারণেই অনেক দল “প্রতিটি commit build হবে আর test পাস করবে” নিয়ম রাখে; ছোট, নিজে-সম্পূর্ণ commit (পর্ব ১৮) আর CI (পর্ব ৫৮) bisect-কে সম্ভব করে তোলে। আর Christian Couder-এর ২০০৯-এর লেখায় যেমন আলোচনা আছে, merge-ভরা DAG-এ Git সরল মাঝখান নয়, এমন commit বাছে যা সন্দেহভাজনদের প্রায় অর্ধেক করে — তবু graph যত জটিল, ধাপ তত বাড়তে পারে।
মূল উপলব্ধি: Bisect-এর শক্তি আসে শৃঙ্খলা থেকে — নির্ধারিত test, প্রতিটি commit build-যোগ্য, আর পরিষ্কার history. শৃঙ্খলা না থাকলে bisect দ্রুত ভুল উত্তরও দেয়।
প্র ০২“git blame” নামটি কি একটি ক্ষতিকর বার্তা দেয়? একটি দল কীভাবে blame-এর ব্যবহার আর blameless সংস্কৃতি একসাথে রাখতে পারে?
নামটির ইতিহাস পুরোনো। CVS আর Subversion-এর যুগেই “annotate” আর “blame” দুটি নাম চালু ছিল; Subversion-এ svn blame-এর বিকল্প নাম হিসেবে praise আর annotate-ও রাখা হয়েছে — যেন নির্মাতারাও নামটির কড়া স্বর নিয়ে সচেতন ছিলেন। Git-এ git blame আর git annotate দুটিই আছে। নাম যা-ই হোক, command-টির কাজ নিরপেক্ষ: একটি লাইনের শেষ পরিবর্তন কোন commit-এ।
সমস্যা ব্যবহারে। এই পর্বেই দেখেছেন, সাধারণ blame লাইন ১৩-কে formatting commit-এর লেখকের নামে দেখায়, অথচ তিনি শুধু space বসিয়েছেন। Function অন্য ফাইলে সরালে সরানো মানুষটি পুরো কোডের “মালিক” হয়ে যান। Bug-এর মূল কারণ প্রায়ই সেই লাইনে নয় — যে test লেখা হয়নি, যে review তাড়াহুড়োয় হয়েছে, যে requirement অস্পষ্ট ছিল। তাই blame-এর ফল থেকে সরাসরি “X দোষী” সিদ্ধান্ত প্রায়ই প্রযুক্তিগতভাবেই ভুল।
আর সামাজিক ক্ষতি আরও বড়। John Allspaw ২০১২ সালে Etsy-র engineering blog-এ “Blameless PostMortems and a Just Culture” লিখে যুক্তি দেন: মানুষ শাস্তির ভয় পেলে ঘটনার বিবরণ লুকান, আর প্রতিষ্ঠান সবচেয়ে মূল্যবান তথ্য হারায়। Google-এর SRE বইও (২০১৬) postmortem-কে দোষারোপহীন রাখার কথা বলে। এর মানে দায়িত্বহীনতা নয় — মানে প্রশ্নটি “কে?” থেকে “কোন ব্যবস্থা এটি সম্ভব করল?”-তে সরানো।
বাস্তবে দলের কয়েকটি নিয়ম কাজে আসে: blame ব্যবহার হবে প্রসঙ্গ খুঁজতে — “কাকে জিজ্ঞেস করলে কারণ জানা যাবে” — শাস্তি দিতে নয়; postmortem-এ মানুষের নাম নয়, commit আর ঘটনার ক্রম; formatting commit .git-blame-ignore-revs-এ; আর প্রতিটি bug-এর পরে প্রশ্ন “কোন test, review-নিয়ম বা tool এটি আগে ধরতে পারত?” Code review-এর মানবিক দিক পর্ব ৪৪-এ আর পেশাগত নৈতিকতা পর্ব ৬৪-এ।
মূল উপলব্ধি: Blame একটি দূরবীন, আদালত নয়। এটি দেখায় কোথায় তাকাতে হবে; কেন ঘটল, তার উত্তর মেলে commit-বার্তা, issue আর মানুষের সাথে খোলামেলা কথায়।
প্র ০৩একটি পুরোনো codebase-এ একবারে পুরোটায় formatter চালানো উচিত, নাকি ধীরে ধীরে, যে ফাইল ছোঁয়া হয় শুধু সেটিতে? History-র দিক থেকে লাভ-ক্ষতি কী?
Black, Prettier বা gofmt-এর মতো code formatter একটি বড় তর্ক শেষ করেছে: কোডের চেহারা নিয়ে review-এ আর সময় নষ্ট হয় না। কিন্তু চালু করার মুহূর্তে একটি প্রশ্ন আসে — পুরো codebase একবারে, নাকি ধীরে ধীরে?
একবারে (big bang): একটি বিশাল commit, হাজার হাজার লাইন বদল। লাভ: তারপর থেকে প্রতিটি diff পরিষ্কার, কারণ আর কখনো formatting-এর পরিবর্তন logic-এর সাথে মিশবে না। ক্ষতি: সেই মুহূর্তে খোলা থাকা প্রতিটি branch-এ বিশাল merge conflict (পর্ব ১৩); blame-এ হাজার লাইন এক commit-এর নামে — যা এই পর্বে দেখা .git-blame-ignore-revs প্রায় পুরোপুরি সারায়; আর git log -G ও log -L-এ সেই commit বারবার আসে। অনেক দল তাই একটি শান্ত সময় বাছে (বড় release-এর পরে), আগে থেকে জানায়, আর সবাইকে নিজের branch rebase বা merge করে নিতে বলে।
ধীরে ধীরে (যে ফাইল ছোঁয়া হয়): লাভ: কোনো একক বিস্ফোরণ নেই। ক্ষতি: বছরের পর বছর প্রতিটি সাধারণ পরিবর্তনের diff-এ formatting মিশে থাকে — reviewer-কে -w দিয়ে আসল পরিবর্তন খুঁজতে হয়, আর blame-এ formatting আর logic আলাদা করা যায় না, কারণ দুটি একই commit-এ। Ignore-revs এখানে কাজ করে না, কারণ commit-টি উপেক্ষা করলে আসল logic-এর পরিবর্তনও উপেক্ষিত হবে।
আরেকটি কৌশল: formatting নিজের আলাদা commit-এ, ফাইল-ভিত্তিক বা module-ভিত্তিক, প্রতিটির hash ignore তালিকায়। এতে conflict ছোট ছোট ভাগে আসে আর blame পরিষ্কার থাকে। যেটিই বাছুন, শর্ত একটি: formatting-এর commit-এ আচরণের কোনো পরিবর্তন নয়, আর test সবুজ। পর্ব ৪২-এ refactoring-কে ছোট নিরাপদ ধাপে রাখার যে নিয়ম, এটি তারই একটি রূপ।
মূল উপলব্ধি: চেহারার পরিবর্তন আর অর্থের পরিবর্তন কখনো এক commit-এ নয়। এই একটি নিয়ম মানলে blame, bisect আর review — তিনটিই সহজ থাকে।
প্র ০৪Squash merge, WIP commit আর বিশাল commit — commit-এর আকার আর ধরন bisect ও blame-কে কীভাবে প্রভাবিত করে?
Bisect-এর রেজোলিউশন commit-এর আকারের সমান: সে বলতে পারে কোন commit, তার ভেতরের কোন লাইন নয়। একটি commit-এ ১০ লাইন বদলালে দোষী খুঁজতে ১০ লাইন পড়তে হয়; ২,০০০ লাইন বদলালে bisect প্রায় কিছুই বলেনি। একইভাবে blame প্রতিটি লাইনের পাশে commit দেখায়, আর তার বার্তা যদি বলে “various fixes”, তবে “কেন” প্রশ্নের উত্তর হারিয়ে গেল।
Squash merge (অনেক platform-এ pull request শেষে সব commit এক করার বিকল্প, পর্ব ১৬) এখানে দুই দিকেই কাটে। লাভ: main-এ প্রতিটি commit একটি সম্পূর্ণ, পর্যালোচিত, build-যোগ্য একক — bisect-এর জন্য ভাঙা WIP commit থাকে না। ক্ষতি: একটি বড় feature-এর দশটি যুক্তিসঙ্গত ধাপ একটি বিশাল commit হয়ে যায়; bisect “এই feature-এ” পর্যন্ত বলতে পারে, তার বেশি নয়।
WIP commit — আমাদের commit ৯-এর মতো, যা import-ই হয় না — branch-এ কাজ সংরক্ষণের জন্য স্বাভাবিক, কিন্তু shared history-তে গেলে bisect-এ skip তৈরি করে। Interactive rebase (পর্ব ১৫) দিয়ে merge-এর আগে এগুলো গুছিয়ে নেওয়া একটি পরিচিত চর্চা। আরেকটি পথ --first-parent: merge commit রেখে দেওয়া, যাতে main-এর ধারায় প্রতিটি merge build-যোগ্য থাকে, আর দরকারে branch-এর ভেতরে নামা যায়।
কোনো একক উত্তর নেই — trunk-based দল ছোট ছোট commit সরাসরি main-এ দেয়, অন্যরা squash পছন্দ করে (পর্ব ১৭)। কিন্তু লক্ষ্য একই: merge commit থাকুক বা না থাকুক, shared history-র প্রতিটি commit যেন (১) build হয়, (২) test পাস করে, (৩) একটি উদ্দেশ্য বহন করে, আর (৪) একটি পরিষ্কার বার্তা রাখে। পর্ব ১৮-এর Conventional Commits এই শেষ দুটিকে কাঠামো দেয়।
মূল উপলব্ধি: ভালো history একটি debug-যন্ত্র, যা আজ লিখে ভবিষ্যতের নিজেকে উপহার দেন। প্রতিটি commit যত ছোট আর অর্থবহ, ভবিষ্যতের তদন্ত তত ছোট।
প্র ০৫Blame, log -S, log -L আর bisect — একটি নতুন bug-এ কোনটি দিয়ে শুরু করবেন? “Code archaeology”-র একটি কৌশল বর্ণনা করুন।
চারটি হাতিয়ার চারটি আলাদা প্রশ্নের উত্তর দেয়, তাই শুরু নির্ভর করে আপনি কী জানেন তার ওপর। আচরণ জানেন, কোড জানেন না (“ঠিক ১,০০০ টাকায় ছাড় আসে না”) — এবং একটি ভালো অবস্থা জানেন: bisect. এর জন্য কোড পড়তেই হয় না, শুধু একটি নির্ভরযোগ্য পরীক্ষা লাগে। সন্দেহের লেখা জানেন (“>= 1000 শর্তটা কোথায় গেল?”): log -S — প্রায়ই bisect-এর চেয়েও দ্রুত, কারণ কিছু চালাতে হয় না। সন্দেহের লাইন বা function আজও আছে: blame (শেষ পরিবর্তন) বা log -L (পুরো জীবনী)।
বাস্তবে এগুলো একটির পর একটি চলে। একটি সাধারণ কৌশল: (১) blame দিয়ে লাইনটির শেষ commit; সেটি formatting বা সরানো হলে -w -C বা ignore-revs. (২) পাওয়া commit-এ git show, আর বার্তায় issue-র নম্বর খোঁজা। (৩) এর আগের অবস্থা দেখতে git blame <commit>^ -- file — লাইনটির আগের রূপের মালিক; বারবার করলে লাইনটির পুরো বংশতালিকা (porcelain-এর previous লাইন ঠিক এই কাজটি সহজ করে)। (৪) কোনো লেখা মুছে গেছে মনে হলে log -S. (৫) আচরণগত প্রশ্নে চূড়ান্ত প্রমাণ bisect run.
Fowler-এর Refactoring বা Michael Feathers-এর Working Effectively with Legacy Code (২০০৪)-এর মতো বই পুরোনো কোডের সাথে কাজের যে শিক্ষা দেয়, তার একটি বড় অংশ এটি: পরিবর্তনের আগে বুঝুন কোডটি কেন এমন। “অদ্ভুত” লাইনটি হয়তো কোনো পুরোনো bug-এর সমাধান — log -L দিয়ে তার বার্তা পড়লে জানবেন, আর সেটি মুছে পুরোনো bug ফিরিয়ে আনবেন না। এর জন্য সবচেয়ে বড় শর্ত মানুষের তৈরি: অর্থবহ commit-বার্তা আর issue-র সাথে সংযোগ। Git-এর সব হাতিয়ার শুধু “কী” আর “কবে” বলতে পারে; “কেন” লেখা থাকলেই তবে পাওয়া যায়।
মূল উপলব্ধি: আচরণ থেকে শুরু হলে bisect, লেখা থেকে হলে pickaxe, লাইন থেকে হলে blame বা log -L — আর সবগুলোর শেষ গন্তব্য commit-বার্তার “কেন”।
অনুশীলন
একটি হিসাব, একটি আউটপুট অনুমান, একটি command-এর ধারা আর একটি নীতির সমালোচনা — প্রতিটির পূর্ণ উত্তর দেওয়া, আর Git-এর অংশগুলো Git ২.৫৩-এ চালিয়ে যাচাই করা।
-
Bisect-এর হিসাব: একটি project-এর release
v2.0ঠিক ছিল, আজকেরmainখারাপ;git rev-list --count v2.0..mainবলে ৩,০০০। প্রতিটি পরীক্ষায় build আর test মিলিয়ে ৬ মিনিট লাগে। (ক) Bisect-এ সর্বোচ্চ কয়টি পরীক্ষা লাগবে, আর মোট কত সময়? (খ) একে একে পুরোনো থেকে নতুনে পরীক্ষা করলে সবচেয়ে খারাপ ক্ষেত্রে কত? (গ) Git-এর প্রথম বার্তায় কী দেখার আশা করবেন? (ঘ) ২০টি পরীক্ষায় সর্বোচ্চ কতগুলো সন্দেহভাজন commit সামলানো যায়?(ক) সন্দেহভাজন ৩,০০০ (প্রথম খারাপ এর যেকোনোটি)। ২¹¹ = ২,০৪৮ < ৩,০০০ ≤ ৪,০৯৬ = ২¹², তাই ⌈log₂ 3000⌉ = ১২টি পরীক্ষা; ১২ × ৬ = ৭২ মিনিট. কয়েকটি commit build না হলে skip-এর জন্য কয়েকবার বেশি চালাতে হতে পারে।
(খ) সবচেয়ে খারাপ ক্ষেত্রে (দোষী শেষ commit) ২,৯৯৯টি পরীক্ষা — শেষটি জানা খারাপ, তাই সেটি বাদ: ২,৯৯৯ × ৬ = ১৭,৯৯৪ মিনিট, প্রায় ৩০০ ঘণ্টা; গড়ে এর প্রায় অর্ধেক।
(গ) একই আকারের একটি সরল repository-তে চালিয়ে পাওয়া:
$ git bisect start main main~3000 Bisecting: 1499 revisions left to test after this (roughly 11 steps)“এই পরীক্ষার পরে আরও প্রায় ১১টি” — অর্থাৎ মোট ১২, (ক)-এর সাথে মেলে।
(ঘ) k টি পরীক্ষায় 2kটি সম্ভাবনা আলাদা করা যায়: ২²⁰ = ১০,৪৮,৫৭৬টি commit. প্রতিটি অতিরিক্ত পরীক্ষা সামলানোর ক্ষমতা দ্বিগুণ করে।
-
আউটপুট অনুমান (
-Sবনাম-G): একটি repository-র যেসব commit-এ “0.95” লেখাটি ছুঁয়েছে:86bd46b—bill.py-তে নতুন লাইনreturn amount*0.95যোগ;87f8469— সেই লাইন বদলেreturn amount * 0.95(শুধু space);1853d66— পুরো functionbill.pyথেকে মুছে নতুন ফাইলdiscount.py-তে বসানো। অন্য কোনো commit “0.95” ছোঁয়নি।git log --oneline -S'0.95'আরgit log --oneline -G'0\.95'কোন কোন commit দেখাবে (নতুন ওপরে)?$ git log --oneline -S'0.95' 1853d66 Move eid discount into discount.py 86bd46b Add eid discount function $ git log --oneline -G'0\.95' 1853d66 Move eid discount into discount.py 87f8469 Format code: spaces around operators, double quotes 86bd46b Add eid discount function86bd46b:bill.py-তে সংখ্যা ০ → ১, আর যোগ হওয়া লাইনে মেলে — দুটিতেই।87f8469: সংখ্যা ১ → ১, তাই-S-এ নেই; কিন্তু diff-এ-আর+দুটি লাইনেই “0.95”, তাই-G-তে আছে।1853d66:bill.py-তে ১ → ০ আরdiscount.py-তে ০ → ১ — প্রতিটি ফাইলে আলাদা করে গোনা হয়, তাই-S-এও আছে।-G-তে regex-এ\.লেখা হয়েছে, কারণ শুধু.মানে “যেকোনো অক্ষর”। -
Command-এর ধারা — ভুল রায় সারানো:
git bisect start HEAD v1.0চালিয়ে একজন হাতে রায় দিচ্ছিলেন: প্রথম checkout7331e00-এbad, তারপরa2aba58-এskip, তারপর3765778-এ test আসলে পাস করেছিল, কিন্তু ভুল করেgit bisect badটাইপ করলেন; পরের392acc2-তে ঠিকভাবেgood. Git বলল:There are only 'skip'ped commits left to test.(ক) কেন এই বার্তা? (খ) পুরো session নতুন করে না চালিয়ে কোন command-এর ধারায় সারাবেন? (গ) এমন ভুল ভবিষ্যতে কীভাবে এড়াবেন?(ক) আসল বার্তা:
$ git bisect good There are only 'skip'ped commits left to test. The first bad commit could be any of: a2aba5872252155ccead4e4e1a8e6301a597184d 376577801e539bcc8b96af38db80a06871369509 We cannot bisect more!ভুল রায়ে Git মেনে নিয়েছে যে commit ১০ (
3765778) খারাপ। তারপর commit ৮ (392acc2) ভালো। মাঝে শুধু commit ৯ — যেটি skip করা — আর ১০। তাই “প্রথম খারাপ ৯ না ১০, বলা যায় না”। আসল দোষী commit ১২ তালিকাতেই নেই — একটি ভুল রায় খোঁজকে সম্পূর্ণ ভুল জায়গায় নিয়ে গেছে।(খ)
$ git bisect log > ../fix.log $ grep -v 3765778 ../fix.log > ../fixed.log $ git bisect reset Previous HEAD position was 392acc2 Add usage notes to README Switched to branch 'main' $ git bisect replay ../fixed.log Bisecting: 6 revisions left to test after this (roughly 3 steps) [7331e0026c8ab498791f4f3f8c62dfd007b1da66] Add delivery charge Bisecting: 2 revisions left to test after this (roughly 1 step) [376577801e539bcc8b96af38db80a06871369509] Finish receipt printing $ python ../check.py total for 1000 taka: 950.0 $ git bisect good Bisecting: 0 revisions left to test after this (roughly 1 step) …খাতা থেকে ভুল লাইনটি (
3765778-এর রায়) বাদ দিয়ে replay করলে Git ঠিক সেই জায়গায় ফেরে, আর আবার3765778পরীক্ষা করতে দেয়। এবারgoodদিলে খোঁজ commit ১২ আর ১১ হয়ে70c0892-এ শেষ হয় (চাইলে এখান থেকেgit bisect run python ../check.py). এখানেgrep -vসেই hash-ওয়ালা মন্তব্যের লাইনগুলোও মুছেছে — ক্ষতি নেই, মন্তব্য replay-তে চলে না। Editor-এ খুলে হাতে মুছলেও একই।(গ) হাতে রায় না দিয়ে
git bisect run; আর রায়ের আগেgit bisect visualize --onelineদিয়ে বাকি সন্দেহভাজন দেখে নেওয়া — ফল অবাক করলে থামুন। -
নীতির সমালোচনা: একটি কাল্পনিক দলের নিয়ম: (১) প্রতিটি production bug-এর পরে postmortem-এ
git blame-এর ফল থেকে “দায়ী developer”-এর নাম লেখা হবে, আর মাসে তিনবার নাম এলে সতর্কবার্তা। (২) গত মাসে পুরো codebase-এ formatter চালানো হয়েছে, কিন্তু সেটি অন্য পরিবর্তনের সাথে একই commit-এ। (৩) Bisect-এর জন্য দলের মানক script:git bisect run pytest— পুরো test suite. কোথায় কোথায় সমস্যা, আর কী প্রস্তাব করবেন?- (১) প্রযুক্তিগতভাবে ভুল, সামাজিকভাবে ক্ষতিকর: blame শেষ পরিবর্তন দেখায় — formatting বা কোড সরানোর মানুষ “দায়ী” হয়ে যাবেন। আর শাস্তির ভয়ে মানুষ ছোট, ঘন ঘন commit এড়াবেন, ঝুঁকিপূর্ণ refactoring থেকে দূরে থাকবেন, ঘটনার তথ্য লুকাবেন। প্রস্তাব: blameless postmortem — ঘটনার সময়রেখা, commit-এর উল্লেখ (মানুষ নয়), আর “কোন test, review বা tool এটি আগে ধরতে পারত” প্রশ্নে কাজের তালিকা।
- (২) Formatting আর logic মিশে গেছে: এখন ignore-revs দিলে সেই commit-এর আসল পরিবর্তনও উপেক্ষিত হবে, না দিলে blame প্রায় পুরোটাই এক commit-এ। ক্ষতি পুরোপুরি সারানো যায় না; সামনে থেকে নিয়ম: formatting আলাদা commit-এ, বার্তায় স্পষ্ট, পরের commit-এ তার পুরো hash
.git-blame-ignore-revs-এ, আর সবার জন্যblame.ignoreRevsFileconfig. বিদ্যমান ক্ষতিতে blame-এর সাথে-wআর প্রয়োজনেlog -L/log -S. - (৩) পুরো suite আর কাঁচা exit code: সম্পর্কহীন একটি flaky test ফেল করলেই commit “খারাপ” — খোঁজ ভুল পথে যায়, আর পুরো suite প্রতিটি ধাপকে ধীর করে। pytest-এর exit code সরাসরি গেলে “কোনো test পাওয়া যায়নি” (৫) বা usage error (৪) — যা পুরোনো commit-এ test ফাইল না থাকলে হয় — “খারাপ” ধরা হবে; অথচ এগুলো হওয়া উচিত skip (125). আর script repository-র ভেতরের test ব্যবহার করলে পুরোনো commit-এ পুরোনো test চলে, নতুন bug-এর test নয়। প্রস্তাব: repository-র বাইরে রাখা একটি ছোট script, যা শুধু ঐ bug-এর test চালায় আর code রূপান্তর করে: পাস = 0, ঐ test ফেল = 1, বাকি সব (build ব্যর্থ, ফাইল নেই, usage error) = 125.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি regression (আগে ঠিক ছিল, এখন ভাঙা) খোঁজা আর সারানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Bug-টিকে একটি ছোট স্বয়ংক্রিয় test-এ রূপ দেওয়া, যা 0 বা 1 দিয়ে শেষ হয়
git bisect start <খারাপ> <ভালো>দিয়ে দুই প্রান্ত জানানোgit bisect runদিয়ে প্রথম খারাপ commit খুঁজে বের করা, তারপরgit bisect resetদিয়ে আগের branch-এ ফেরাgit show, blame আর commit-বার্তা থেকে কী বদলেছিল ও কেন, তা বোঝা- সমাধান করে test-টিকে স্থায়ী regression test হিসেবে যোগ করা
একটি ভালো আর একটি খারাপ commit-এর মাঝে মোট ৫০০টি সন্দেহভাজন commit (প্রথম খারাপ commit এদের যেকোনোটি হতে পারে)। প্রতিটি commit পরীক্ষাযোগ্য আর bug একবার ঢুকলে থেকে যায়। git bisect-এ সর্বোচ্চ কয়টি পরীক্ষা লাগবে?
- ৮
- ৯
- ২৫০
- ৫০০
git bisect run ./check.sh চলার সময় একটি commit-এ check.sh exit code 125 দিয়ে শেষ হলো। Git কী করবে?
- Commit-টিকে খারাপ (bad) ধরবে
- Commit-টিকে ভালো (good) ধরবে
- Commit-টি skip করে কাছের অন্য একটি commit পরীক্ষা করবে
- পুরো bisect session বন্ধ করবে
একটি commit শুধু একটি function-কে একই ফাইলের নিচ থেকে ওপরে সরিয়েছে; লেখায় কোনো পরিবর্তন নেই। git log -S'def format_taka' আর git log -G'def format_taka'-এর মধ্যে কোনটি এই commit দেখাবে?
- শুধু
-S - কোনোটিই না
- দুটিই
- শুধু
-G
একটি দল পুরো codebase-এ formatter চালিয়েছে (space, উদ্ধৃতিচিহ্ন আর লাইন ভাঙা বদলেছে), আর commit-টিতে অন্য কোনো পরিবর্তন নেই। এখন git blame প্রায় সব লাইন সেই commit-এর নামে দেখায়। পুরো দলের জন্য স্থায়ী, সবচেয়ে উপযুক্ত সমাধান কোনটি?
- Commit-টির পুরো ৪০ অক্ষরের hash
.git-blame-ignore-revsফাইলে রেখে commit করা, আরblame.ignoreRevsFileconfig ব্যবহার - সবাইকে সবসময়
git blame -wচালাতে বলা - Interactive rebase দিয়ে commit-টি history থেকে মুছে ফেলা
- Commit-টির ৭ অক্ষরের ছোট hash
.git-blame-ignore-revs-এ রাখা
একটি commit-এ eid_discount function bill.py থেকে মুছে হুবহু নতুন ফাইল discount.py-তে বসানো হয়েছে, আর bill.py-তে একটি import যোগ হয়েছে। discount.py-এর প্রতিটি লাইন আসলে কোন commit-এ লেখা হয়েছিল দেখতে কোনটি?
git log --follow -- discount.pygit blame -M discount.pygit blame -C discount.pygit blame -w discount.py
git blame দেখাচ্ছে, একটি ভুল হিসাবের লাইন শেষবার commit 87f8469-এ বদলেছে, যার লেখক Rahim. এ থেকে নিশ্চিতভাবে কী বলা যায়?
- Rahim ভুলটি ঢুকিয়েছেন
- লাইনটির বর্তমান রূপ শেষবার
87f8469-এ এসেছে; bug সেখানে ঢুকেছে কি না, তা আলাদা করে যাচাই করতে হবে - ঐ commit-এর আগে লাইনটি সঠিক ছিল
- লাইনটি ফাইল তৈরির প্রথম commit থেকে আছে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Bisect = history-র ওপর binary search: n টি সন্দেহভাজনে ⌈log₂ n⌉টি পরীক্ষা; শর্ত — bug একবার ঢুকলে থেকে যায়, আর পরীক্ষা নির্ধারিত। শেষে সবসময়
git bisect reset. bisect run-এর চুক্তি: 0 ভালো, 1–127 (125 বাদে) খারাপ, 125 skip, 128+ থামা। Script repository-র বাইরে, শুধু একটি প্রশ্ন।- ভুল রায় দিলে:
git bisect log→ ভুল লাইন মুছুন →reset→replay. - Blame = শেষ পরিবর্তন: formatting পেরোতে
-wবা.git-blame-ignore-revs(পুরো hash), সরানো কোডে-M/-C; দোষ নয়, প্রসঙ্গ খুঁজুন। -Sগোনে,-Gমেলায়: সরানো বা চেহারা-বদলানো লাইন শুধু-G-তে; function-এর জীবনীgit log -L :func:file, পুরোনো snapshot-এ খোঁজgit grep <rev>.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৬ · Git-এর শক্তিশালী হাতিয়ার — stash, cherry-pick, worktree, sparse checkout আর hookপরবর্তী পর্বকাজ সরিয়ে রাখা, commit বেছে তোলা আর পাশাপাশি checkout.
- পর্ব ৫৪ · Merge-এর ভেতরে — merge base, ort strategy, diff3 আর rerereআগের পর্বMerge-ভরা DAG — যেখানে bisect আর
--first-parentকাজ করে। - সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- DSA · Linear আর binary searchগভীরে যানBisect-এর পেছনের binary search, log₂ n-এর প্রমাণসহ।
- অপারেটিং সিস্টেম · Signal — প্রসেসকে বার্তা পাঠানো আর সামলানোপাশের ট্র্যাকExit code 128 + signal কোথা থেকে আসে, যেমন crash-এ 139.