এই পর্বে যা শিখবেন
- Stash-এর প্রতিটি রূপ —
-m,-u,-a,--staged,-p— আরlist,show,apply,pop,drop,branch; stash ফিরিয়ে আনতে conflict হলে কী করবেন - একটি fix release branch-এ cherry-pick করা,
-x,-n,-m, range-এর নিয়ম, conflict-এ--continue/--abort/--skip, আর duplicate commit চেনা - Worktree দিয়ে একসাথে কয়েকটি branch-এ কাজ, আর sparse checkout দিয়ে বড় repository-র শুধু দরকারি অংশ খোলা
- Client-side আর server-side hook-এর input, argument আর exit code; নিজে একটি commit-msg hook লেখা, দলে ভাগ করা, আর কেন CI-কে আবার যাচাই করতে হয়
- ছোট কিন্তু কাজের হাতিয়ার —
git notes,git bundle,git archive,git clean -i,git rev-list --count
১ · Stash — অর্ধেক কাজ তাকে তুলে রাখা
আপনি একটি কাল্পনিক অনলাইন বাজার অ্যাপের feature/discount branch-এ ছাড়ের badge বানাচ্ছেন। কোড অর্ধেক লেখা, একটি নতুন CSS file এখনো Git-এ যোগই করেননি। এমন সময় খবর এলো: production-এ মোট দামের দশমিক ভুল দেখাচ্ছে, এখনই ঠিক করতে হবে। অর্ধেক কাজ commit করতে মন চায় না — আবার ফেলে দেওয়াও যায় না। পর্ব ১২-এ এই সমস্যার এক লাইনের উত্তর দেখেছিলেন: git stash, পরে git stash pop. এই পর্বে তার পুরো ব্যবহার, আর তার সাথে Git-এর আরও কয়েকটি “দৈনন্দিন কিন্তু কম জানা” হাতিয়ার।
দর্জির দোকানের কথা ভাবুন। ওস্তাদ একটি পাঞ্জাবির হাতা সেলাই করছেন, এমন সময় এক খদ্দের এসে বললেন, “ভাই, আমার প্যান্টের বোতামটা এখনই লাগিয়ে দিন, বাস ছেড়ে দেবে।” ওস্তাদ অর্ধেক সেলাই করা পাঞ্জাবিটা সুঁই-সুতোসহ ভাঁজ করে তাকে রাখলেন, ওপরে একটি চিরকুট: “হাতা, অর্ধেক”। টেবিল এখন খালি; বোতাম লাগানো শেষ হলে তাক থেকে সবার ওপরের ভাঁজটি নামিয়ে আবার শুরু। তাকে একাধিক ভাঁজ থাকলে সবচেয়ে নতুনটি সবার ওপরে। Stash ঠিক এই তাক: একটি stack, যার ওপরেরটির নাম stash@{0}, তার নিচেরটি stash@{1}, আর চিরকুটটি -m-এর বার্তা।
নিচের transcript-এ দুটি stash তৈরি হচ্ছে: প্রথমে README-তে একটি পরীক্ষামূলক লাইন, তারপর আসল অর্ধেক কাজ — app.js-এ বদল আর একটি নতুন, untracked file badge.css. এই পর্বের সব transcript Git ২.৫৩-এ (Windows) আলাদা scratch repository-তে সত্যিই চালানো; লেখক Rahim, তারিখ নির্দিষ্ট (১৫ জানুয়ারি ২০২৬, সকাল ১০টা থেকে প্রতি ধাপে এক মিনিট করে), core.autocrlf=false, তাই আপনি একই ধাপ একই সময় দিয়ে চালালে একই hash পাবেন। # … দিয়ে শুরু লাইনগুলো ব্যাখ্যা, command নয়।
$ echo 'Run with: node app.js' >> README.md
$ git stash push -m "try: shorter README"
Saved working directory and index state On feature/discount: try: shorter README
$ # edit app.js (label now calls badge()) and create badge.css
$ git status --short
M app.js
?? badge.css
$ git stash push -u -m "discount: half-done badge"
Saved working directory and index state On feature/discount: discount: half-done badge
$ git status --short
$ git stash list
stash@{0}: On feature/discount: discount: half-done badge
stash@{1}: On feature/discount: try: shorter README
$ git stash show -p stash@{1}
diff --git a/README.md b/README.md
index 28b9074..329eaa6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
# Shop
A tiny cart demo.
+Run with: node app.js
$ git stash show --include-untracked --stat stash@{0}
app.js | 2 +-
badge.css | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)git stash push (Git ২.১৩, ২০১৭ থেকে; পুরোনো git stash save এখন deprecated) working tree আর index দুটিকেই শেষ commit-এর অবস্থায় ফিরিয়ে দেয়। -m না দিলে বার্তা হয় “WIP on feature/discount: bbd74cc feat: add cart total” ধাঁচের, যা পাঁচটি stash জমলে কোনো কাজে আসে না। -u (--include-untracked) না দিলে badge.css folder-এই থেকে যেত — ডিফল্টে stash শুধু tracked file নেয়। দ্বিতীয় push-এর পরে git status --short একেবারে ফাঁকা, আর তালিকায় নতুনটি stash@{0}, পুরোনোটি নিচে নেমে stash@{1}. git stash show ডিফল্টে শুধু সংক্ষেপ (--stat) দেখায়; -p দিলে পুরো diff. Untracked অংশ দেখতে --include-untracked লাগে (এই সুবিধা Git ২.৩২-এর দিকে যোগ হয়েছে বলে release note-এ আছে)।
কোন flag কী তুলে রাখে
| Command | কী stash হয় | কখন কাজে লাগে |
|---|---|---|
git stash push -m "…" | Tracked file-এর staged আর unstaged সব বদল | সাধারণ ব্যবহার; বার্তা সবসময় দিন |
-u / --include-untracked | ওপরেরগুলো + untracked file | নতুন file তৈরি করেছেন কিন্তু add করেননি |
-a / --all | ওপরেরগুলো + ignored file-ও (যেমন build/, .env) | খুব কম; বড় node_modules-ও ঢুকে যেতে পারে |
--staged | শুধু index-এ তোলা বদল; বাকিটা folder-এ থাকে | Git ২.৩৫ (২০২২) থেকে, পুরোনো Git-এ নেই; staged অংশ আলাদা রাখতে |
-p / --patch | যে hunk-গুলো আপনি এক এক করে y বলে বেছে নেন | একই file-এ দুই রকম কাজ মিশে গেছে |
git stash push -- app.js | শুধু নির্দিষ্ট path-এর বদল | একটি file আলাদা করতে |
-k / --keep-index | সব stash হয়, কিন্তু staged অংশ index-এ রেখেও দেওয়া হয় | শুধু staged অংশ দিয়ে test চালিয়ে দেখতে |
--staged আসলে কী করে, তা একটি ছোট transcript-এ দেখুন। app.js-এর বদল staged, badge.css-এর বদল staged নয়:
$ # stage a new constant in app.js; also edit badge.css (not staged)
$ git status --short
M app.js
M badge.css
$ git stash push --staged -m "shipping constant only"
Saved working directory and index state On feature/discount: shipping constant only
$ git status --short
M badge.css
$ git stash show --stat
app.js | 1 +
1 file changed, 1 insertion(+)git status --short-এর প্রথম কলাম index, দ্বিতীয়টি working tree (পর্ব ৯)। app.js-এর M প্রথম কলামে, মানে staged; badge.css-এর M দ্বিতীয় কলামে, মানে শুধু folder-এ। --staged-এর পরে শুধু staged অংশটি stash-এ গেছে — stash show-এ একটিমাত্র file, app.js; badge.css-এর বদল যেমন ছিল তেমনই আছে। পুরোনো Git-এ একই কাজ করতে ঘুরপথ লাগত, যেমন --keep-index দিয়ে উল্টো দিক থেকে। আপনার দলের কারও Git ২.৩৫-এর আগের হলে এই flag সেখানে কাজ করবে না — git --version দেখে নিন।
Stash ভেতরে আসলে commit
Stash কোনো আলাদা জাদুর বাক্স নয়। প্রতিটি entry Git-এর সাধারণ commit object, যা refs/stash নামের একটি ref-এ ঝোলে; আর stash@{1} লেখাটি আসলে reflog-এর syntax — “refs/stash একধাপ আগে কোথায় ছিল”। -u দিয়ে তৈরি entry-টি খুলে দেখুন:
$ git cat-file -p stash@{0}
tree a8eacb18f7568732d455342921064720cfb00be5
parent bbd74cc27a827f93c233a4e7b5d0b1c743e633b5
parent bfbb63a326954a46de41ffd1ac269818dca94643
parent 0fa2871bb00fc030d59ff87b3c191ca0ab5c2e8d
author Rahim <rahim@example.com> 1768449900 +0600
committer Rahim <rahim@example.com> 1768449900 +0600
On feature/discount: discount: half-done badge
$ git log --format='%h %s' --no-walk 'stash@{0}^1' 'stash@{0}^2' 'stash@{0}^3'
bfbb63a index on feature/discount: bbd74cc feat: add cart total
0fa2871 untracked files on feature/discount: bbd74cc feat: add cart total
bbd74cc feat: add cart totalএকটি commit-এর তিনটি parent — এটি merge commit-এর মতো দেখতে, যদিও কোনো merge হয়নি। প্রথাগত নাম: stash commit-টিকে বলা হয় W (working tree-র অবস্থা), প্রথম parent bbd74cc হলো তখনকার HEAD, দ্বিতীয় parent I (bfbb63a, “index on …”) হলো index-এর অবস্থা, আর তৃতীয় parent U (0fa2871, “untracked files on …”) শুধু -u বা -a দিলে তৈরি হয়, তাতে untracked file-গুলো। তাই git stash apply --index চাইলে staged অবস্থাও হুবহু ফেরাতে পারে — I commit-এ সেটা লেখা আছে। এই commit, tree আর blob-এর গঠন বিস্তারিত পর্ব ৫২-এ (Git-এর ভেতরে), আর ভুল করে drop বা git stash clear করা stash খুঁজে আনা পর্ব ৫৩-এ (Ref, reflog আর উদ্ধার)।
২ · Stash ফিরিয়ে আনা — apply, pop, drop, branch আর conflict
তুলে রাখার চেয়ে ফিরিয়ে আনাতেই ভুল বেশি হয়। চারটি command মনে রাখুন:
git stash apply [stash@{n}]
বদলগুলো বর্তমান working tree-তে বসায়, কিন্তু entry তালিকায় থেকে যায়। একই বদল কয়েকটি branch-এ বসাতে চাইলে নিরাপদ।
entry থাকেgit stash pop [stash@{n}]
Apply করে, আর সফল হলে entry মুছে দেয়। Conflict হলে মোছে না — সমাধানের পরে নিজে drop করতে হয়।
git stash drop [stash@{n}]
একটি entry মুছে দেয়, কিছু বসায় না। git stash clear সব মুছে দেয় — দুটিই কোনো প্রশ্ন না করে।
git stash branch <নাম> [stash@{n}]
Stash যে commit-এর ওপর তৈরি হয়েছিল, সেখান থেকে নতুন branch খুলে stash বসায় আর সফল হলে entry মোছে। পুরোনো stash নতুন কোডে না মিললে সবচেয়ে নিরাপদ পথ।
conflict এড়ানোএবার আসল ঘটনা। Badge-এর কাজ stash করে main-এ hotfix করা হলো; hotfix label()-এর ঠিক সেই লাইনটি বদলেছে, যেটি stash-এর ভেতরের অর্ধেক কাজও বদলেছিল। Feature branch-এ ফিরে hotfix-টি নিয়ে আসার পরে pop:
$ git switch main
Switched to branch 'main'
$ # hotfix: label now uses t.toFixed(2)
$ git commit -q -am "fix(cart): show total with 2 decimals"
$ git switch feature/discount
Switched to branch 'feature/discount'
$ git merge -q --ff-only main
$ git stash pop
Auto-merging app.js
CONFLICT (content): Merge conflict in app.js
On branch feature/discount
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: app.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
badge.css
no changes added to commit (use "git add" and/or "git commit -a")
The stash entry is kept in case you need it again.
$ git status --short
UU app.js
?? badge.css
$ git stash list
stash@{0}: On feature/discount: discount: half-done badge
stash@{1}: On feature/discount: try: shorter README
$ cat app.js
const VAT = 0.15;
function total(items) {
return items.reduce((s, i) => s + i.price, 0);
}
function label(t) {
<<<<<<< Updated upstream
return "Total: " + t.toFixed(2);
=======
return "Total: " + t + badge();
>>>>>>> Stashed changes
}
$ # resolve by hand: keep both changes
$ grep -n Total app.js
6: return "Total: " + t.toFixed(2) + badge();
$ git add app.js
$ git stash drop
Dropped refs/stash@{0} (e8c32b8d2886f481bff770aaf6fc93f292c62aae)
$ git stash list
stash@{0}: On feature/discount: try: shorter READMEStash ফিরিয়ে আনা আসলে একটি three-way merge: base হলো stash-এর সময়ের HEAD (bbd74cc), এক দিকে বর্তমান HEAD-এর লেখা (hotfix-সহ), অন্য দিকে stash-এর লেখা। দুই দিকেই একই লাইন আলাদাভাবে বদলেছে, তাই conflict. Conflict marker-এর লেবেল লক্ষ করুন: “Updated upstream” মানে বর্তমান HEAD-এর দিক, “Stashed changes” মানে stash-এর দিক। শেষ লাইনটি সবচেয়ে জরুরি: The stash entry is kept in case you need it again — তাই stash list-এ এখনো দুটি entry. সমাধানের পরে git add app.js শুধু conflict-চিহ্ন সরায় (এখানে commit করার দরকার নেই, কাজ তো এখনো অর্ধেক), তারপর নিজে git stash drop. Untracked badge.css conflict ছাড়াই ফিরে এসেছে। git restore --staged app.js দিলে বদলটি index থেকে নামিয়ে আবার শুধু folder-এ রাখা যায়।
এই পুরো ঘটনাটি ধাপে ধাপে, প্রতিটি ধাপে branch, working tree আর stash-এর তালিকাসহ:
| Command | |
| HEAD | |
| Working tree | |
| Stash list |
- মাঝপথে।
app.jsবদলানো (M), নতুনbadge.cssএখনো untracked (??). তালিকায় আগের একটি পরীক্ষামূলক stash আছে। - তুলে রাখা।
-uথাকায় untracked file-ও গেল; folder এখন শেষ commitbbd74cc-এর মতো পরিষ্কার। নতুন entry সবার ওপরে,stash@{0}; পুরোনোটি নেমেstash@{1}. - Branch বদল। পরিষ্কার folder-এ switch নির্বিঘ্ন। Stash কোনো branch-এর সম্পত্তি নয় — যেকোনো branch থেকে তালিকা একই দেখায়।
- Hotfix commit.
mainএগিয়ে16e6075-এ। এই commitlabel()-এর লাইনটি বদলেছে — stash-এর ভেতরের বদলও ঠিক সেই লাইনে। - ফিরে আসা। Feature branch-এ ফিরে
--ff-onlymerge-এ hotfix আনা হলো, যাতে অর্ধেক কাজ সর্বশেষ কোডের ওপর বসে। Branch-এর নিজের কোনো commit ছিল না, তাই fast-forward. - Pop-এ conflict. Base
bbd74cc, এক দিকেtoFixed(2), অন্য দিকেbadge()— একই লাইন। Git marker বসিয়ে থেমেছে;badge.cssফিরেছে। Pop অসম্পূর্ণ, তাই entry মোছা হয়নি। - সমাধান আর পরিষ্কার। দুই বদল মিলিয়ে
toFixed(2) + badge();git addconflict-এর চিহ্ন সরায়; এবার নিজেdrop. তালিকায় শুধু পুরোনো README-র পরীক্ষাটি।
সেই পুরোনো README-র পরীক্ষাটি এখন অনেক পেছনের commit-এর ওপর তৈরি। সেটি সরাসরি বর্তমান branch-এ না বসিয়ে নিজের branch-এ নেওয়া যায়:
$ git add badge.css
$ git commit -q -m "feat(cart): add discount badge"
$ git stash branch try/readme stash@{0}
Switched to a new branch 'try/readme'
On branch try/readme
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
no changes added to commit (use "git add" and/or "git commit -a")
Dropped stash@{0} (925d620bc246ba19018a0b0aaf6901747483d5e8)
$ git log --oneline --decorate --all
9d0acd4 (feature/discount) feat(cart): add discount badge
16e6075 (main) fix(cart): show total with 2 decimals
bbd74cc (HEAD -> try/readme) feat: add cart totalপ্রথমে অর্ধেক কাজ commit করে folder পরিষ্কার করা হলো; না করলে stash branch “Your local changes … would be overwritten by checkout” বলে থেমে যায়। তারপর নতুন branch try/readme খোলা হলো bbd74cc-এ — stash যে commit-এর ওপর তৈরি, সেখানে — তাই conflict হওয়ার সুযোগই নেই। বন্ধনীর hash-টিই সেই stash commit — ভুল করে মুছলে এটি দিয়েই ফেরানো যায় (git stash apply <hash>), যতক্ষণ garbage collection তাকে মুছে না ফেলে।
Stash-কে একটি stack হিসেবে ছোট একটি model-এ চালিয়ে দেখুন। প্রতিটি file এখানে একটিমাত্র “লাইন”; আসল Git লাইন ধরে merge করে, তাই আলাদা লাইনের বদল সেখানে conflict ছাড়াই মিলে যেত।
// Stash as a stack. Each entry keeps, per file, the HEAD text it started from
// (base) and the edited text. A file is one "line" here; Git merges line by line.
let HEAD = { app: "total", readme: "intro" };
let wt = { ...HEAD };
const stash = []; // stash[0] is stash@{0}
const show = (what) => console.log(what.padEnd(26) + "wt=" + JSON.stringify(wt) +
" list=[" + stash.map((e, i) => i + ":" + e.msg).join(", ") + "]");
function push(msg) {
const ch = {};
for (const f in wt) if (wt[f] !== HEAD[f]) ch[f] = { base: HEAD[f], text: wt[f] };
stash.unshift({ msg, ch }); wt = { ...HEAD }; show("push " + msg);
}
function apply(n, drop = false) {
const e = stash[n], clash = [];
for (const f in e.ch) {
const { base, text } = e.ch[f];
if (wt[f] === base) wt[f] = text; // only the stash changed it
else if (wt[f] !== text) { clash.push(f); wt[f] = `<<<${wt[f]}|${text}>>>`; }
}
if (clash.length) return show("CONFLICT " + clash + ", kept");
if (drop) stash.splice(n, 1);
show((drop ? "pop" : "apply") + ` stash@{${n}}`);
}
const drop = (n) => { stash.splice(n, 1); show(`drop stash@{${n}}`); };
show("start");
wt.readme = "intro+run"; push("try-readme");
wt.app = "total+badge"; push("badge");
apply(1); // README back, entry stays in the list
wt = { ...HEAD }; show("git restore .");
apply(0, true); // pop: badge back, entry removed
push("badge"); // stash it again for the hotfix
HEAD.app = "total.toFixed"; wt = { ...HEAD }; show("hotfix commit on HEAD");
apply(0, true); // pop now clashes with the hotfix
wt.app = "total.toFixed+badge"; show("resolve by hand");
drop(0); // a conflicted pop never drops; do itআউটপুট:
start wt={"app":"total","readme":"intro"} list=[]
push try-readme wt={"app":"total","readme":"intro"} list=[0:try-readme]
push badge wt={"app":"total","readme":"intro"} list=[0:badge, 1:try-readme]
apply stash@{1} wt={"app":"total","readme":"intro+run"} list=[0:badge, 1:try-readme]
git restore . wt={"app":"total","readme":"intro"} list=[0:badge, 1:try-readme]
pop stash@{0} wt={"app":"total+badge","readme":"intro"} list=[0:try-readme]
push badge wt={"app":"total","readme":"intro"} list=[0:badge, 1:try-readme]
hotfix commit on HEAD wt={"app":"total.toFixed","readme":"intro"} list=[0:badge, 1:try-readme]
CONFLICT app, kept wt={"app":"<<<total.toFixed|total+badge>>>","readme":"intro"} list=[0:badge, 1:try-readme]
resolve by hand wt={"app":"total.toFixed+badge","readme":"intro"} list=[0:badge, 1:try-readme]
drop stash@{0} wt={"app":"total.toFixed+badge","readme":"intro"} list=[0:try-readme]
চতুর্থ লাইনে apply stash@{1} README ফিরিয়েছে কিন্তু তালিকা বদলায়নি — দুটি entry-ই আছে। ষষ্ঠ লাইনে pop badge ফিরিয়েছে আর তালিকা থেকে badge সরে গিয়ে try-readme উঠে এসেছে 0-তে — stack-এর নম্বর সবসময় ওপর থেকে গোনা, তাই একটি entry গেলে নিচেরগুলোর নম্বর বদলায়। এজন্য script বা লম্বা কাজে stash@{2} লিখে রাখা বিপজ্জনক; stash list আবার দেখে নিন। শেষ অংশে HEAD-এর app বদলে গেছে, তাই pop-এর হিসাবে base (total), HEAD (total.toFixed) আর stash (total+badge) তিনটিই আলাদা — conflict, entry রাখা হলো, ঠিক Git-এর মতো।
Stash স্থানীয় — git push এটি কোথাও পাঠায় না, আর অন্য কম্পিউটারে বা নতুন clone-এ এটি থাকে না। কয়েক দিনের বেশি রাখার কাজ stash-এ নয়, একটি WIP branch-এ commit করে রাখুন (পরে পর্ব ১৫-এর interactive rebase-এ গুছিয়ে নেবেন)। আর মাসের পর মাস জমে থাকা দশটি “WIP on main” stash প্রায় সবসময় ভুলে যাওয়া কাজ — মাঝে মাঝে git stash list দেখে সিদ্ধান্ত নিন।
৩ · Cherry-pick — একটি commit-এর বদল অন্য branch-এ
Cherry-pick একটি commit নেয়, তার parent-এর তুলনায় সেটি কী বদলেছিল (diff) বের করে, আর সেই বদল বর্তমান branch-এর মাথায় একটি নতুন commit হিসেবে বসায়। Commit-টি সরে না, কপি হয় — বার্তা আর লেখক একই থাকে, কিন্তু parent আলাদা, তাই hash আলাদা। পর্ব ১৭-এ দেখেছেন, “upstream first” নিয়মে fix আগে main-এ যায়, তারপর cherry-pick করে release branch-এ — এই কাজকে বলে backport. এবার তার খুঁটিনাটি।
রান্নাঘরের রেসিপি খাতার কথা ভাবুন। মা নতুন খাতায় বিরিয়ানির রেসিপিতে একটি সংশোধন লিখেছেন: “লবণ দুই চামচ নয়, দেড় চামচ”। পুরোনো খাতাটিও এখনো রান্নাঘরে চলে, তাই সেখানেও সংশোধনটা দরকার — কিন্তু নতুন খাতার বাকি পরীক্ষামূলক রেসিপি পুরোনো খাতায় ঢোকানো যাবে না। তিনি শুধু ওই একটি লাইনের বদল পুরোনো খাতায় নতুন করে লিখলেন, আর পাশে ছোট করে লিখলেন “নতুন খাতার ১২ পৃষ্ঠা থেকে”। এই শেষ চিরকুটটিই -x.
Scratch repository-র ইতিহাস: v1.4.0 tag-এর পরে main-এ চারটি commit, আর release/1.4 branch এখনো v1.4.0-এ। VAT গোল করার fix-টি release-এ নিতে হবে, কিন্তু coupon-এর নতুন feature নয়।
$ git log --oneline main
4321f1c fix(cart): reject zero or negative qty
b1845f3 docs: explain VAT rounding
ef0492c fix(cart): round VAT to 2 decimals
911374e feat(cart): add coupon field
93306c4 feat: first release of the cart
$ git switch release/1.4
Switched to branch 'release/1.4'
$ git cherry-pick -x ef0492c
[release/1.4 2e808fc] fix(cart): round VAT to 2 decimals
Date: Thu Jan 15 10:03:00 2026 +0600
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log -1 --format=%B
fix(cart): round VAT to 2 decimals
(cherry picked from commit ef0492cbb781bea76dea8f2206ee3b388805d91a)
$ git log --oneline release/1.4
2e808fc fix(cart): round VAT to 2 decimals
93306c4 feat: first release of the cart[release/1.4 2e808fc] — নতুন commit, নতুন hash, যদিও বার্তা আর বদল ef0492c-এর মতোই। Date: লাইনটি এসেছে কারণ author-এর সময় (মূল commit-এর, ১০:০৩) আর এখনকার committer-এর সময় আলাদা — cherry-pick author-কে রাখে, committer হন যিনি pick করলেন। -x বার্তার শেষে একটি ফাঁকা লাইন আর “(cherry picked from commit ef0492c…)” যোগ করেছে, পুরো ৪০ অক্ষরের hash-সহ। ছয় মাস পরে কেউ release branch-এর ইতিহাস দেখে জানতে পারবেন এই fix কোথা থেকে এল, আর main-এ সেটি খুঁজে পাবেন।
Conflict — --continue, --abort, --skip
Release branch-এ শুধু release-এর জন্য একটি বদল হয়েছিল: পরিমাণ সর্বোচ্চ ১০। এখন main-এর আরেকটি fix (4321f1c, শূন্য বা ঋণাত্মক পরিমাণ বাতিল) একই লাইন বদলায়:
$ git log --oneline -1
229d7dd fix(cart): cap qty at 10 on 1.4
$ git cherry-pick -x 4321f1c
Auto-merging cart.js
CONFLICT (content): Merge conflict in cart.js
error: could not apply 4321f1c... fix(cart): reject zero or negative qty
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
$ git status --short
UU cart.js
$ cat cart.js
function addItem(cart, qty) {
<<<<<<< HEAD
if (qty > 0 && qty <= 10) cart.qty += qty;
=======
if (qty <= 0) throw new Error("bad qty");
cart.qty += qty;
>>>>>>> 4321f1c (fix(cart): reject zero or negative qty)
}
$ cat cart.js
function addItem(cart, qty) {
if (qty <= 0 || qty > 10) throw new Error("bad qty");
cart.qty += qty;
}
$ git add cart.js
$ git cherry-pick --continue
[release/1.4 4d40735] fix(cart): reject zero or negative qty
Date: Thu Jan 15 10:05:00 2026 +0600
1 file changed, 2 insertions(+), 1 deletion(-)
$ git log --oneline release/1.4
4d40735 fix(cart): reject zero or negative qty
229d7dd fix(cart): cap qty at 10 on 1.4
2e808fc fix(cart): round VAT to 2 decimals
93306c4 feat: first release of the cartCherry-pick-এর merge-এ base হলো উৎস commit-এর parent (b1845f3), “ours” বর্তমান HEAD (229d7dd), “theirs” উৎস commit. Marker-এ HEAD মানে release-এর লাইন, 4321f1c (…) মানে আনা fix. সমাধানে দুই নিয়মই রাখা হলো: qty <= 0 || qty > 10 হলে error. Git-এর hint নিজেই তিনটি পথ দেখায়: সমাধান করে git add আর git cherry-pick --continue; এই commit বাদ দিয়ে (range-এ থাকলে) পরেরটিতে যেতে --skip; আর সব বাতিল করে cherry-pick শুরুর আগের অবস্থায় ফিরতে --abort. --continue বার্তা সম্পাদনার জন্য editor খোলে (এই scratch-এ editor বন্ধ রাখা ছিল); লক্ষ করুন, conflict সমাধানের পরেও -x-এর চিহ্ন বার্তায় থেকে গেছে (Git ২.৫৩-এর এই পরীক্ষায়; documentation-এর একটি পুরোনো বাক্য উল্টো ইঙ্গিত দেয়, তাই নিজের version-এ git log -1 দেখে নিন)।
পুরো backport-এর কাজটি ধাপে ধাপে:
| Command | |
| release/1.4 | |
| Git যা করছে |
- বাছাই। Conventional Commits-এর type (পর্ব ১৮) এখানে কাজে আসে:
fix(cart)দুটি backport-যোগ্য,featআরdocsrelease-এ যাবে না। - লক্ষ্য branch-এ যাওয়া। Cherry-pick সবসময় বর্তমান branch-এ বসায়, তাই আগে release branch-এ switch.
- প্রথম pick. Git শুধু
ef0492c-এর নিজের বদল নেয়, তার আগের coupon-এর commit নয়। ফল নতুন commit2e808fc, বার্তায় উৎস লেখা। - দুই branch আলাদা হয়ে যায়। Release-এ main-এ নেই এমন একটি বদল এলো — এখন থেকে main-এর কোনো fix ওই লাইন ছুঁলে conflict হতে পারে।
- দ্বিতীয় pick-এ conflict. Git থেমে আছে;
git statusদেখায় “You are currently cherry-picking commit 4321f1c”, আর.git/CHERRY_PICK_HEADউৎস মনে রাখে। - Continue. দুই নিয়ম মিলিয়ে সমাধান,
add,--continue. নতুন commit4d40735; এর diff মূল fix-এর থেকে একটু আলাদা। - যাচাই।
git cherrypatch-এর মিল দেখে:-মানে একই বদল release-এ আছে,+মানে নেই।4321f1c-কে+দেখাচ্ছে, কারণ হাতে মেলানো বদল আর হুবহু এক নয় — পরের অংশে কারণটি দেখবেন। এরপর tag, যেমনv1.4.1.
-n আর merge commit-এর -m
$ git switch -q -c demo3 v1.4.0
$ git cherry-pick -n 911374e ef0492c
$ git status --short
A coupon.js
M vat.js
$ git log --oneline -1
93306c4 feat: first release of the cart
$ git log --oneline --graph -3 main
* e8a15f5 Merge branch 'feat/receipt'
|\
| * 1b559a1 feat(cart): print receipt
|/
* 4321f1c fix(cart): reject zero or negative qty
$ git switch -q release/1.4
$ git cherry-pick e8a15f5
error: commit e8a15f50e9f93b33464697d6272d241dcf0c8321 is a merge but no -m option was given.
fatal: cherry-pick failed
$ git cherry-pick -x -m 1 e8a15f5
[release/1.4 13758fe] Merge branch 'feat/receipt'
Date: Thu Jan 15 10:32:00 2026 +0600
1 file changed, 1 insertion(+)
create mode 100644 receipt.js-n (--no-commit) দুটি commit-এর বদল শুধু index আর working tree-তে বসিয়েছে — A coupon.js, M vat.js — কিন্তু কোনো commit হয়নি, HEAD এখনো 93306c4. কয়েকটি commit মিলিয়ে একটি commit বানাতে, বা commit-এর আগে কিছু যোগ করতে এটি কাজে লাগে। Merge commit-এর দুটি parent, তাই “কার তুলনায় diff?” প্রশ্নের উত্তর না দিলে Git pick করতে পারে না: -m 1 মানে প্রথম parent (merge-এর সময় যে branch-এ ছিলেন, এখানে main) তুলনার base — ফলে feature branch যা এনেছিল (receipt.js) সেটুকুই বসে। -m 2 উল্টো দিক থেকে হিসাব করত, যা প্রায় কখনো চাওয়া হয় না।
৪ · Range-এর নিয়ম আর duplicate commit-এর ঝুঁকি
একসাথে কয়েকটি commit pick করতে range লেখা যায়, কিন্তু এর নিয়মটি অনেক অভিজ্ঞ developer-ও উল্টো মনে রাখেন। X..Y মানে “Y থেকে পেছনে হেঁটে যাদের পাওয়া যায়, তাদের মধ্যে যাদের X থেকে পাওয়া যায় না” — পর্ব ৯-এ git log main..feature-এ এই নিয়মই দেখেছেন। X নিজে X থেকে পাওয়া যায়, তাই X বাদ. X-সহ চাইলে শুরু করতে হয় X-এর parent থেকে: X^..Y. Cherry-pick এই commit-গুলো পুরোনো থেকে নতুন ক্রমে বসায়।
$ git switch -q -c demo1 v1.4.0
$ git cherry-pick 911374e..b1845f3
[demo1 38c993d] fix(cart): round VAT to 2 decimals
Date: Thu Jan 15 10:03:00 2026 +0600
1 file changed, 1 insertion(+), 1 deletion(-)
[demo1 33114cb] docs: explain VAT rounding
Date: Thu Jan 15 10:04:00 2026 +0600
1 file changed, 1 insertion(+)
$ git log --oneline
33114cb docs: explain VAT rounding
38c993d fix(cart): round VAT to 2 decimals
93306c4 feat: first release of the cart
$ git switch -q -c demo2 v1.4.0
$ git cherry-pick 911374e^..b1845f3
[demo2 05b0b67] feat(cart): add coupon field
Date: Thu Jan 15 10:02:00 2026 +0600
1 file changed, 1 insertion(+)
create mode 100644 coupon.js
[demo2 5348b31] fix(cart): round VAT to 2 decimals
Date: Thu Jan 15 10:03:00 2026 +0600
1 file changed, 1 insertion(+), 1 deletion(-)
[demo2 1e11b72] docs: explain VAT rounding
Date: Thu Jan 15 10:04:00 2026 +0600
1 file changed, 1 insertion(+)
$ git log --oneline
1e11b72 docs: explain VAT rounding
5348b31 fix(cart): round VAT to 2 decimals
05b0b67 feat(cart): add coupon field
93306c4 feat: first release of the cartদুটি নতুন branch, দুটিই v1.4.0 থেকে। 911374e..b1845f3 এনেছে দুটি commit (VAT fix আর docs) — coupon-এর 911374e নিজে বাদ। 911374e^..b1845f3 এনেছে তিনটি। দুই branch-এ একই বদলের hash আলাদা (38c993d বনাম 5348b31), কারণ parent আলাদা — Git-এর hash পুরো ইতিহাসের ওপর নির্ভর করে।
একই নিয়ম একটি ছোট model-এ, আরও তিনটি ক্ষেত্রসহ — উল্টো range, একই commit দুই দিকে, আর প্রথম commit-এর parent:
// Which commits does "git cherry-pick X..Y" pick? A linear history, oldest first;
// each commit's parent is the one before it. X^ means "the parent of X".
const log = [
["93306c4", "feat: first release of the cart"],
["911374e", "feat(cart): add coupon field"],
["ef0492c", "fix(cart): round VAT to 2 decimals"],
["b1845f3", "docs: explain VAT rounding"],
["4321f1c", "fix(cart): reject zero or negative qty"],
];
function pos(rev) { // "hash" or "hash^" -> index in log
const up = rev.endsWith("^"), h = up ? rev.slice(0, -1) : rev;
const i = log.findIndex(([x]) => x === h);
if (i < 0) throw new Error("unknown revision " + rev);
if (up && i === 0) throw new Error(rev + ": a root commit has no parent");
return up ? i - 1 : i;
}
const reach = (i) => new Set(log.slice(0, i + 1).map(([h]) => h)); // i + ancestors
function range(spec) { // X..Y = reachable from Y, minus from X
const [x, y] = spec.split("..");
const fromY = reach(pos(y)), fromX = reach(pos(x));
return log.filter(([h]) => fromY.has(h) && !fromX.has(h)); // oldest first
}
for (const spec of ["911374e..b1845f3", "911374e^..b1845f3", "b1845f3..911374e",
"ef0492c..ef0492c", "93306c4^..ef0492c"]) {
try {
const r = range(spec);
console.log(spec.padEnd(18) + " -> " + r.length + " commit(s)");
for (const [h, s] of r) console.log(" pick " + h + " " + s);
} catch (e) { console.log(spec.padEnd(18) + " -> error: " + e.message); }
}আউটপুট:
911374e..b1845f3 -> 2 commit(s)
pick ef0492c fix(cart): round VAT to 2 decimals
pick b1845f3 docs: explain VAT rounding
911374e^..b1845f3 -> 3 commit(s)
pick 911374e feat(cart): add coupon field
pick ef0492c fix(cart): round VAT to 2 decimals
pick b1845f3 docs: explain VAT rounding
b1845f3..911374e -> 0 commit(s)
ef0492c..ef0492c -> 0 commit(s)
93306c4^..ef0492c -> error: 93306c4^: a root commit has no parent
প্রথম দুটি লাইন আসল Git-এর ফলের সাথে হুবহু মেলে। উল্টো লেখা b1845f3..911374e কোনো commit দেয় না, কারণ পুরোনো commit থেকে পেছনে হেঁটে নতুনটিতে পৌঁছানো যায় না — আসল Git-এ এটি “error: empty commit set passed” বলে থামে। ef0492c..ef0492c ফাঁকা, কারণ একই set থেকে একই set বাদ। আর প্রথম commit-এর কোনো parent নেই, তাই 93306c4^ অর্থহীন; আসল Git বলে “fatal: bad revision”. শাখাওয়ালা ইতিহাসে নিয়ম একই, তবে তালিকায় পাশের শাখার commit-ও আসতে পারে — আগে git log --oneline X..Y দেখে নিন।
Duplicate commit — git cherry আর patch-id
Cherry-pick-এর দাম হলো একই বদল ইতিহাসে দুইবার, দুটি আলাদা hash-এ। পরে release branch আবার main-এ merge হলে Git সাধারণত দুটিকে মিলিয়ে নিতে পারে — দুই দিকে একই লেখা হলে three-way merge-এ conflict হয় না। কিন্তু মাঝে কোনো একদিকে ওই লাইন আবার বদলালে conflict হয়, আর git log-এ একই বার্তা দুইবার দেখে মানুষ বিভ্রান্ত হন। কোন commit আসলে আগেই আছে, তা Git বের করে patch-id দিয়ে: diff-এর এমন একটি hash, যাতে line number আর সাদা জায়গা গোনা হয় না। পর্ব ১৫-এ দেখেছেন, rebase এটি দেখেই আগে থেকে থাকা commit বাদ দেয়।
$ git cherry -v release/1.4 main
+ 911374ebfffe16048b4fd1ae5c1e52c1ad2ba7e0 feat(cart): add coupon field
- ef0492cbb781bea76dea8f2206ee3b388805d91a fix(cart): round VAT to 2 decimals
+ b1845f3a557b4526ac8dbddee03abb86194383a1 docs: explain VAT rounding
+ 4321f1c446242c4bce7dcd0c00b7adbb9be69bc6 fix(cart): reject zero or negative qty
- 1b559a153fd8433df356d721fd7263edf40b4666 feat(cart): print receiptgit cherry -v <upstream> <head> head-এর প্রতিটি commit-এর পাশে চিহ্ন দেয়: - মানে একই patch-id-এর commit upstream-এ (এখানে release/1.4-এ) আছে, + মানে নেই। VAT fix (ef0492c) আর receipt-এর commit (1b559a1, merge থেকে -m 1 দিয়ে আনা) -. কিন্তু 4321f1c +, যদিও আমরা সেটি pick করেছি — conflict-এ হাতে মেলানোয় diff আলাদা হয়েছে, তাই patch-id মেলেনি। শিক্ষা: patch-id হুবহু একই বদল চেনে, “একই উদ্দেশ্যের” বদল নয়। এজন্য -x-এর লেখা উৎস-hash একটি দ্বিতীয় সূত্র — git log --grep="cherry picked from commit 4321f1c" দিয়ে খুঁজে পাওয়া যায়। git log --cherry-mark --oneline release/1.4...main একই তথ্য দুই দিক থেকে দেখায়।
কখন cherry-pick নয়: একটি branch-এর প্রায় সব commit অন্য branch-এ নিতে হলে merge বা rebase-ই সৎ পথ। নিয়মিতভাবে দুই দিকে cherry-pick (release থেকে main, main থেকে release) করলে ইতিহাস জট পাকায়। অনেক দল তাই নিয়ম রাখে: fix সবসময় আগে main-এ, তারপর শুধু main থেকে release-এ, সবসময় -x দিয়ে। যে fix শুধু পুরোনো release-এর জন্য (main-এ সেই কোড আর নেই), সেটি release branch-এ সরাসরি commit, বার্তায় কারণসহ।
৫ · Worktree — এক repository, অনেক working folder
Stash একটি কাজ সরিয়ে আরেকটি করার উপায়। কিন্তু কখনো দুটি কাজই খোলা রাখা দরকার: feature-এর লম্বা build বা test চলছে, এর মধ্যে hotfix; বা একজন সহকর্মীর pull request নিজের কম্পিউটারে চালিয়ে দেখতে হবে, অথচ নিজের branch-এ অর্ধেক কাজ। আরেকটি clone করা যায়, কিন্তু তাতে পুরো ইতিহাস আবার নামে, আর দুই clone-এর branch আলাদা থাকে — একটিতে fetch করলে অন্যটি জানে না। git worktree (Git ২.৫, ২০১৫ থেকে) একই repository-র জন্য আরেকটি working folder খোলে: object আর branch একটাই জায়গায়, কিন্তু প্রতিটি folder-এর নিজের HEAD, নিজের index আর নিজের file.
একটি বড় মুদি দোকানের কথা ভাবুন, যার গুদাম একটাই কিন্তু কাউন্টার তিনটি। প্রতিটি কাউন্টারে আলাদা কর্মচারী আলাদা খদ্দেরের মাল সাজাচ্ছেন (নিজের index আর working tree), কিন্তু সবাই একই গুদাম (object store) আর একই হিসাবের খাতা (branch-এর তালিকা) ব্যবহার করেন। এক কাউন্টারে খাতায় নতুন লেখা হলে বাকি দুই কাউন্টার সাথে সাথে দেখতে পান। শুধু একটি নিয়ম: একই খদ্দেরের অর্ডার (একই branch) একসাথে দুই কাউন্টারে সাজানো যাবে না — নইলে কে কোন মাল তুলল, তা মেলানো অসম্ভব।
তিনটি worktree, একটি .git
Shared object store and refs · separate HEAD, index and files per worktree
git log hotfix/receipt-এ দেখা যায়। প্রতিটি folder-এর HEAD আর index আলাদা, তাই একই সময়ে তিনটি আলাদা অবস্থায় কাজ চলে। Detached HEAD-এর ওপর কোনো branch-এর তালা নেই।$ git status --short
M app.js
$ git worktree add ../shop-hotfix -b hotfix/receipt main
Preparing worktree (new branch 'hotfix/receipt')
HEAD is now at 0e7c38a feat: add cart total
$ git worktree list
~/work/shop 0e7c38a [feature/discount]
~/work/shop-hotfix 0e7c38a [hotfix/receipt]
$ cd ../shop-hotfix
$ cat .git
gitdir: ~/work/shop/.git/worktrees/shop-hotfix
$ git branch --show-current
hotfix/receipt
$ git commit -q -am 'fix(cart): print receipt'
$ git worktree add ../shop-review feature/discount
Preparing worktree (checking out 'feature/discount')
fatal: 'feature/discount' is already used by worktree at '~/work/shop'
$ cd ../shop
$ git switch hotfix/receipt
fatal: 'hotfix/receipt' is already used by worktree at '~/work/shop-hotfix'
$ git status --short
M app.js
$ git worktree add --detach ../shop-review main
Preparing worktree (detached HEAD 0e7c38a)
HEAD is now at 0e7c38a feat: add cart total
$ git worktree list
~/work/shop 0e7c38a [feature/discount]
~/work/shop-hotfix 139f6dc [hotfix/receipt]
~/work/shop-review 0e7c38a (detached HEAD)
$ git worktree remove ../shop-hotfix
$ rm -rf ../shop-review
$ git worktree list
~/work/shop 0e7c38a [feature/discount]
~/work/shop-review 0e7c38a (detached HEAD) prunable
$ git worktree prune
$ git worktree list
~/work/shop 0e7c38a [feature/discount]
$ git branch
* feature/discount
hotfix/receipt
mainPath ছোট করে ~/work লেখা হয়েছে; আপনার কম্পিউটারে পুরো path দেখাবে। git worktree add ../shop-hotfix -b hotfix/receipt main একসাথে তিনটি কাজ করে: main থেকে নতুন branch, নতুন folder, আর সেখানে checkout. মূল folder-এর M app.js একটুও ছোঁয়া হয়নি — stash লাগেনি। Linked worktree-তে .git একটি ছোট file, যাতে লেখা মূল .git/worktrees/shop-hotfix-এর ঠিকানা (linked worktree). এরপর দুটি “fatal”: feature/discount মূল folder-এ খোলা, তাই নতুন worktree-তে খোলা যায় না; আর hotfix/receipt অন্য folder-এ খোলা, তাই মূল folder-এ switch-ও করা যায় না। পর্ব ১২-এ এই সতর্কতার কথাই বলা হয়েছিল। শুধু দেখার জন্য --detach দিয়ে কোনো branch ছাড়াই একটি commit খোলা যায়। শেষে দুই রকম পরিষ্কার: git worktree remove folder আর তার তথ্য দুটোই সরায় (বদলানো বা untracked file থাকলে --force ছাড়া রাজি হয় না); হাতে rm -rf করলে Git-এর খাতায় entry “prunable” হয়ে পড়ে থাকে, যা git worktree prune সরায়।
| পরিস্থিতি | Worktree দিয়ে | খেয়াল রাখুন |
|---|---|---|
| মাঝপথের feature, হঠাৎ hotfix | git worktree add ../app-hotfix -b hotfix/x main | কাজ শেষে remove; branch আলাদাভাবে মুছতে হয় |
| লম্বা build বা test চলছে | এক folder-এ build চলুক, অন্য folder-এ কাজ | দুই folder-এ build-এর output folder আলাদা, তাই disk বেশি লাগে |
| সহকর্মীর pull request চালিয়ে দেখা | git fetch origin pull-branch, তারপর git worktree add --detach ../review origin/pull-branch | Review শেষে folder মুছুন; detached HEAD-এ commit না করাই ভালো |
নিয়মগুলো একটি ছোট model-এ — একই ধাপগুলোর সাথে আরও তিনটি ভুল চেষ্টা:
// Worktree rules: one .git, many working folders. A branch may be checked out in
// at most ONE worktree at a time; a detached HEAD has no such limit.
const branches = new Set(["main", "feature/discount"]);
const wts = [{ path: "~/work/shop", head: "feature/discount", main: true, dirty: true }];
const usedBy = (b) => wts.find((w) => w.head === b);
function add(path, ref, { b, detach } = {}) {
if (wts.some((w) => w.path === path)) return `fatal: '${path}' already exists`;
if (b) { if (branches.has(b)) return `fatal: a branch named '${b}' already exists`;
branches.add(b); ref = b; } else if (!detach && usedBy(ref))
return `fatal: '${ref}' is already used by worktree at '${usedBy(ref).path}'`;
wts.push({ path, head: detach ? "(detached HEAD)" : ref, dirty: false });
return `ok: ${path} [${detach ? "detached HEAD" : ref}]`;
}
function sw(path, ref) { // git switch inside one worktree
const w = wts.find((x) => x.path === path), u = usedBy(ref);
if (u && u !== w) return `fatal: '${ref}' is already used by worktree at '${u.path}'`;
w.head = ref; return `ok: ${path} is now on ${ref}`;
}
function remove(path, force = false) {
const i = wts.findIndex((w) => w.path === path), w = wts[i];
if (w.main) return `fatal: '${path}' is a main working tree`;
if (w.dirty && !force) return `fatal: '${path}' contains modified or untracked files, use --force to delete it`;
wts.splice(i, 1); return `ok: removed ${path}; branch '${w.head}' still exists`;
}
const steps = [
["add -b hotfix/receipt", () => add("~/work/shop-hotfix", "main", { b: "hotfix/receipt" })],
["add feature/discount", () => add("~/work/shop-review", "feature/discount")],
["add --detach", () => add("~/work/shop-review", "feature/discount", { detach: true })],
["switch hotfix/receipt", () => sw("~/work/shop", "hotfix/receipt")],
["add -b hotfix/receipt", () => add("~/work/shop-2", "main", { b: "hotfix/receipt" })],
["remove main worktree", () => remove("~/work/shop")],
["remove shop-hotfix", () => remove("~/work/shop-hotfix")],
["switch hotfix/receipt", () => sw("~/work/shop", "hotfix/receipt")],
];
for (const [label, run] of steps) console.log(label.padEnd(22) + run());আউটপুট:
add -b hotfix/receipt ok: ~/work/shop-hotfix [hotfix/receipt]
add feature/discount fatal: 'feature/discount' is already used by worktree at '~/work/shop'
add --detach ok: ~/work/shop-review [detached HEAD]
switch hotfix/receipt fatal: 'hotfix/receipt' is already used by worktree at '~/work/shop-hotfix'
add -b hotfix/receipt fatal: a branch named 'hotfix/receipt' already exists
remove main worktree fatal: '~/work/shop' is a main working tree
remove shop-hotfix ok: removed ~/work/shop-hotfix; branch 'hotfix/receipt' still exists
switch hotfix/receipt ok: ~/work/shop is now on hotfix/receipt
“fatal” বার্তাগুলোর লেখা Git ২.৫৩-এর আসল বার্তা থেকে নেওয়া (scratch repository-তে একই ভুল চালিয়ে মিলিয়ে দেখা)। দ্বিতীয় লাইনে branch-এর তালা, তৃতীয় লাইনে --detach দিয়ে সেই তালা এড়ানো, পঞ্চম লাইনে -b দিয়ে আগে থেকে থাকা branch-এর নাম আবার দেওয়া যায় না, ষষ্ঠ লাইনে মূল worktree remove করা যায় না। সপ্তম লাইনে hotfix-এর folder মুছতেই শেষ লাইনে মূল folder সেই branch-এ switch করতে পারল — তালা folder-এর সাথে, branch-এর সাথে নয়। Model-এ একটি সরলীকরণ আছে: আসল Git খালি folder-এ worktree বানাতে দেয়, শুধু ফাঁকা নয় এমন folder-এ “already exists” বলে।
কেন এই তালা? একই branch দুই folder-এ খোলা থাকলে এক folder-এ commit করলে branch-এর মাথা সরে যেত, অথচ অন্য folder-এর index আর file পুরোনো commit-এর মতো থাকত — পরের commit পুরোনো অবস্থার ওপর বসে আগের commit-এর বদল উল্টে দিত। প্রয়োজনে git worktree add -f তালা উপেক্ষা করতে পারে, কিন্তু সেটি এই ঝুঁকি জেনেই। আর submodule-ওয়ালা repository-তে worktree-এর সমর্থন documentation অনুযায়ী এখনো অসম্পূর্ণ — সেখানে সাবধান।
৬ · Sparse checkout-এর cone mode — বড় repository-র শুধু দরকারি folder
পর্ব ১৯-এ monorepo-র প্রসঙ্গে sparse checkout-এর পরিচয় পেয়েছেন: partial clone-এর সাথে মিলিয়ে শুধু একটি folder খোলা। এবার command-টির পুরো চক্র, আর cone mode আসলে কোন নিয়ম লেখে। git sparse-checkout command Git ২.২৫ (২০২০) থেকে আছে; cone mode Git ২.৩৭ (২০২২) থেকে ডিফল্ট বলে release note-এ আছে। এর আগে sparse checkout চালু হতো core.sparseCheckout config আর হাতে লেখা .git/info/sparse-checkout file দিয়ে।
$ find . -path ./.git -prune -o -type f -print | sort
./README.md
./docs/main.txt
./package.json
./services/README.md
./services/catalog/main.txt
./services/payments/main.txt
./web/admin/main.txt
./web/shop/main.txt
$ git sparse-checkout init --cone
$ find . -path ./.git -prune -o -type f -print | sort
./README.md
./package.json
$ git sparse-checkout set services/payments
$ find . -path ./.git -prune -o -type f -print | sort
./README.md
./package.json
./services/README.md
./services/payments/main.txt
$ git sparse-checkout list
services/payments
$ cat .git/info/sparse-checkout
/*
!/*/
/services/
!/services/*/
/services/payments/
$ git sparse-checkout add web/shop
$ git sparse-checkout list
services/payments
web/shop
$ git status
On branch main
You are in a sparse checkout with 63% of tracked files present.
nothing to commit, working tree clean
$ git sparse-checkout disable
$ find . -path ./.git -prune -o -type f -print | sort
./README.md
./docs/main.txt
./package.json
./services/README.md
./services/catalog/main.txt
./services/payments/main.txt
./web/admin/main.txt
./web/shop/main.txtinit --cone-এর পরে শুধু root-এর file (README.md, package.json) — cone mode-এ root-এর সরাসরি file সবসময় থাকে। set services/payments-এর পরে এলো ওই folder-এর সব কিছু, আর পথের ওপরের folder services-এর সরাসরি file services/README.md; কিন্তু পাশের services/catalog নয়। .git/info/sparse-checkout-এ Git ঠিক এই নিয়মটিই pattern হিসেবে লিখেছে: /* (root-এর সব), !/*/ (কিন্তু root-এর কোনো folder নয়), /services/ আর !/services/*/ (services-এর file, কিন্তু তার folder নয়), শেষে /services/payments/ (এটি পুরোটা)। add আগের তালিকার সাথে যোগ করে, set পুরো তালিকা বদলে দেয় — এই পার্থক্যে ভুল করলে হঠাৎ folder “উধাও” হয়। git status মনে করিয়ে দেয় আপনি sparse checkout-এ আছেন: ৮টি tracked file-এর ৫টি folder-এ (৫ ÷ ৮ = ৬২.৫%, Git দেখিয়েছে ৬৩%). disable সব ফিরিয়ে আনে। Git-এর documentation এখন init-কে deprecated বলে; সরাসরি set দিলেই sparse checkout চালু হয়।
Cone mode (ডিফল্ট)
শুধু folder-এর নাম দেন। Git folder ধরে হিসাব করে, তাই লাখো file-এর repository-তেও দ্রুত। নিয়মটি অনুমানযোগ্য: folder-এর সব, ওপরের folder-গুলোর সরাসরি file.
প্রায় সব ক্ষেত্রেNon-cone mode (--no-cone)
.gitignore-এর মতো যেকোনো pattern, যেমন *.md. নমনীয়, কিন্তু বড় repository-তে ধীর। Documentation একে নিরুৎসাহিত করে।
তিনটি ফাঁদ মনে রাখুন। এক, sparse checkout শুধু working tree ছোট করে; সাধারণ clone-এ সব object তবু নামানো থাকে — জায়গা বাঁচাতে git clone --filter=blob:none --sparse একসাথে লাগে (পর্ব ১৯)। দুই, cone-এর বাইরের path-এ git add করলে Git সতর্ক করে আর থামে, কারণ ওই file আপনার working tree-তে থাকার কথা নয়। তিন, CI বা build tool যদি পুরো repository ধরে নেয়, sparse folder-এ সেগুলো অনুপস্থিত file-এর জন্য ব্যর্থ হতে পারে।
৭ · Git hook — Git-এর কাজের ফাঁকে নিজের script
Hook হলো একটি script, যা Git নির্দিষ্ট মুহূর্তে নিজে চালায় — commit তৈরির আগে, বার্তা লেখার পরে, push-এর আগে, server-এ push গ্রহণের আগে-পরে। Script-টি যেকোনো ভাষায় হতে পারে (shell, Python, Node.js); Git শুধু file-টি চালায়, argument আর standard input দেয়, আর শেষে exit code দেখে। “আগে” ধরনের hook-এ exit code 0 ছাড়া অন্য কিছু হলে Git কাজটি থামিয়ে দেয়। “পরে” ধরনের hook (post-…) কিছু থামাতে পারে না, শুধু খবর পায়। পর্ব ৪৪-এ pre-commit hook-এ linter চালানোর কথা দেখেছেন; এখানে পুরো ব্যবস্থাটি।
বিয়েবাড়ির গেটে দুই রকম পাহারা ভাবুন। বাড়ির ভেতরে প্রতিটি ঘরের দরজায় পরিবারের কেউ মনে করিয়ে দেন, “জুতা বাইরে রাখুন” — কিন্তু কেউ চাইলে পাশ কাটিয়ে ঢুকে যেতে পারেন। এটি client-side hook: আপনার নিজের কম্পিউটারে, আপনার নিয়ন্ত্রণে। আর মূল গেটে নিরাপত্তারক্ষী দাওয়াতপত্র দেখে তবে ঢুকতে দেন, কারও অনুরোধে নিয়ম বদলান না। এটি server-side hook আর CI: যেখানে কোড আসলে জমা হয়।
| Hook | কখন চলে | কী পায় | শূন্য ছাড়া exit code দিলে |
|---|---|---|---|
pre-commit | Commit-এর বার্তা চাওয়ার আগে | কোনো argument নয়; staged অবস্থা git diff --cached দিয়ে দেখে | Commit বাতিল |
prepare-commit-msg | Editor খোলার আগে, ডিফল্ট বার্তা তৈরির পরে | বার্তার file; বার্তার উৎস (message, template, merge, squash বা commit); কখনো commit-এর hash | Commit বাতিল |
commit-msg | বার্তা চূড়ান্ত হওয়ার পরে, commit তৈরির আগে | বার্তার file-এর path (একটিই argument) | Commit বাতিল |
pre-push | git push-এ কিছু পাঠানোর আগে | Argument: remote-এর নাম আর URL; stdin-এ প্রতি লাইনে local ref, local hash, remote ref, remote hash | Push হয় না |
post-checkout | checkout/switch (আর clone)-এর পরে | আগের HEAD, নতুন HEAD, আর একটি flag (1 = branch বদল, 0 = file checkout) | ফল বদলায় না (শুধু exit code ফেরে) |
post-merge | সফল merge-এর পরে (pull-সহ) | একটি flag: squash merge কি না | Merge বাতিল হয় না |
pre-receive (server) | Push গ্রহণের আগে, একবার | stdin-এ প্রতিটি ref-এর জন্য: পুরোনো hash, নতুন hash, ref-এর নাম | পুরো push প্রত্যাখ্যাত |
update (server) | প্রতিটি ref-এর জন্য আলাদা করে | Argument: ref-এর নাম, পুরোনো hash, নতুন hash | শুধু সেই ref প্রত্যাখ্যাত |
post-receive (server) | সব ref আপডেট হয়ে যাওয়ার পরে | pre-receive-এর মতো stdin | কিছু থামে না; notification বা deploy শুরু করতে |
প্রতিটি নতুন repository-তে Git .git/hooks/-এ কয়েকটি নমুনা রাখে, নামের শেষে .sample — তাই সেগুলো চলে না। কোনো নমুনা চালু করতে .sample বাদ দিয়ে নাম দিন আর (Linux/macOS-এ) চালানোর অনুমতি দিন। কিন্তু .git/hooks repository-র ইতিহাসের অংশ নয় — clone-এ আসে না, push-এ যায় না। দলের সবার জন্য একই hook চাইলে সাধারণ পথ: hook-গুলো repository-র একটি folder-এ (যেমন .githooks/) commit করা, আর প্রত্যেকে একবার core.hooksPath (Git ২.৯, ২০১৬ থেকে) দিয়ে Git-কে সেই folder দেখানো। নিচে একটি commit-msg hook, যা Conventional Commits-এর (পর্ব ১৮) প্রথম লাইনের আকার যাচাই করে:
$ ls .git/hooks
applypatch-msg.sample
commit-msg.sample
fsmonitor-watchman.sample
post-update.sample
pre-applypatch.sample
pre-commit.sample
pre-merge-commit.sample
pre-push.sample
pre-rebase.sample
pre-receive.sample
prepare-commit-msg.sample
push-to-checkout.sample
sendemail-validate.sample
update.sample
$ mkdir .githooks
$ cat .githooks/commit-msg
#!/bin/sh
# commit-msg hook: $1 is the file that holds the proposed message
subject=$(head -n 1 "$1")
types='feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert'
if ! printf '%s\n' "$subject" | grep -Eq "^($types)(\([a-z0-9-]+\))?!?: [^ ]"; then
echo "commit-msg: subject must look like 'type(scope): description'" >&2
echo " got: $subject" >&2
exit 1
fi
if [ ${#subject} -gt 72 ]; then
echo "commit-msg: subject has ${#subject} characters (max 72)" >&2
exit 1
fi
exit 0
$ git config core.hooksPath .githooks
$ git add --chmod=+x .githooks/commit-msg
$ git add vat.js
$ git commit -m "fixed stuff"; echo "exit code: $?"
commit-msg: subject must look like 'type(scope): description'
got: fixed stuff
exit code: 1
$ git commit -m 'fix(cart): round VAT to two decimals'
[main 4f7a80e] fix(cart): round VAT to two decimals
2 files changed, 15 insertions(+)
create mode 100755 .githooks/commit-msg
create mode 100644 vat.js
$ git commit -am "wip"; echo "exit code: $?"
commit-msg: subject must look like 'type(scope): description'
got: wip
exit code: 1
$ git commit --no-verify -am 'wip'
[main fe266b0] wip
1 file changed, 1 insertion(+)
$ git log --oneline
fe266b0 wip
4f7a80e fix(cart): round VAT to two decimals
c0820a3 chore: start projectHook-টি POSIX shell script — Git for Windows hook চালায় নিজের সাথে আসা sh দিয়ে, তাই একই file Windows, Linux আর macOS-এ চলে। $1 হলো বার্তার file (সাধারণত .git/COMMIT_EDITMSG); প্রথম লাইনটি grep -E দিয়ে মেলানো হয়। git add --chmod=+x file-টিকে Git-এর ভেতরে চালানোর অনুমতিসহ রাখে — আউটপুটে create mode 100755; Windows-এ file system এই অনুমতি রাখে না, তাই এটি না দিলে Linux বা macOS-এ সহকর্মীর কাছে hook “not set as executable” বলে উপেক্ষিত হতো। “fixed stuff” আর “wip” দুবারই exit code: 1, কোনো commit হয়নি, staged বদল যেমন ছিল তেমনই। তারপর --no-verify: Git pre-commit আর commit-msg hook না চালিয়েই fe266b0 wip বানিয়ে ফেলল। একটি সতর্কতা: core.hooksPath সেট হলে Git .git/hooks-এর hook আর দেখে না।
--no-verify আর server-এর পাহারা
--no-verify (git commit -n, আর git push --no-verify) থাকার কারণ বাস্তব: hook ভুল করে আটকালে বা জরুরি অবস্থায় মানুষকে আটকে রাখা যায় না। কিন্তু এর মানে, client-side hook কোনো নিয়মের নিশ্চয়তা নয় — কেউ hook চালুই না করতে পারেন, বা পুরোনো version রাখতে পারেন। নিয়ম সত্যিই বাধ্যতামূলক করতে হয় যেখানে কোড জমা হয়: server-এর pre-receive hook, বা hosting platform-এ protected branch আর CI-এর বাধ্যতামূলক check (পর্ব ১৬, আর পর্ব ৫৮-এ continuous integration). একটি local bare repository-কে “server” বানিয়ে দেখুন:
#!/bin/sh
# pre-receive hook (server): stdin has one line per ref: <old> <new> <refname>
zero=0000000000000000000000000000000000000000
status=0
while read old new ref; do
[ "$ref" = "refs/heads/main" ] || continue
if [ "$old" = "$zero" ]; then range=$new; else range=$old..$new; fi
for c in $(git rev-list "$range"); do
subject=$(git log -1 --format=%s "$c")
if ! printf '%s\n' "$subject" | grep -Eq '^[a-z]+(\([a-z0-9-]+\))?!?: [^ ]'; then
echo "pre-receive: $(git rev-parse --short "$c") on main: bad subject '$subject'"
status=1
fi
done
done
exit $status$ git init -q --bare ../server.git
$ git remote add origin ../server.git
$ git push origin main
remote: pre-receive: fe266b0 on main: bad subject 'wip'
To ../server.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '../server.git'
$ git commit --amend -m 'feat(cart): add shipping constant'
[main 3aede59] feat(cart): add shipping constant
Date: Thu Jan 15 10:11:00 2026 +0600
1 file changed, 1 insertion(+)
$ git push origin main
To ../server.git
* [new branch] main -> mainServer-এ নতুন branch, তাই stdin-এর পুরোনো hash চল্লিশটি শূন্য, আর script তখন নতুন commit থেকে পৌঁছানো সব commit দেখে। “remote:” দিয়ে শুরু লাইন server-এর hook-এর আউটপুট; তারপর ! [remote rejected] main -> main (pre-receive hook declined). Client-এর --no-verify এখানে কিছুই করতে পারেনি, কারণ hook চলছে অন্য যন্ত্রে, অন্যের নিয়ন্ত্রণে। --amend দিয়ে বার্তা ঠিক করে (এবার local commit-msg hook-ও পাস) আবার push — গ্রহণ। Server-side hook লিখতে server-এ অধিকার লাগে; নিজে চালানো Git server-এ সেটি সম্ভব, কিন্তু অনেক hosted platform (লেখার সময়ে) সাধারণ ব্যবহারকারীকে নিজস্ব pre-receive hook দেয় না — সেখানে একই কাজ হয় branch protection, push rule বা CI দিয়ে, platform-ভেদে আলাদাভাবে।
পুরো ঘটনাটি ধাপে ধাপে — কোন hook চলল, কী পেল, কী ফল:
| Command | |
| যে hook চলল | |
| Hook যা পেল | |
| ফল |
- প্রথম চেষ্টা। Git বার্তাটি একটি file-এ লিখে commit-msg hook-কে তার path দেয়।
fixed stuff-এtype:নেই,grepমেলে না, hookexit 1— Git commit বানায় না। - ঠিক বার্তা।
fix(cart): …নিয়মে মেলে, দৈর্ঘ্য ৭২-এর কম, hookexit 0. Git commit4f7a80eতৈরি করে। - আবার ভুল।
wipআটকে গেল — hook প্রতিবারই চলে, কাউকে আলাদা মনে রাখতে হয় না। - পাশ কাটানো।
--no-verifycommit-এর দুটি hook-ই বাদ দেয়। নিয়ম-ভাঙা commit এখন local ইতিহাসে — client-side hook-এর সীমা এটাই। - Server-এর পাহারা। Push-এর সময় server নিজের pre-receive চালায়, stdin-এ ref-এর পুরোনো আর নতুন hash পায়,
git rev-listদিয়ে নতুন commit-গুলো দেখেwipখুঁজে পায়,exit 1. পুরো push প্রত্যাখ্যাত। - ঠিক করে আবার।
--amendনতুন commit3aede59বানায় (local hook এবার পাস), push-এ server-এর hook-ও পাস। ভালো নকশায় একই নিয়ম দুই জায়গায়: local hook দ্রুত মনে করায়, server বা CI নিশ্চিত করে।
Hook-এর মূল যুক্তি — বার্তা পড়া, কারণ জানানো, exit code ফেরত দেওয়া — Python-এ লিখলে পরীক্ষা করা সহজ। নিচের program-টি Git-এর মতোই একটি বার্তার file বানিয়ে তার path hook-এর function-কে দেয়। আসল hook হলে কারণগুলো stderr-এ লিখে sys.exit(code) করত; এখানে runner পরিষ্কার রাখতে stdout-এ লেখা আর exit code ফেরত দেওয়া হয়েছে।
import os, re, tempfile
TYPES = {"feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"}
HEADER = re.compile(r"^(?P<type>[A-Za-z]+)(\([a-z0-9-]+\))?!?: (?P<desc>\S.*)$")
PAST = {"added", "fixed", "changed", "updated", "removed"}
def check(subject): # [] means accept
if subject.startswith(("Merge ", 'Revert "', "fixup! ", "squash! ")):
return [] # messages Git itself writes
m = HEADER.match(subject)
if not m:
return ["not in the form type(scope): description"]
t = m["type"]
rules = [(t.lower() not in TYPES, f"unknown type '{t}'"),
(t.lower() in TYPES and t != t.lower(), "team rule: type in lower case"),
(len(subject) > 72, f"{len(subject)} characters (max 72)"),
(subject.endswith("."), "team rule: no full stop at the end"),
(m["desc"].split()[0].lower() in PAST, "team rule: imperative ('add', not 'added')")]
return [msg for bad, msg in rules if bad]
def commit_msg_hook(path): # Git passes the message file path
with open(path, encoding="utf-8") as f:
lines = [l.rstrip("\n") for l in f if not l.startswith("#")]
subject = next((l for l in lines if l.strip()), "")
why = check(subject)
print("ACCEPT exit 0" if not why else "REJECT exit 1", "|", subject)
for w in why: print(" -", w)
return 1 if why else 0
samples = ["fix(cart): round VAT to two decimals", "fixed stuff", "feat(cart)!: drop the old coupon API",
"Feat(auth): add OTP login", "fix(cart): Fixed rounding.", "feature(auth): add OTP login",
"docs(readme): explain every setting of the shop config, the cart and the VAT", "Merge branch 'hotfix/vat'"]
with tempfile.TemporaryDirectory() as d:
path, codes = os.path.join(d, "COMMIT_EDITMSG"), []
for s in samples:
with open(path, "w", encoding="utf-8") as f: f.write(s + "\n\n# Please enter the commit message\n")
codes.append(commit_msg_hook(path))
print("exit codes:", codes)আউটপুট:
ACCEPT exit 0 | fix(cart): round VAT to two decimals
REJECT exit 1 | fixed stuff
- not in the form type(scope): description
ACCEPT exit 0 | feat(cart)!: drop the old coupon API
REJECT exit 1 | Feat(auth): add OTP login
- team rule: type in lower case
REJECT exit 1 | fix(cart): Fixed rounding.
- team rule: no full stop at the end
- team rule: imperative ('add', not 'added')
REJECT exit 1 | feature(auth): add OTP login
- unknown type 'feature'
REJECT exit 1 | docs(readme): explain every setting of the shop config, the cart and the VAT
- 76 characters (max 72)
ACCEPT exit 0 | Merge branch 'hotfix/vat'
exit codes: [0, 1, 0, 1, 1, 1, 1, 0]
আটটি বার্তার তিনটি গ্রহণ। feat(cart)!:-এর ! Conventional Commits ১.০.০-এ breaking change-এর চিহ্ন, তাই বৈধ। Merge branch … Git নিজে লেখে, তাই নিয়মের বাইরে রাখা — না রাখলে প্রতিটি merge আটকে যেত; একইভাবে fixup! আর squash! (পর্ব ১৫-এর autosquash). লক্ষ করুন কোন কারণগুলোর আগে “team rule” লেখা: Conventional Commits-এর spec নিজে type-এর বড়-ছোট হাতের অক্ষরকে আলাদা ধরতে নিষেধ করে (শুধু BREAKING CHANGE বড় হাতের), আর শেষে দাঁড়ি বা imperative mood-এর কথা spec-এ নেই — এগুলো অনেক দলের নিজস্ব নিয়ম, অনেক commit linter-এর ডিফল্টও। Fixed rounding. দুটি নিয়ম ভেঙেছে, তাই দুটি কারণ। আর docs(readme): … ৭৬ অক্ষর, সীমা ৭২।
দলে hook ভাগ করা — framework আর সাবধানতা
core.hooksPath-এর পথ সহজ, কিন্তু প্রত্যেককে একবার চালু করতে হয় আর hook-এর dependency (linter-এর version) প্রত্যেকের কম্পিউটারে আলাদা হতে পারে। এজন্য অনেক দল একটি hook framework ব্যবহার করে। সবচেয়ে পরিচিতগুলোর একটি হলো pre-commit নামের Python tool (নামটি hook-এর নামের মতোই, তবে এটি Git-এর অংশ নয়): .pre-commit-config.yaml-এ hook-এর তালিকা আর প্রতিটির নির্দিষ্ট version লেখা থাকে, pre-commit install এটিকে .git/hooks-এ বসায়, আর CI-তে pre-commit run --all-files একই check সব file-এ চালায় (পর্ব ৪৪-এ এর YAML দেখেছেন)। JavaScript জগতে Husky-র মতো tool-ও আছে। এগুলোর আচরণ আর setting version-ভেদে বদলায় — নিজের version-এর documentation দেখে নিন।
Hook আসলে আপনার কম্পিউটারে চলা যেকোনো program. তাই Git কখনো clone-এর সাথে hook বসায় না — বসালে অচেনা repository clone করা মানেই অচেনা কোড চালানো। একই কারণে অন্যের repository-র .githooks/ বা .pre-commit-config.yaml চালু করার আগে পড়ে নিন। Commit-এর আগে password বা API key ধরার জন্য secret-scanning hook খুব কাজের — সেটি নিয়ে বিস্তারিত পর্ব ৫৭-এ (Git-এ নিরাপত্তা)। আর hook দ্রুত রাখুন: প্রতিটি commit-এ এক মিনিটের test চললে মানুষ --no-verify-এ অভ্যস্ত হয়ে যান; ভারী test-এর জায়গা pre-push বা CI.
৮ · আরও কিছু কাজের হাতিয়ার — notes, bundle, archive, clean আর rev-list
প্রতিদিন লাগে না, কিন্তু দরকারের দিনে বাঁচিয়ে দেয় — এমন কয়েকটি command. প্রথমে git notes: একটি commit প্রকাশিত হয়ে গেছে, এখন তার সাথে একটি তথ্য জুড়তে চান (load test-এর ফল, review-এর মন্তব্য), কিন্তু commit বদলালে hash বদলাবে।
$ git notes add -m 'Load-tested on staging: 200 orders/min, OK' HEAD
$ git notes show HEAD
Load-tested on staging: 200 orders/min, OK
$ git log -1
commit 9be7e1ecec407c790700c172728c9c3ee80fea1e
Author: Rahim <rahim@example.com>
Date: Thu Jan 15 10:03:00 2026 +0600
fix(cart): round VAT
Notes:
Load-tested on staging: 200 orders/min, OK
$ git notes list
3935d87280ba5f89f2b627b1474fae723e955e0c 9be7e1ecec407c790700c172728c9c3ee80fea1eCommit-এর hash 9be7e1e… একই থাকল, অথচ git log-এ নিচে “Notes:” অংশ এসেছে। Note আলাদা একটি blob (3935d87…), আর কোন commit-এর note কোনটি, সেই তালিকা থাকে refs/notes/commits ref-এ। এর মানে note সাধারণ git push-এ যায় না — git push origin refs/notes/commits আলাদা করে লাগে, আর অন্যদের fetch-এর নিয়মেও যোগ করতে হয়।
git bundle পুরো repository (বা তার একটি অংশ) একটি file-এ ভরে দেয় — যেখানে network নেই বা খুব ধীর, যেমন internet-ছাড়া একটি ল্যাবে বা নিরাপত্তার কারণে বিচ্ছিন্ন কোনো যন্ত্রে, pen drive-এ করে repository নিতে। আর git archive একটি commit বা tag-এর file-গুলো tar বা zip-এ দেয়, ইতিহাস ছাড়া — release-এর source প্রকাশ করতে।
$ git bundle create ../shop.bundle --all
$ git bundle verify ../shop.bundle
../shop.bundle is okay
The bundle contains these 4 refs:
9be7e1ecec407c790700c172728c9c3ee80fea1e refs/heads/main
cf2c40f72349a68b45125c839d292d06f10f234d refs/notes/commits
93eeb28b94af3164aff73b6f64b78db9e25aba59 refs/tags/v1.0
9be7e1ecec407c790700c172728c9c3ee80fea1e HEAD
The bundle records a complete history.
The bundle uses this hash algorithm: sha1
$ git clone -q ../shop.bundle ../copy
$ git -C ../copy log --oneline --decorate
9be7e1e (HEAD -> main, origin/main, origin/HEAD) fix(cart): round VAT
5cce398 (tag: v1.0) feat(cart): add shipping
0ccfbde feat: first version
$ git archive --format=tar --prefix=shop-1.0/ -o ../v1.0.tar v1.0
$ tar -tf ../v1.0.tar
shop-1.0/
shop-1.0/README.md
shop-1.0/ship.js
shop-1.0/vat.js--all দিয়ে সব branch, tag আর note-এর ref bundle-এ গেছে; verify জানায় bundle-টি সম্পূর্ণ (“records a complete history”) — কোনো পূর্বশর্ত commit লাগবে না। Bundle file থেকে সাধারণ URL-এর মতোই clone হয়, আর নতুন clone-এ bundle-টিই origin. পরে শুধু নতুন অংশ পাঠাতে incremental bundle বানানো যায়, যেমন git bundle create new.bundle v1.0..main; গ্রহণকারীর কাছে v1.0 আগে থেকে থাকলে git fetch new.bundle কাজ করে। Archive-এ .git নেই, শুধু চারটি entry; --prefix সব file-কে একটি folder-এর ভেতরে রাখে। কোনো file archive থেকে বাদ রাখতে .gitattributes-এ export-ignore (পর্ব ১০)।
$ git rev-list --count HEAD
3
$ git rev-list --count v1.0..HEAD
1
$ git clean -n
Would remove debug.log
$ git clean -nd
Would remove build/
Would remove debug.loggit rev-list --count HEAD HEAD থেকে পৌঁছানো commit গোনে — ৩টি; v1.0..HEAD শুধু tag-এর পরেরগুলো — ১টি। কিছু দল এই সংখ্যা build number হিসেবে ব্যবহার করে (shallow clone-এ সংখ্যাটি ভুল আসে)। git clean-এর (পর্ব ১১) -n শুধু দেখায় কী মুছবে; -d ছাড়া untracked folder (build/) ধরে না। Transcript-এ দেখানো হয়নি এমন আরেকটি রূপ git clean -i: একটি ছোট মেনু খোলে — সব মুছবেন, pattern দিয়ে কিছু বাদ দেবেন, নাকি এক এক করে জিজ্ঞেস করবেন। মোছা file Git-এ ফেরত আসে না, কারণ Git সেগুলো কখনো রাখেনি; তাই আগে সবসময় -n বা -i.
শেষে দুটি পুরোনো পরিচিতের কথা। Alias (পর্ব ১০) এই পর্বের লম্বা command-গুলো ছোট করতে পারে, যেমন git config --global alias.wt worktree বা alias.sl "stash list". আর বড় repository-তে পেছনে নিয়মিত রক্ষণাবেক্ষণের জন্য git maintenance — পর্ব ১৯-এ দেখেছেন।
আরও গভীরে: প্রতিটি hook কখন চলে, কী argument পায় আর কী করতে পারে, তার প্রামাণ্য তালিকা Git-এর githooks(5) manual page-এ (git help hooks). Stash যে আসলে W, I আর (ঐচ্ছিক) U commit, তার বর্ণনা git help stash-এর “DISCUSSION” অংশে। Cherry-pick, revert আর rebase সবাই ভেতরে একই “sequencer” ব্যবস্থা ব্যবহার করে — এজন্য তিনটিতেই --continue, --skip, --abort একরকম; আর তাদের প্রতিটি ধাপ ভেতরে একটি three-way merge (base = উৎস commit-এর parent), যার merge base আর ort strategy-র কাজ পর্ব ৫৪-এ (Merge-এর ভেতরে)।
পরীক্ষা ও ইন্টারভিউতে:
git stash applyআরpop-এর পার্থক্য? — দুটিই বদল বসায়;popসফল হলে entry মোছে, conflict হলে মোছে না;applyকখনো মোছে না।git cherry-pick A..B-তে A আসে? — না; A..B মানে B থেকে পৌঁছানো কিন্তু A থেকে নয়। A-সহ চাইলেA^..B.- একই branch দুটি worktree-তে খোলা যায় না কেন? — branch-এর ref একটাই; এক folder-এ commit করলে অন্য folder-এর index পুরোনো থাকত, পরের commit আগের বদল উল্টে দিত।
- দলের সবাইকে কীভাবে একই hook দেবেন, আর তাতে কি নিয়ম নিশ্চিত হয়? — repository-তে hook রেখে
core.hooksPathবা একটি framework; নিশ্চিত হয় না (--no-verify), তাই server-side hook বা CI-তে একই check. cherry-pick -xকী করে? — বার্তায় “(cherry picked from commit <hash>)” যোগ করে, যাতে backport-এর উৎস খুঁজে পাওয়া যায়।
৯ · AI-এর যুগে — stash, cherry-pick আর hook-এ AI-এর পরামর্শ যাচাই
এই পর্বের হাতিয়ারগুলোতে AI-এর কিছু ভুল বিশেষভাবে দামি, কারণ stash আর untracked file-এর মতো জিনিস Git-এর বাইরে কোথাও থাকে না:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Conflict-এর পরে “git reset --hard করে আবার stash pop করুন”, বা “git stash clear দিয়ে তালিকা পরিষ্কার করুন” — কোনো সতর্কতা ছাড়া | আগে git stash list আর git status; reset --hard সমাধান-করা অংশও মুছে দেয়; clear-এর আগে প্রতিটি entry show -p দিয়ে দেখুন |
“git clean -fdx চালালেই হবে” — ignored .env আর local config-ও মুছে যায় | আগে git clean -n বা -i; -x কেন লাগবে, প্রশ্ন করুন |
Hook আটকালে “--no-verify দিন” — কারণ না খুঁজে | Hook-এর বার্তা পড়ুন; নিয়মটি ভুল হলে hook ঠিক করুন, পাশ কাটাবেন না; CI তো আবার ধরবেই |
git cherry-pick A..B-তে A-ও আসবে বলে ধরে নেওয়া | git log --oneline A..B দিয়ে আগে তালিকা দেখুন |
অস্তিত্বহীন flag বা hook — git stash pop --keep, “post-push hook”, “pre-add hook” | git help stash, git help hooks-এ খুঁজুন; hook-এর তালিকা সীমিত আর নির্দিষ্ট |
“.git/hooks-এ রাখুন, clone করলে সবাই পাবে” | Hook clone-এ যায় না; repository-র folder + core.hooksPath বা framework |
একই branch দ্বিতীয় worktree-তে খোলার পরামর্শ, বা -f দিয়ে জোর করতে বলা | নতুন branch (-b) বা --detach নিন |
Version না দেখে নতুন flag (stash push --staged, sparse-checkout) পুরোনো Git-এ চালাতে বলা | git --version আর release note মিলিয়ে নিন |
নিচের prompt-এ AI-কে অবস্থার পুরো তথ্য দিয়ে, ঝুঁকিসহ ধাপ চাওয়া হয়েছে — যাতে আপনি প্রতিটি command নিজে যাচাই করতে পারেন।
AI-এর পরামর্শ হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Git-এ branch আর commit এত সস্তা, তবু stash কেন আছে? কখন stash ভুল হাতিয়ার?
Stash Git-এ এসেছে ২০০৭ সালের দিকে, Git-এর বয়স তখন দুই বছর। তখনো branch সস্তা ছিল, তবু মানুষ একটি নির্দিষ্ট অস্বস্তিতে পড়তেন: অর্ধেক কাজ commit করলে ইতিহাসে একটি অর্থহীন “WIP” commit আসে, যা পরে মুছতে বা গোছাতে হয়; আর না করলে branch বদলানো যায় না। Stash এই ফাঁকটি ভরেছে — ইতিহাস না ছুঁয়ে, এক command-এ, কয়েক মিনিটের জন্য টেবিল খালি করা। মজার ব্যাপার হলো ভেতরে stash নিজেই commit (W, I, U), শুধু কোনো branch-এ নয়, একটি বিশেষ ref-এর reflog-এ। অর্থাৎ Git-এর নকশাকারীরা নতুন কোনো সংরক্ষণ-ব্যবস্থা বানাননি, পুরোনো ব্যবস্থার ওপর একটি সুবিধাজনক “ইন্টারফেস” বসিয়েছেন — এটি ভালো নকশার একটি উদাহরণ: নতুন ধারণা কম, নতুন ব্যবহার বেশি।
কিন্তু সুবিধার দামও আছে। Stash-এর কোনো নাম নেই, শুধু নম্বর, আর নম্বর বদলায়; এটি push হয় না, তাই কম্পিউটার নষ্ট হলে বা নতুন clone-এ হারায়; আর বার্তা ছাড়া stash এক সপ্তাহ পরে কেউ চিনতে পারেন না। Reflog-এ থাকায় ভুল করে মুছে ফেলা stash কিছুদিন খুঁজে আনা যায় (পর্ব ৫৩), তবে সেটি নিরাপত্তার জাল, পরিকল্পনা নয়। তাই অভিজ্ঞ developer-দের মধ্যে একটি প্রচলিত পরামর্শ: stash মিনিট বা ঘণ্টার জন্য; এক দিনের বেশি হলে একটি WIP branch-এ commit করে push করুন, পরে interactive rebase-এ গুছিয়ে নিন।
আরেকটি ভুল ব্যবহার হলো stash দিয়ে “দুটি কাজ একসাথে” চালানো — বারবার push, switch, pop. সেখানে worktree অনেক কম ঝুঁকির, কারণ দুটি কাজের file আসলেই আলাদা folder-এ থাকে, কিছু তাকে তুলতে বা নামাতে হয় না, conflict-এর সুযোগও কম। আর stash pop-এর conflict মানুষকে প্রায়ই আতঙ্কিত করে, অথচ entry তখনো নিরাপদে আছে — এটা জানা থাকলে অর্ধেক ভয় কমে যায়।
মূল উপলব্ধি: Stash হলো ইতিহাসের বাইরে অল্প সময়ের তাক; সময় লম্বা হলে branch, আর একসাথে দুই কাজ হলে worktree — হাতিয়ার বাছাই হয় কাজের সময়কাল আর ঝুঁকি দেখে।
প্র ০২বড় open source project-গুলো কয়েক বছরের পুরোনো release-এ fix পৌঁছায় কীভাবে? Cherry-pick-নির্ভর backport-এর ঝুঁকি কী?
অনেক project একসাথে কয়েকটি release “চালু” রাখে — যেমন একটি সর্বশেষ, আর দীর্ঘমেয়াদি সমর্থনের (LTS) এক-দুটি পুরোনো। Linux kernel-এর stable branch-এর প্রক্রিয়া প্রকাশ্যে লেখা আছে: fix আগে মূল ধারায় (mainline) ঢোকে, তারপর stable-এর রক্ষণাবেক্ষণকারীরা সেটি পুরোনো branch-এ আনেন, আর stable-এর patch-এ মূল ধারার commit-এর id উল্লেখ করা হয় — ঠিক -x-এর মতো চিন্তা। নিয়মের মূল যুক্তি “upstream first”: fix যদি শুধু পুরোনো release-এ থাকে, পরের বড় release-এ একই bug ফিরে আসে, যাকে বলে regression.
ঝুঁকিগুলো বাস্তব। এক, পুরোনো branch-এর কোড সময়ের সাথে আলাদা হয়ে যায়, তাই fix প্রায়ই হুবহু বসে না — হাতে মেলাতে হয়, আর হাতে মেলানো কোড মূল fix-এর মতো পরীক্ষিত নয়। এই পর্বে দেখেছেন, হাতে মেলানো pick-কে git cherry আর “একই” বলে চেনে না। দুই, একটি fix প্রায়ই আগের কোনো refactor-এর ওপর নির্ভর করে; শুধু fix-টি pick করলে compile হয় না, বা আরও খারাপ, compile হয় কিন্তু ভুল আচরণ করে। তিন, ইতিহাসে একই বদল দুই hash-এ থাকায় পরে দুই branch merge করলে বা bisect চালালে বিভ্রান্তি।
Microsoft-এর প্রকৌশলী Raymond Chen তাঁর পরিচিত blog-এ ২০১৮ সালের দিকে একটি ধারাবাহিকে যুক্তি দিয়েছিলেন যে অনেক ক্ষেত্রে cherry-pick-এর বদলে fix-কে একটি সাধারণ ভিত্তি থেকে branch বানিয়ে দুই দিকে merge করলে ইতিহাস পরিষ্কার থাকে। আবার অনেক দল উল্টো পথে চলে আর ভালো থাকে — নিয়মিত, ছোট, -x-সহ cherry-pick, সাথে প্রতিটি backport-এর জন্য release branch-এ CI. দুটি পথের মিল একটাই: নিয়ম লিখিত, সবাই একভাবে করে, আর প্রতিটি backport পরীক্ষিত।
মূল উপলব্ধি: Backport একটি প্রক্রিয়া, একটি command নয় — উৎস চিহ্নিত রাখা (-x), upstream first, আর প্রতিটি release branch-এ আলাদা test ছাড়া cherry-pick ঝুঁকিপূর্ণ।
প্র ০৩Git কেন clone-এর সাথে hook দেয় না, আর এর সাথে software supply chain-এর নিরাপত্তার সম্পর্ক কী?
Hook মানে Git-এর কোনো কাজের সময় স্বয়ংক্রিয়ভাবে চলা program. যদি clone-এর সাথে repository-র hook চালু হয়ে যেত, তাহলে একটি অচেনা repository clone করে git checkout বা git commit করামাত্র সেই repository-র লেখকের কোড আপনার কম্পিউটারে, আপনার সব অধিকার নিয়ে চলত — আপনার SSH key, browser-এর তথ্য, অন্য project-এর secret সবই তার নাগালে। তাই Git-এর নকশায় hook সবসময় স্থানীয় সিদ্ধান্ত: .git/hooks repository-র ইতিহাসের অংশ নয়, আর core.hooksPath প্রত্যেককে নিজে চালু করতে হয়। এটি “secure by default” নীতি — সুবিধা কম, কিন্তু অজান্তে কোড চলার পথ বন্ধ।
তবু আক্রমণকারীরা এই সীমানা ভাঙার পথ খোঁজেন। ২০২৪ সালের মে মাসে Git-এর একটি নিরাপত্তা-release (CVE-2024-32002 নামে পরিচিত দুর্বলতাসহ) জানায়, বিশেষভাবে বানানো submodule আর symbolic link-এর মিশ্রণে কিছু system-এ clone --recurse-submodules-এর সময় Git-কে দিয়ে একটি hook লিখিয়ে চালানো সম্ভব ছিল — অর্থাৎ শুধু clone করেই কোড চলতে পারত। সমাধান ছিল Git আপডেট করা। এর শিক্ষা: “clone নিরাপদ” ধারণাটি Git-এর একটি সচেতন প্রতিশ্রুতি, আর সেটি রক্ষা করতে Git-কে নিয়মিত আপডেট রাখা আপনার দায়িত্ব।
দলের hook framework-এও একই প্রশ্ন আসে। .pre-commit-config.yaml আসলে বলে, “এই URL থেকে এই tool-এর এই version নামিয়ে চালান” — অর্থাৎ এটি একটি dependency তালিকা, যার নিরাপত্তা package manager-এর মতোই ভাবতে হয়: version pin করা, বিশ্বস্ত উৎস, আর পরিবর্তন review করা। একটি ক্ষতিকর hook বা প্রতিস্থাপিত tool প্রতিটি developer-এর কম্পিউটারে চলবে। Server-side hook-এর ঝুঁকি আরেক রকম: সেটি server-এর অধিকারে চলে, তাই সেখানে least privilege — hook শুধু যা দরকার তা পড়বে, বাইরের input বিশ্বাস করবে না।
মূল উপলব্ধি: যা স্বয়ংক্রিয়ভাবে কোড চালায়, তা-ই আক্রমণের পথ; Git hook-কে ইচ্ছাকৃতভাবে স্থানীয় আর opt-in রেখেছে, আর দলকেও hook-কে dependency-র মতো সাবধানে সামলাতে হয়।
প্র ০৪Client-side hook আর CI দুটোই একই check চালালে সেটা কি অপচয়? “Shift left”-এর যুক্তি আর তার সীমা কী?
“Shift left” মানে ভুল যত আগে ধরা যায়, তত সস্তা — software engineering-এর একটি পুরোনো পর্যবেক্ষণ, Boehm-সহ অনেকের লেখায় ভুল দেরিতে ধরা পড়লে সারানোর খরচ বাড়ার কথা আছে (নির্দিষ্ট অনুপাতগুলো নিয়ে অবশ্য বিতর্ক আছে)। Commit-এর মুহূর্তে বার্তার format বা formatting-এর ভুল ধরা পড়লে ঠিক করতে কয়েক সেকেন্ড লাগে; push-এর পরে CI-তে ধরা পড়লে নতুন commit, নতুন pipeline, অপেক্ষা; আর merge-এর পরে ধরা পড়লে অন্যদের কাজও আটকায়। এই হিসাবে local hook দারুণ।
কিন্তু local hook নিশ্চয়তা দিতে পারে না, তিনটি কারণে। এক, --no-verify — Git ইচ্ছা করেই মানুষকে পাশ কাটানোর পথ দেয়। দুই, hook চালু থাকা প্রত্যেকের নিজের ব্যাপার; নতুন সদস্য core.hooksPath সেট করতে ভুলতেই পারেন। তিন, প্রত্যেকের কম্পিউটারে tool-এর version, OS, path আলাদা — একই hook একজনের কাছে পাস, আরেকজনের কাছে ব্যর্থ। আর web interface থেকে করা edit বা অন্য tool-এর commit hook-ই দেখে না। তাই যে নিয়ম ভাঙলে সত্যিকারের ক্ষতি (ভাঙা build, ফাঁস হওয়া secret, নিয়মবহির্ভূত release), সেটি এমন জায়গায় যাচাই হতে হবে, যেখানে কেউ পাশ কাটাতে পারে না: server-side hook, বা protected branch-এর সাথে বাধ্যতামূলক CI check.
তাহলে একই check দুইবার কি অপচয়? না, কারণ দুটির উদ্দেশ্য আলাদা: local hook developer-এর সুবিধা (দ্রুত feedback), CI দলের নিশ্চয়তা। ভালো নকশায় একটিই উৎস — যেমন একটি config file বা script — দুই জায়গা থেকে চালানো হয়, যাতে নিয়ম আলাদা হয়ে না যায়। Local অংশ দ্রুত রাখা হয় (শুধু বদলানো file, কয়েক সেকেন্ড), ভারী test CI-তে। এই ভারসাম্য নষ্ট হলে দুই দিকেই ক্ষতি: ধীর hook মানুষকে --no-verify-এ অভ্যস্ত করে, আর CI ছাড়া শুধু hook-এ ভরসা করলে একদিন নিয়মভাঙা commit main-এ ঢোকে।
মূল উপলব্ধি: Local hook সুবিধা, server আর CI নিশ্চয়তা; একই নিয়ম একই উৎস থেকে দুই জায়গায় চালালে দ্রুত feedback আর নির্ভরযোগ্যতা দুটোই মেলে।
প্র ০৫একাধিক clone, stash আর worktree — একসাথে কয়েকটি কাজের জন্য কোনটি কখন? আজকের AI coding tool-এর যুগে worktree আবার আলোচনায় কেন?
Worktree আসার আগে (Git ২.৫, ২০১৫) একসাথে দুই branch-এ কাজ করার পথ ছিল দুটি: stash করে বারবার switch, অথবা একই repository-র দ্বিতীয় clone. Git-এর contrib folder-এ git-new-workdir নামে একটি script symbolic link দিয়ে একই .git ভাগ করত, কিন্তু কোনো তালা ছাড়া। git worktree সেই ধারণাটিকে নিরাপদ নিয়মসহ Git-এর মূল অংশে এনেছে: আলাদা HEAD আর index, ভাগ করা object আর ref, আর branch-এর তালা।
তিনটির তুলনা করলে: stash সবচেয়ে হালকা, কিন্তু এক সময়ে একটিই কাজ খোলা, আর ফিরিয়ে আনতে conflict-এর ঝুঁকি। দ্বিতীয় clone পুরোপুরি আলাদা — একটিতে ভুল হলে অন্যটি নিরাপদ, ভিন্ন remote বা ভিন্ন config রাখা যায় — কিন্তু পুরো ইতিহাস আবার নামে আর দুই clone-এর branch আলাদা থাকে, তাই এক clone-এর commit অন্যটিতে আনতে push-fetch লাগে। Worktree মাঝামাঝি: নতুন folder সেকেন্ডে তৈরি, ইতিহাস একটাই, এক folder-এর commit অন্য folder-এ সাথে সাথে দৃশ্যমান; কিন্তু একই branch দুই জায়গায় নয়, আর প্রতিটি folder-এ build-এর ফল ও dependency আলাদা, তাই disk খরচ শূন্য নয়।
সাম্প্রতিক সময়ে worktree নতুন করে আলোচনায় এসেছে AI coding agent-এর কারণে: একাধিক agent বা একাধিক AI সেশন একসাথে আলাদা কাজ করলে প্রত্যেককে আলাদা worktree আর আলাদা branch দিলে তারা একে অপরের file-এ হাত দেয় না, আর মানুষ পরে প্রতিটি branch আলাদাভাবে review করতে পারেন। কিছু tool এই ব্যবস্থা নিজেই করে বলে জানা যায়; বিস্তারিত tool-ভেদে আলাদা আর দ্রুত বদলায়। মূল নীতিটি অবশ্য পুরোনো: একসাথে চলা কাজের মধ্যে ভাগ করা জিনিস যত কম, সংঘর্ষ তত কম — operating system-এর process আলাদা রাখার মতোই।
মূল উপলব্ধি: একসাথে কয়েকটি কাজের জন্য প্রশ্ন হলো কী ভাগ করবেন আর কী আলাদা রাখবেন; worktree ইতিহাস ভাগ করে আর working folder আলাদা রাখে — মানুষ বা AI, দুইয়ের জন্যই।
অনুশীলন
আউটপুট অনুমান, command-এর ক্রম, একটি hook লেখা আর নকশা-সমালোচনা — প্রতিটির পূর্ণ উত্তর আছে; Git-এর অংশ আসল Git ২.৫৩ দিয়ে যাচাই করা।
-
আউটপুট অনুমান — range pick আর stash: (ক) একটি সরলরেখার ইতিহাস, পুরোনো থেকে নতুন:
93306c4(tagv1.4.0) →911374efeat coupon →ef0492cfix VAT →b1845f3docs →4321f1cfix qty.git switch -c p1 v1.4.0তারপরgit cherry-pick 911374e..4321f1cচালালে কয়টি commit তৈরি হবে, কোনগুলো, কোন ক্রমে?git rev-list --count v1.4.0..p1কত দেখাবে? (খ) একটি পরিষ্কার repository-তেf.txtআরg.txtআছে। নিচের ক্রমে প্রতিটিstash listআরstatus --shortকী দেখাবে?$ echo one >> f.txt; git stash push -q -m one $ echo two >> g.txt; git stash push -q -m two $ git stash apply -q stash@{1} $ git stash list # (১) $ git status --short # (২) $ git stash pop -q $ git stash list # (৩) $ git status --short # (৪)(ক) তিনটি:
ef0492c,b1845f3,4321f1c-এর বদল, এই ক্রমে —911374eনিজে বাদ। Git ২.৫৩-এর আসল ফল:$ git cherry-pick 911374e..4321f1c [p1 0a40977] fix(cart): round VAT to 2 decimals Date: Thu Jan 15 10:03:00 2026 +0600 1 file changed, 1 insertion(+), 1 deletion(-) [p1 ecba8c4] docs: explain VAT rounding Date: Thu Jan 15 10:04:00 2026 +0600 1 file changed, 1 insertion(+) [p1 6436489] fix(cart): reject zero or negative qty Date: Thu Jan 15 10:05:00 2026 +0600 1 file changed, 2 insertions(+), 1 deletion(-) $ git log --oneline 6436489 fix(cart): reject zero or negative qty ecba8c4 docs: explain VAT rounding 0a40977 fix(cart): round VAT to 2 decimals 93306c4 feat: first release of the cart $ git rev-list --count v1.4.0..p1 3প্রতিটি নতুন commit-এর hash মূলটির থেকে আলাদা, কারণ parent আলাদা (যেমন coupon-এর commit নেই)।
Date:লাইন আসে কারণ author-এর সময় পুরোনো। কোনো conflict হয়নি, কারণ এই তিনটি commit coupon-এর file ছোঁয় না।(খ)
$ git stash push -q -m one $ git stash push -q -m two $ git stash apply -q stash@{1} $ git stash list stash@{0}: On main: two stash@{1}: On main: one $ git status --short M f.txt $ git stash pop -q $ git stash list stash@{0}: On main: one $ git status --short M f.txt M g.txt(১)
applyentry মোছে না — দুটিই আছে। (২)stash@{1}(“one”) বসেছে, তাইf.txtবদলানো। (৩)popডিফল্টেstash@{0}(“two”) নেয় আর সফল হওয়ায় মুছে দেয়; “one” উঠে আসেstash@{0}-তে। লক্ষ করুন, working tree পরিষ্কার না থাকলেও pop সফল — কারণ “two” শুধুg.txtছোঁয়, আরg.txt-এ স্থানীয় কোনো বদল নেই। একই file-এ স্থানীয় বদল থাকলে Git “Your local changes … would be overwritten” বলে থামত। (৪) দুটি file-ই বদলানো। আর “one” এখনো তালিকায় — কাজ শেষেgit stash dropনা করলে পড়ে থাকবে। -
Command-এর ক্রম — worktree-তে hotfix আর backport: আপনি
feature/search-এ মাঝপথে, কয়েকটি file বদলানো আর একটি নতুন untracked file. এমন সময় productionrelease/2.0-তে একটি bug. দলের নিয়ম: fix আগে main-এ (upstream first), তারপর release-এ, উৎসসহ। আপনার চলমান কাজ ছোঁয়া যাবে না, stash-ও নয়। ধাপে ধাপে command লিখুন — fix main-এ commit ও push, release-এ backport, যাচাই, আর শেষে পরিষ্কার।$ git fetch origin $ git worktree add ../app-fix -b fix/price-rounding origin/main $ cd ../app-fix $ # fix the bug, run the tests $ git commit -am "fix(price): round to 2 decimals" $ git push -u origin fix/price-rounding # open a pull request, merge into main $ git fetch origin $ git log --oneline origin/main -3 # note the merged fix commit, e.g. a1b2c3d $ git switch -c backport/price-2.0 origin/release/2.0 $ git cherry-pick -x a1b2c3d $ git log -1 --format=%B # "(cherry picked from commit a1b2c3d…)" $ git cherry -v origin/release/2.0 origin/main | grep a1b2c3d # "+" until merged $ git push -u origin backport/price-2.0 # pull request into release/2.0, CI runs $ cd ../app $ git worktree remove ../app-fix $ git worktree listমূল সিদ্ধান্তগুলো: নতুন worktree
origin/main-এর সর্বশেষ অবস্থা থেকে, তাই আপনারfeature/searchfolder, untracked file-সহ, অক্ষত।a1b2c3dএখানে উদাহরণের hash; আসলটিgit logথেকে নেবেন। Squash merge হলে main-এ ঢোকা commit-এর hash আলাদা — সেটিই pick করবেন। Release-এ সরাসরি না বসিয়ে একটি backport branch আর pull request, যাতে release branch-এর CI চলে আর কেউ review করেন। শেষেremove(বদলানো file থাকলে Git রাজি হবে না, সেটিই নিরাপত্তা), আর merge হয়ে গেলে branch দুটিও মুছতে পারেন। Conflict হলে: সমাধান,git add,git cherry-pick --continue, আর বার্তায় কী মেলানো হলো তার এক লাইন। -
একটি hook লিখুন — pre-push: দলের নিয়ম: কেউ সরাসরি
main-এ push করবেন না, সব কাজ pull request দিয়ে। একটি POSIX shellpre-pushhook লিখুন, যা remote-এরrefs/heads/main-এ যেকোনো push আটকায়, কারণসহ বার্তা দেয়, আর অন্য branch-এ push হতে দেয়। দলে কীভাবে ভাগ করবেন, আর এটি কেন যথেষ্ট নয়?#!/bin/sh # pre-push hook: $1 = remote name, $2 = remote URL # stdin: one line per ref: <local ref> <local oid> <remote ref> <remote oid> while read local_ref local_oid remote_ref remote_oid; do if [ "$remote_ref" = "refs/heads/main" ]; then echo "pre-push: direct push to main is blocked; open a pull request" >&2 exit 1 fi done exit 0এই pre-push hook stdin-এর প্রতিটি লাইনে চারটি মান — local ref, local hash, remote ref, remote hash — পড়ে, আর remote ref
refs/heads/mainহলে stderr-এ বার্তা লিখেexit 1. Argument$1,$2(remote-এর নাম, URL) দরকার হয় শুধু নির্দিষ্ট remote-এর জন্য নিয়ম চাইলে। ভাগ করা আর পরীক্ষা, Git ২.৫৩-এ আসল ফল (hooksrc/folder-এ লেখা file-টি repository-র.githooks/-এ কপি করা হয়েছে, তারপর একটি local bare repository-কে remote বানিয়ে push):$ mkdir .githooks $ cp ../hooksrc/pre-push .githooks/ $ git add --chmod=+x .githooks/pre-push $ git config core.hooksPath .githooks $ git commit -q -m "chore: add pre-push hook" $ git push origin main pre-push: direct push to main is blocked; open a pull request error: failed to push some refs to '../srv.git' $ git switch -q -c feat/login $ git push -q origin feat/login $ git ls-remote --heads origin c34d58642dcbe91e94344990c7ed4bb6236da71b refs/heads/feat/loginMain-এ push আটকে গেছে, hook-এর বার্তাসহ;
feat/loginগেছে, remote-এ শুধু সেটিই। কেন যথেষ্ট নয়:git push --no-verifyhook এড়ায়; যিনিcore.hooksPathসেট করেননি, তাঁর কাছে hook নেই; আর web interface থেকে বদল hook দেখে না। আসল নিয়ম তাই hosting platform-এর protected branch (main-এ সরাসরি push নিষেধ, pull request আর CI বাধ্যতামূলক) বা নিজের server-এ pre-receive hook; local hook শুধু আগেভাগে মনে করিয়ে দেয়। উন্নতির জায়গা: branch মোছার push (local hash শূন্য) আলাদা বার্তা, আর জরুরি অবস্থার জন্য একটি লিখিত প্রক্রিয়া —--no-verify-এর গোপন অভ্যাস নয়। -
নকশা-সমালোচনা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের দলনেতা প্রস্তাব দিলেন: “(১) সব check — format, lint, পুরো test suite (৮ মিনিট) — pre-commit hook-এ চলবে; তাই CI লাগবে না। (২) জরুরি হলে সবাই
--no-verifyব্যবহার করবেন, কাউকে জানাতে হবে না। (৩) অর্ধেক কাজ রাখার জন্য সবাই stash ব্যবহার করবেন, দরকার হলে কয়েক সপ্তাহ। (৪) Release-এর প্রতিটি fix release branch-এ আগে, পরে সময় পেলে main-এ cherry-pick.” প্রতিটির সমস্যা আর বিকল্প বলুন।(১) প্রতিটি commit-এ ৮ মিনিট মানে মানুষ commit কম করবেন বা
--no-verify-এ অভ্যস্ত হবেন; আর hook সবার কাছে চালু থাকার নিশ্চয়তা নেই, এক কম্পিউটারের tool-version অন্যটির থেকে আলাদা। বিকল্প: pre-commit-এ শুধু দ্রুত check (বদলানো file-এর format আর lint, কয়েক সেকেন্ড), pre-push-এ দ্রুত unit test, আর পুরো test suite CI-তে, protected branch-এ বাধ্যতামূলক।(২) লুকানো বাইপাস নিয়মকে অর্থহীন করে। জরুরি পথ থাকুক, কিন্তু দৃশ্যমান: CI আর server-এর check তখনো চলে, আর জরুরি merge-এর পরে একটি follow-up issue.
(৩) Stash স্থানীয়, push হয় না, নম্বর বদলায়, নাম ছাড়া চেনা যায় না — কয়েক সপ্তাহে কম্পিউটার নষ্ট হলে কাজ হারায়। বিকল্প: WIP branch-এ commit করে push (পরে interactive rebase-এ গুছিয়ে), আর একসাথে দুটি কাজ চললে worktree. Stash থাকুক মিনিট-ঘণ্টার জন্য, সবসময়
-m-সহ।(৪) “পরে সময় পেলে” main-এ আনলে পরের release-এ একই bug ফিরে আসে (regression). বিকল্প upstream first: fix আগে main-এ (review আর CI-সহ), তারপর
cherry-pick -xদিয়ে release-এ; fix যদি শুধু পুরোনো কোডের জন্য হয়, release-এ সরাসরি, বার্তায় কারণসহ। প্রতিটি backport-এর জন্য release branch-এ আলাদা CI.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমfeature/discount-এ অর্ধেক কাজ (একটি untracked file-সহ) রেখে main-এ hotfix করে ফিরে আসার ধাপগুলো সাজান (▲▼ চেপে সরান)
git stash push -u -m "discount: half-done badge"git switch main- Bug ঠিক করে
git commit -am "fix(cart): …" git switch feature/discountgit stash pop(conflict হলে সমাধান করেgit stash drop)
git stash pop চালানোর পরে Git “CONFLICT (content): Merge conflict in app.js” দেখাল। এখন stash-এর তালিকায় সেই entry-র কী অবস্থা?
- Entry মুছে গেছে; শুধু reflog থেকে ফেরানো যাবে
- Pop বাতিল হয়েছে, working tree আগের মতো, entry অক্ষত
- Entry তালিকায় রয়ে গেছে; সমাধানের পরে নিজে
git stash dropকরতে হবে - Entry-টি conflict-সহ নতুন নম্বরে আবার সংরক্ষিত হয়েছে
একটি সরলরেখার ইতিহাসে পুরোনো থেকে নতুন commit P → Q → R → S. একটি আলাদা branch-এ git cherry-pick Q..S চালালে কোন commit-গুলোর বদল আসবে?
- Q, R আর S
- R আর S
- শুধু R
- P, Q, R আর S
git cherry-pick -x ef0492c সফল হলে নতুন commit সম্পর্কে কোনটি সত্য?
- এর hash
ef0492c-ই থাকে, শুধু branch বদলায় - Author হিসেবে pick করা ব্যক্তির নাম বসে, মূল লেখকের নাম মুছে যায়
- এটি একটি merge commit, যার দ্বিতীয় parent
ef0492c - বার্তার শেষে “(cherry picked from commit ef0492c…)” লাইন থাকে, আর hash নতুন
মূল folder-এ feature/discount checkout করা আছে। একই repository থেকে git worktree add ../review feature/discount চালালে Git ২.৫৩ কী করে?
- “fatal: 'feature/discount' is already used by worktree at …” বলে থামে
- নিজে থেকে detached HEAD-এ নতুন folder খোলে
- পুরো ইতিহাসসহ একটি দ্বিতীয় clone বানায়
- মূল folder-কে main-এ switch করে নতুন folder-এ branch-টি খোলে
একটি দল hook-গুলো repository-র .githooks/ folder-এ commit করেছে। নতুন সদস্য repository clone করে commit করলেন, কিন্তু কোনো hook চলল না। সবচেয়ে সম্ভাব্য কারণ?
- Windows-এ Git hook চালাতে পারে না
- তিনি
core.hooksPathসেট করেননি; clone নিজে থেকে hook চালু করে না - Hook folder-এর নাম
.git/hooksছাড়া অন্য কিছু হলে Git কখনো চালায় না -mদিয়ে বার্তা দিলে commit-msg hook চলে না
একজন developer git commit --no-verify দিয়ে নিয়ম-ভাঙা বার্তার commit বানিয়ে push করলেন। কোনটি এই push আটকাতে পারে?
- তাঁর কম্পিউটারের commit-msg hook
- তাঁর কম্পিউটারের post-commit hook
- Server-এর
pre-receivehook (বা বাধ্যতামূলক CI check-সহ protected branch) - Server-এর
post-receivehook
Cone mode-এ git sparse-checkout set services/payments-এর পরে নিচের কোন file working tree-তে থাকবে না? (Repository-তে আছে: README.md, services/README.md, services/payments/main.txt, services/catalog/main.txt)
services/catalog/main.txtREADME.mdservices/README.mdservices/payments/main.txt
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Stash = commit-এর একটি stack (
refs/stash-এর reflog): সবসময়-m, নতুন file থাকলে-u;applyentry রাখে,popসফল হলে মোছে, conflict হলে রাখে — তখন সমাধান করে নিজেdrop. পুরোনো stash-এর জন্যstash branch. - Cherry-pick একটি commit-এর diff নতুন commit হিসেবে বসায়: backport-এ
-x, conflict-এadd+--continue(বা--skip/--abort), merge-এর জন্য-m 1.A..B-তে A বাদ,A^..B-তে A-সহ। - Duplicate চেনা:
git cherry -vpatch-id দেখে-/+দেয়; হাতে মেলানো pick আলাদা দেখায়, তখন-x-এর লেখা উৎস-hash ভরসা। - Worktree = এক
.git, অনেক folder;add/list/remove/prune; একটি branch একসাথে একটি worktree-তেই। Sparse checkout-এর cone mode:setতালিকা বদলায়,addযোগ করে,disableসব ফেরায়। - Hook exit code দিয়ে কাজ থামায়;
.git/hooksclone-এ আসে না, দলে ভাগ করতেcore.hooksPathবা framework;--no-verifyclient hook এড়ায়, তাই server-side hook বা CI-তে একই নিয়ম।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৭ · Git-এ নিরাপত্তা — signed commit, secret ফাঁস ঠেকানো আর history পরিষ্কারপরবর্তী পর্বSecret-scanning hook, signed commit আর ফাঁস হওয়া secret সামলানো।
- পর্ব ৫৫ · Git দিয়ে bug খোঁজা — bisect, blame আর log -Sআগের পর্বকোন commit bug আনল, binary search দিয়ে খোঁজা।
- পর্ব ১৭ · Branching strategy — Git Flow, GitHub Flow আর trunk-based developmentসম্পর্কিতRelease branch, hotfix আর upstream first — cherry-pick-এর প্রেক্ষাপট।
- পর্ব ৫৮ · Continuous integration — pipeline, build, test আর দ্রুত feedbackসম্পর্কিতHook-এর নিয়ম যেখানে বাধ্যতামূলকভাবে আবার যাচাই হয়।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Operating Systems · Shell কীভাবে কাজ করে — job control, ps, top আর /procপাশের ট্র্যাকShell কীভাবে program চালায় আর process-এর অবস্থা দেখায় — hook চলার পেছনের ব্যবস্থা।
- Python · regular expression — লেখার আকৃতি দিয়ে খোঁজাপাশের ট্র্যাকCommit বার্তা যাচাইয়ের pattern লেখা আর পরীক্ষা করা।