এই পর্বে যা শিখবেন
- Branch কেন লাগে, আর Git-এ branch কেন এত সস্তা —
.gitফোল্ডারের ভেতরে নিজের চোখে দেখে - HEAD কী, commit করলে কোন pointer সরে আর কোনটি সরে না
- Branch বানানো, তালিকা দেখা, বদলানো, নাম বদলানো আর নিরাপদে মোছা — আসল Git আউটপুটসহ
- Commit না করা পরিবর্তন নিয়ে switch কখন যায় আর কখন Git আটকে দেয়
- Detached HEAD-এ ঢোকা, সেখানে কাজ করা, কাজ বাঁচানো আর হারানো commit উদ্ধার
- Branch-এর নামের নিয়ম ও রীতি, আর long-lived বনাম short-lived branch
১ · Branch কেন — একই project-এ একসাথে কয়েকটি কাজ
একটি কাল্পনিক অনলাইন বাজার অ্যাপের কথা ভাবুন। রহিম অ্যাপে মোবাইল-পেমেন্টের নতুন ফর্ম বানাচ্ছেন — কাজটি তিন দিনের। দ্বিতীয় দিন দুপুরে খবর এলো: cart-এর মোট দামে পয়সার হিসাবে গোলমাল, গ্রাহকেরা অভিযোগ করছেন, আজই ঠিক করতে হবে। রহিমের অর্ধেক লেখা payment কোড এখনো চলার মতো নয়। এখন তিনি কী করবেন?
পুরোনো দিনের উপায় ছিল project folder-টি কপি করা — bazar-app-payment, bazar-app-fix, bazar-app-final-2… কিছুদিনের মধ্যেই কেউ বলতে পারেন না কোন folder-এ কোন পরিবর্তন। Version control-এর পুরো উদ্দেশ্যই এই বিশৃঙ্খলা দূর করা (পর্ব ৭-এ, Version control কী)। আর Git-এ এর উত্তর হলো branch: একই repository-র ভেতরে কাজের আলাদা আলাদা ধারা।
🧩 সমান্তরাল কাজ
রহিম payment-এ, করিম search-এ, সাথী login-এ — প্রত্যেকে নিজের branch-এ। কেউ কারও অর্ধেক কাজে বাধা দেন না।
feature/payment, feature/search🩹 জরুরি সংশোধন
অর্ধেক কাজ এক পাশে রেখে মূল কোড থেকে নতুন branch, ছোট সংশোধন, দ্রুত প্রকাশ। তারপর আবার নিজের কাজে ফেরা।
fix/cart-total🧪 পরীক্ষা-নিরীক্ষা
“grid layout দেখতে কেমন লাগে?” — একটি branch-এ চেষ্টা করুন। ভালো না লাগলে branch মুছে ফেলুন; মূল কোডে কোনো দাগ থাকে না।
experiment/grid📦 প্রকাশের প্রস্তুতি
কিছু দল প্রকাশের আগে একটি release branch রাখে, যেখানে শুধু bug-fix ঢোকে। কোন দল কী নিয়ম মানে, সেটি পর্ব ১৭-এর বিষয়।
release/1.4একটি মোটা রেসিপি খাতার কথা ভাবুন, যার প্রতিটি পাতায় আগের পাতার পরের সংস্করণ লেখা। আপনি খাতার ভেতরে কয়েকটি রঙিন ফিতে (বুকমার্ক) রেখেছেন — লাল ফিতেতে লেখা “ঈদের বিরিয়ানি”, নীল ফিতেতে “কম তেলের সংস্করণ”। নতুন রেসিপি চেষ্টা করতে চাইলে খাতা কপি করেন না, শুধু একটি নতুন ফিতে লাগান। আর আঙুল দিয়ে যে ফিতেটি ধরে আছেন — সেটিই “আপনি এখন কোথায়”। Git-এ ফিতেগুলো হলো branch, আর আঙুলটি হলো HEAD.
এই উপমার আসল শিক্ষা একটাই: branch ইতিহাসের কপি নয়, ইতিহাসের একটি জায়গায় লাগানো নাম। Commit-গুলো আছে একবারই, একটি সাধারণ ভাণ্ডারে; branch শুধু বলে “এই commit থেকে পেছনের দিকে যা আছে, সেটাই আমার কাজ।” পর্ব ৮-এ (প্রথম Git repository) আপনি দেখেছেন প্রতিটি commit তার আগের commit-কে (parent) মনে রাখে। এই parent-এর তীরগুলো মিলে তৈরি হয় একটি commit graph — গণিতের ভাষায় directed acyclic graph বা DAG: তীরের দিক আছে, কিন্তু ঘুরে আগের জায়গায় ফেরার কোনো চক্র নেই।
Branch-এর ধারণা Git-এর আগেও ছিল, কিন্তু খরচ ছিল অন্যরকম। CVS-এ branch লাগানো হতো প্রতিটি ফাইলে আলাদা করে, বড় project-এ যা ধীর ছিল। Subversion (SVN) branch বানাত পুরো folder-এর একটি “সস্তা কপি” হিসেবে — বানানো দ্রুত হলেও merge-এর হিসাব রাখার ব্যবস্থা যোগ হয় অনেক পরে, ২০০৮ সালের দিকে সংস্করণ ১.৫-এ। ফলে অনেক দল branch এড়িয়ে চলত। ২০০৫ সালে Linus Torvalds যখন Linux kernel-এর জন্য Git লেখেন, তাঁর লক্ষ্য ছিল হাজারো মানুষের সমান্তরাল কাজ সামলানো; তাই Git-এ branch শুরু থেকেই একটি commit-এর দিকে ছোট্ট নির্দেশক মাত্র।
২ · Branch আসলে কী — একটি ছোট্ট ফাইলে একটি commit hash
কথা নয়, চোখে দেখি। নিচের প্রতিটি আউটপুট এই কোর্সের জন্য একটি খালি scratch folder-এ Git ২.৫৩ চালিয়ে নেওয়া। লেখক Rahim, সময় ঠিক করে দেওয়া (১৫ জানুয়ারি ২০২৬, সকাল ১০টা থেকে প্রতি commit-এ এক মিনিট করে) — তাই একই কমান্ড একই ক্রমে চালালে আপনিও হুবহু এই commit hash পাবেন। Repository-তে দুটি commit আছে: “Add product list” আর “Add cart page”. এবার একটি branch বানাই:
$ git branch feature/payment
$ git branch
feature/payment
* main
$ git branch -v
feature/payment ee98167 Add cart page
* main ee98167 Add cart page
$ cat .git/HEAD
ref: refs/heads/main
$ cat .git/refs/heads/main
ee981670c64fd4824100b660959c4be958cb034f
$ cat .git/refs/heads/feature/payment
ee981670c64fd4824100b660959c4be958cb034fgit branch feature/payment কিছুই ছাপেনি — নিঃশব্দে কাজ শেষ। git branch তালিকা দেখায়; তারকা (*) মানে আপনি এখন কোন branch-এ। -v যোগ করলে প্রতিটি branch কোন commit-এ আছে (ছোট hash আর বার্তা) সেটিও দেখা যায় — দুটোই ee98167-এ। তারপর আসল রহস্য: .git/refs/heads/main আর .git/refs/heads/feature/payment দুটি সাধারণ text ফাইল, দুটিতেই একই ৪০ অক্ষরের hash. নতুন branch মানে নতুন একটি ফাইল, ৪০ অক্ষর আর একটি newline — মোট ৪১ বাইট। কোনো কোড কপি হয়নি। আর .git/HEAD-এ লেখা ref: refs/heads/main — অর্থাৎ HEAD একটি hash রাখে না, রাখে একটি branch-এর নাম।
এই ফাইলগুলোকে Git বলে ref (reference). Branch থাকে refs/heads/-এর নিচে, tag থাকে refs/tags/-এর নিচে। আর HEAD হলো একটি symbolic ref: সে অন্য একটি ref-এর নাম ধরে রাখে। তাই HEAD থেকে commit-এ পৌঁছাতে দুই লাফ: HEAD → refs/heads/main → ee98167.
একটি commit-এর আগে আর পরে — কোন তীর সরে
Before and after "git commit": only the current branch pointer moves
main-এর ফাইলে নতুন hash লিখে দেয়। HEAD নিজে বদলায় না — সে আগের মতোই “main” বলে।Commit স্থির, branch চলমান। একবার তৈরি commit আর কখনো বদলায় না (তার hash-ই তার ভেতরের সবকিছুর ছাপ)। কিন্তু branch একটি চলমান pointer: প্রতিটি নতুন commit-এ সে এক ঘর সামনে সরে। তাই “main branch-এ কী আছে?” প্রশ্নের নিখুঁত উত্তর: “main এখন যে commit-এ আছে, সেখান থেকে parent-এর তীর ধরে পেছনে যত commit পৌঁছানো যায় — সবগুলো।”
একটি মজার প্রান্তিক অবস্থা: একদম নতুন repository-তে git init-এর পর .git/HEAD-এ লেখা থাকে ref: refs/heads/main (বা আপনার সেটিং অনুযায়ী অন্য নাম), কিন্তু refs/heads/main ফাইলটি তখনো নেই — কারণ দেখানোর মতো কোনো commit-ই নেই! একে বলে unborn branch. এ অবস্থায় git branch কিছুই ছাপে না। প্রথম commit হলে তবেই ফাইলটি জন্মায়।
৩ · HEAD — “আপনি এখন কোথায়”, আর কোন pointer কখন সরে
মেলার মাঠে বড় মানচিত্রে একটি লাল বিন্দু থাকে: “আপনি এখানে”। HEAD ঠিক সেই বিন্দু। HEAD দুটি কাজ করে: (১) working tree-এ কোন commit-এর ফাইল সাজানো আছে তা ঠিক করে, আর (২) পরের commit কোথায় বসবে — তার parent কে হবে, আর কোন branch সামনে সরবে — তা ঠিক করে।
নিচের ডায়াগ্রামে রহিমের repository-র আসল ঘটনাগুলো ধাপে ধাপে দেখুন। নিচের সারণিতে .git-এর ভেতরের ফাইলগুলোতে ঠিক কী লেখা থাকে, সেটিও বদলাতে থাকবে। খেয়াল রাখুন, প্রতিটি commit-এ কোন লেবেল সরে।
| HEAD | |
| refs/heads/main | |
| refs/heads/feature/payment | |
| refs/heads/fix/cart-total | |
| শেষ কমান্ড |
- শুরু। দুটি commit, A আর B. শুধু একটি branch,
main, যা B (ee98167) commit-এ আছে। HEAD-এ লেখাref: refs/heads/main— আপনি main-এ আছেন। git branch feature/payment. নতুন একটি ref ফাইল তৈরি হলো, তাতেও B-র hash. কোনো নতুন commit হয়নি, কোনো ফাইল বদলায়নি, আর HEAD এখনো main-এই। Branch বানানো মানে শুধু একটি নতুন লেবেল লাগানো — তাতে “চলে যাওয়া” হয় না।git switch feature/payment. এবার শুধু HEAD ফাইলটি বদলাল:ref: refs/heads/feature/payment. দুটি branch একই commit-এ, তাই working tree-র একটি ফাইলও বদলাতে হয়নি।- প্রথম commit (C,
9c3478e). C-এর parent হলো HEAD-এর commit, অর্থাৎ B. Git সামনে সরাল শুধু HEAD যে branch-এর নাম রাখে — feature/payment, এখন C-তে। main B-তেই রয়ে গেল। - দ্বিতীয় commit (D,
fc2d7cf). feature/payment আরও এক ঘর সামনে, D-তে। main তখনো B-তে। দুই branch-এর মধ্যে এখন দুটি commit-এর পার্থক্য। git switch main. HEAD আবার main-এর নাম রাখে। Git working tree-কে B-র অবস্থায় ফিরিয়ে দিল:payment.txtফাইলটি folder থেকে সরে গেল (মুছে যায়নি — সেটি C আর D commit-এ নিরাপদে আছে)। feature/payment D-তেই অপেক্ষা করছে।git switch -c fix/cart-total. একসাথে দুটি কাজ: B-তে নতুন branch বানানো আর তাতে চলে যাওয়া। HEAD এখনref: refs/heads/fix/cart-total. B-তে এখন দুটি লেবেল — main আর fix/cart-total.- Commit E (
c78d7eb). E-এর parent B, আর সরল শুধু fix/cart-total. এখন B থেকে দুটি আলাদা পথ: ওপরে C–D, নিচে E. একেই বলে divergent history — দুই branch একটি সাধারণ পূর্বসূরি (B) থেকে আলাদা দিকে গেছে। এগুলো আবার মেলানো পর্ব ১৩-এর (Merge আর conflict) কাজ।
একই ধারণাটি একটি ছোট্ট মডেলে লিখে ফেলা যায়। নিচের কোডে commit-গুলো একটি object-এ রাখা, প্রতিটি শুধু তার parent মনে রাখে; branch একটি নাম → commit id-র তালিকা; HEAD হয় { ref: "main" } (একটি branch-এর নাম) নয়তো { id: "C2" } (সরাসরি commit, পরের অংশে দেখবেন)। চালিয়ে দেখুন প্রতিটি কমান্ডের পর কোন branch-এর ইতিহাস লম্বা হয়:
// A tiny model of Git refs. Commits form a graph; a branch is only a name
// pointing at one commit (like .git/refs/heads/<name>); HEAD names the branch you are on.
const commits = {}; // id -> { parent }
const branches = {}; // name -> commit id
let HEAD = { ref: "main" }; // attached: { ref }, detached: { id }
let n = 0;
const headId = () => (HEAD.ref ? branches[HEAD.ref] : HEAD.id);
function commit() {
const id = "C" + ++n;
commits[id] = { parent: headId() || null };
if (HEAD.ref) branches[HEAD.ref] = id; // only the CURRENT branch moves
else HEAD.id = id;
}
const branch = (name) => { branches[name] = headId(); }; // new pointer, no new commit
const sw = (name) => { HEAD = { ref: name }; };
function history(id) { // follow parent links back to the root
const out = [];
for (; id; id = commits[id].parent) out.unshift(id);
return out.join("--");
}
const run = [
["git commit (C1)", commit], ["git commit (C2)", commit],
["git branch feature", () => branch("feature")],
["git switch feature", () => sw("feature")],
["git commit (C3)", commit], ["git commit (C4)", commit],
["git switch main", () => sw("main")],
["git commit (C5)", commit],
["git switch -c fix", () => { branch("fix"); sw("fix"); }],
];
for (const [cmd, act] of run) {
act();
console.log("$ " + cmd);
for (const [name, id] of Object.entries(branches))
console.log(" " + name.padEnd(8) + history(id) + (HEAD.ref === name ? " <- HEAD" : ""));
}আউটপুট (অংশবিশেষ; প্রথম দুই কমান্ডে শুধু main-এর লাইন C1, তারপর C1--C2):
$ git branch feature
main C1--C2 <- HEAD
feature C1--C2
$ git switch feature
main C1--C2
feature C1--C2 <- HEAD
$ git commit (C3)
main C1--C2
feature C1--C2--C3 <- HEAD
…
$ git commit (C5)
main C1--C2--C5 <- HEAD
feature C1--C2--C3--C4
$ git switch -c fix
main C1--C2--C5
feature C1--C2--C3--C4
fix C1--C2--C5 <- HEAD
প্রতিটি লাইন একটি branch-এর পুরো ইতিহাস — branch-এর commit থেকে parent ধরে পেছনে হেঁটে পাওয়া। খেয়াল করুন: git branch feature-এর পর দুটি লাইন হুবহু এক, কারণ দুটি নাম একই commit দেখায়। commit() ফাংশনে মূল লাইন একটিই — branches[HEAD.ref] = id — তাই শুধু HEAD-এর branch এগোয়। শেষে main আর feature-এর ইতিহাসের সাধারণ অংশ C1--C2, তারপর দুজন দুই দিকে: এটাই divergent history. আর fix বানাতে কোনো commit কপি হয়নি — main-এর ইতিহাসই তার ইতিহাস।
৪ · Branch বানানো, বদলানো আর দেখা — switch, checkout আর log --graph
স্টেপারের ঘটনাগুলো আসল Git-এ এভাবে দেখায়। প্রথমে feature/payment-এ গিয়ে দুটি commit:
$ git switch feature/payment
Switched to branch 'feature/payment'
$ git commit -m "Add payment form"
[feature/payment 9c3478e] Add payment form
1 file changed, 1 insertion(+)
create mode 100644 payment.txt
$ git commit -m "Validate phone number"
[feature/payment fc2d7cf] Validate phone number
1 file changed, 1 insertion(+)
$ git branch -v
* feature/payment fc2d7cf Validate phone number
main ee98167 Add cart pageCommit-এর প্রথম লাইনে বন্ধনীর ভেতরে Git নিজেই বলে দেয় কোন branch-এ commit হলো: [feature/payment 9c3478e]. (প্রতিটি commit-এর আগে git add চালানো হয়েছে, জায়গা বাঁচাতে দেখানো হয়নি।) git branch -v-এ দেখুন — feature/payment এগিয়ে fc2d7cf-এ, main পড়ে আছে ee98167-এ।
তারপর main-এ ফিরে জরুরি সংশোধনের branch, একটি commit, আর main-কে সেই সংশোধন পর্যন্ত এগিয়ে নেওয়া:
$ git switch main
Switched to branch 'main'
$ git switch -c fix/cart-total
Switched to a new branch 'fix/cart-total'
$ git commit -m "Fix cart total rounding"
[fix/cart-total c78d7eb] Fix cart total rounding
1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
$ git merge --ff-only fix/cart-total
Updating ee98167..c78d7eb
Fast-forward
cart.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --all
* c78d7eb (HEAD -> main, fix/cart-total) Fix cart total rounding
| * fc2d7cf (feature/payment) Validate phone number
| * 9c3478e Add payment form
|/
* ee98167 Add cart page
* 1195e12 Add product listgit merge --ff-only এখানে শুধু main-এর pointer-কে ee98167 থেকে c78d7eb-এ সরিয়ে দিয়েছে (fast-forward) — কোনো নতুন commit হয়নি, কারণ main-এর commit আগে থেকেই fix/cart-total-এর ইতিহাসে ছিল। Merge-এর পুরো গল্প পর্ব ১৩-এ। এবার graph পড়ুন, নিচ থেকে ওপরে: 1195e12 → ee98167, তারপর |/ চিহ্নে রাস্তা দুই ভাগ। বাঁ দিকের লাইনে c78d7eb, যেখানে দুটি লেবেল — HEAD -> main (আপনি main-এ, main এখানে) আর fix/cart-total. ডান দিকের লাইনে feature/payment-এর দুটি commit. বন্ধনীর লেবেলগুলো terminal-এ Git নিজে থেকেই দেখায় (log.decorate-এর default “auto”); আউটপুট ফাইলে পাঠালে লেবেল চাইলে --decorate যোগ করুন। --all না দিলে শুধু HEAD থেকে পৌঁছানো যায় এমন commit দেখাত — feature/payment-এর লাইনটি থাকত না।
switch বনাম checkout — একই কাজের দুই প্রজন্ম
Git ২.২৩ (২০১৯ সালের আগস্ট) পর্যন্ত branch বদলানো, নতুন branch বানানো, আর ফাইলকে পুরোনো অবস্থায় ফেরানো — তিনটিই হতো একটি কমান্ডে, git checkout. একটি কমান্ডের এত কাজ নতুনদের প্রায়ই বিভ্রান্ত করত: git checkout main branch বদলায়, কিন্তু git checkout -- cart.txt ফাইলের অসংরক্ষিত পরিবর্তন মুছে দেয়! তাই ২.২৩-এ দুটি নতুন কমান্ড আসে: branch-এর জন্য git switch, ফাইলের জন্য git restore (পর্ব ১১-এ, ভুল শোধরানো)। শুরুতে এদের “experimental” চিহ্ন ছিল; Git ২.৫৩-এর documentation-এ সেই সতর্কতা আর নেই। checkout বাদ পড়েনি — পুরোনো script আর টিউটোরিয়ালে আপনি এটি প্রচুর দেখবেন।
| কাজ | Git ২.২৩+ (পরামর্শ) | পুরোনো সমতুল্য |
|---|---|---|
| অন্য branch-এ যাওয়া | git switch fix/x | git checkout fix/x |
| নতুন branch বানিয়ে তাতে যাওয়া | git switch -c fix/x | git checkout -b fix/x |
| নির্দিষ্ট commit থেকে নতুন branch | git switch -c fix/x ee98167 | git checkout -b fix/x ee98167 |
| শুধু branch বানানো, না যাওয়া | git branch fix/x (দুই প্রজন্মেই একই) | |
| আগের branch-এ ফেরা | git switch - | git checkout - |
| কোনো commit-এ detached HEAD | git switch --detach ee98167 | git checkout ee98167 |
একটি ছোট নিরাপত্তা-পার্থক্য: git switch শুধু branch-এর নাম নেয়। Commit-এর hash দিলে সে থেমে যায়:
$ git switch ee98167
fatal: a branch is expected, got commit 'ee98167'
hint: If you want to detach HEAD at the commit, try again with the --detach option.অর্থাৎ detached হতে চাইলে আপনাকে সজ্ঞানে --detach লিখতে হয়। git checkout ee98167 কোনো প্রশ্ন ছাড়াই detached করে দেয় (অংশ ৬-এ তার বার্তা দেখবেন)। নতুনদের জন্য switch-এর এই কড়াকড়িই সুবিধা।
তালিকা দেখার বিকল্পগুলো
| কমান্ড | যা দেখায় |
|---|---|
git branch | স্থানীয় সব branch; * চিহ্ন বর্তমানটিতে |
git branch -v | সাথে প্রতিটির শেষ commit-এর ছোট hash আর বার্তা (-vv দিলে upstream-ও) |
git branch -a | স্থানীয় আর remote-tracking, দুই রকমই (-r দিলে শুধু remote-tracking) |
git branch --merged | যেসব branch-এর commit HEAD থেকে পৌঁছানো যায় — অর্থাৎ কাজ ইতিমধ্যে HEAD-এ ঢুকে গেছে |
git branch --no-merged | যেসব branch-এ এমন commit আছে যা HEAD-এ এখনো নেই |
git branch --show-current | শুধু বর্তমান branch-এর নাম (detached হলে কিছুই না) — script-এ কাজের |
$ git branch --merged
fix/cart-total
* main
$ git branch --no-merged
feature/payment
$ git branch -a
feature/mobile-payment
feature/payment2
* main
remotes/origin/mainপ্রথম দুটি কমান্ড চালানো হয়েছে ওপরের graph-এর অবস্থায়: fix/cart-total-এর commit c78d7eb main থেকে পৌঁছানো যায়, তাই “merged”; feature/payment-এর fc2d7cf যায় না। শেষ কমান্ডটি একই repository-তে পরে চালানো (ততক্ষণে কয়েকটি branch মুছে, নাম বদলে, একটি স্থানীয় bare repository-কে origin নামে যোগ করে main push করা হয়েছে — সেগুলো নিচে দেখবেন)। remotes/origin/main একটি remote-tracking branch: remote-এ main শেষবার কোথায় ছিল তার স্থানীয় স্মৃতি। এর বিস্তারিত পর্ব ১৪-এ (Remote repository).
৫ · Commit না করা পরিবর্তন নিয়ে switch — কখন যায়, কখন আটকায়
Branch বদলানো মানে working tree-কে অন্য commit-এর অবস্থায় সাজানো। কিন্তু আপনার folder-এ যদি এমন পরিবর্তন থাকে যা এখনো commit হয়নি? Git-এর নিয়মটি সরল আর নিরাপদ: আপনার অসংরক্ষিত কাজ Git কখনো চুপচাপ মুছে দেয় না।
- যে ফাইল দুই branch-এ হুবহু এক: সেটি বদলানোর দরকারই নেই, তাই আপনার পরিবর্তনসহ ফাইলটি নতুন branch-এ “সাথে চলে যায়”।
- যে ফাইল দুই branch-এ আলাদা, আর আপনিও সেটি বদলেছেন: Git-কে ফাইলটি নতুন branch-এর সংস্করণে বদলাতে হতো, তাতে আপনার কাজ হারাত। তাই Git পুরো switch বাতিল করে, কিছুই বদলায় না।
$ git status --short
M README.md
$ git switch feature/payment
Switched to branch 'feature/payment'
M README.md
$ git status --short
M README.mdmain-এ README.md-এ একটি লাইন যোগ করা হয়েছে, commit করা হয়নি ( M মানে working tree-তে বদলানো)। README.md main আর feature/payment-এ একই, তাই switch সফল, আর Git জানিয়ে দেয় M README.md সাথে এসেছে। এটি সুবিধা আবার ফাঁদও: পরিবর্তনটি কোনো branch-এর “মালিকানায়” নেই — যে branch-এ commit করবেন, সেখানেই ঢুকবে। ভুল branch-এ commit হয়ে যাওয়ার একটি সাধারণ কারণ এটি।
$ git status --short
M cart.txt
$ git switch feature/payment
error: Your local changes to the following files would be overwritten by checkout:
cart.txt
Please commit your changes or stash them before you switch branches.
Aborting
$ git branch --show-current
mainএবার main-এ cart.txt বদলানো হয়েছে। main-এর cart.txt-এ rounding-এর সংশোধন আছে (c78d7eb), feature/payment-এ নেই — দুই branch-এ ফাইলটি আলাদা। Switch করলে আপনার পরিবর্তন মুছে যেত, তাই Git বলল Aborting, আর --show-current দেখাচ্ছে আপনি এখনো main-এ। (বার্তায় “checkout” শব্দটি দেখে অবাক হবেন না — switch আর checkout ভেতরে একই যন্ত্র ব্যবহার করে।) Git ২.৫৩-এ বার্তার ভাষা এমন; পুরোনো সংস্করণে সামান্য আলাদা হতে পারে।
আটকে গেলে আপনার সামনে কয়েকটি পথ:
| পথ | কমান্ড | কখন |
|---|---|---|
| Commit করে ফেলা | git add + git commit | কাজটি এই branch-এরই, আর একটি অর্থপূর্ণ ধাপে পৌঁছেছে। অসম্পূর্ণ হলে “WIP” commit করে পরে --amend (পর্ব ১১) বা rebase (পর্ব ১৫) দিয়ে গোছানো যায়। |
| সাময়িকভাবে সরিয়ে রাখা | git stash, পরে git stash pop | হাতের কাজ পাঁচ মিনিটের জন্য সরাতে হবে। Stash-এর পুরো ব্যবহার পর্ব ৫৬-এ (Git-এর শক্তিশালী হাতিয়ার)। |
| পরিবর্তন সাথে নিয়ে মেলানোর চেষ্টা | git switch -m feature/payment | পরিবর্তনটি আসলে অন্য branch-এ যাওয়ার কথা। Git একটি ছোট merge চেষ্টা করে; conflict হলে চিহ্ন বসায় — conflict বোঝার পর (পর্ব ১৩) ব্যবহার করুন। |
| পরিবর্তন ফেলে দেওয়া | git switch --discard-changes বা -f | নিশ্চিত যে কাজটি লাগবে না। এই পরিবর্তন commit হয়নি, তাই reflog-ও ফেরাতে পারবে না — ফেরত নেই। |
অচেনা worktree-ও লুকিয়ে থাকতে পারে: একই repository-র একটি branch অন্য একটি linked worktree-তে খোলা থাকলে Git সেই branch-এ switch করতে দেয় না (পর্ব ৫৬-এ worktree). আর একটি সাধারণ পরামর্শ — branch বদলানোর আগে অভ্যাস করে git status দেখুন। পরিষ্কার working tree-তে switch সবচেয়ে কম বিস্ময় দেয়।
৬ · Detached HEAD — branch ছাড়া একটি commit-এ দাঁড়ানো
এখন পর্যন্ত HEAD সবসময় একটি branch-এর নাম রেখেছে। কিন্তু আপনি যদি সরাসরি একটি commit-এর hash (বা একটি tag) checkout করেন — যেমন পুরোনো সংস্করণে bug ছিল কি না দেখতে? তখন কোন branch-কে সামনে সরাতে হবে, Git জানে না। তাই HEAD সরাসরি hash ধরে রাখে। এই অবস্থার নাম detached HEAD — HEAD কোনো branch থেকে “আলগা”। Git ২.৫৩-এর হুবহু বার্তা:
$ git checkout ee98167
Note: switching to 'ee98167'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at ee98167 Add cart page
$ cat .git/HEAD
ee981670c64fd4824100b660959c4be958cb034f
$ git branch
* (HEAD detached at ee98167)
feature/payment
fix/cart-total
mainবার্তাটি ভয় দেখানোর জন্য নয়, পথ দেখানোর জন্য। এর মূল কথা তিনটি: (১) আপনি ঘুরে দেখতে, এমনকি commit-ও করতে পারেন; (২) সেই commit কোনো branch-এ যুক্ত হবে না, তাই branch-এ ফিরে গেলে সেগুলো “ফেলে আসা” হবে; (৩) রাখতে চাইলে git switch -c <নাম>. এবার .git/HEAD-এ আর ref: … নেই — সরাসরি ৪০ অক্ষরের hash. git branch-এর তারকাটিও কোনো branch-এর পাশে নয়, বসেছে (HEAD detached at ee98167)-এর পাশে।
কখন detached HEAD স্বাভাবিক? পুরোনো release-এর tag দেখা (git switch --detach v1.0), git bisect দিয়ে bug খোঁজা (পর্ব ৫৫) — Git নিজেই প্রতিটি ধাপে আপনাকে একটি commit-এ detached রাখে, rebase চলার মাঝখানে (পর্ব ১৫), আর অনেক CI ব্যবস্থায় build চালানোর জন্য একটি নির্দিষ্ট commit checkout করা হয়। সমস্যা হয় শুধু তখন, যখন কেউ না বুঝে সেখানে কাজ করেন আর branch না বানিয়ে চলে যান। নিচে ঠিক সেটাই ঘটছে — আর তারপর উদ্ধার:
| .git/HEAD | |
| main | |
| experiment/grid | |
| F-এ পৌঁছানো যায় কোথা থেকে | |
| কমান্ড |
- শুরু। আপনি main-এ, main আছে
c78d7eb-এ। HEAD একটি branch-এর নাম রাখছে — স্বাভাবিক, “attached” অবস্থা। git checkout ee98167. পুরোনো commit B-তে গেলেন। HEAD-এ এখন সরাসরি hash — detached. Working tree B-র অবস্থায় (cart.txt-এ rounding-এর সংশোধন নেই)। কোনো branch নড়েনি।- এখানেই একটি commit (F). Git নিয়ম মেনেই commit বানাল: parent = HEAD-এর commit (B). কিন্তু সামনে সরানোর মতো কোনো branch নেই, তাই সরল শুধু HEAD — সে এখন F-এর hash রাখে। F-এ পৌঁছানোর একমাত্র পথ এখন HEAD. আসল আউটপুটের প্রথম লাইনে লেখা
[detached HEAD 2cbf7b9]. git switch main. HEAD আবার main-এর নাম রাখে। F-এর দিকে আর কোনো নাম নেই — F কোনো branch থেকে reachable নয়।git log --allF-কে আর দেখায় না! Git যাওয়ার সময় সতর্ক করে: “you are leaving 1 commit behind” আর hash-টিও বলে দেয়।git reflog. সতর্কবার্তা হারিয়ে ফেললেও উপায় আছে। Reflog হলো HEAD কখন কোথায় ছিল তার স্থানীয় খাতা। সেখানেHEAD@{1}: commit: Try grid layout— F-এর hash2cbf7b9.git branch experiment/grid 2cbf7b9. F-এ একটি নাম লাগানো হলো। এখন F একটি branch থেকে reachable,git log --all-এ আবার দেখা যায়, আর garbage collection একে মুছবে না। উদ্ধার শেষ।
আসল Git-এ ধাপ ৩ থেকে ৬:
$ git commit -m "Try grid layout"
[detached HEAD 2cbf7b9] Try grid layout
1 file changed, 1 insertion(+)
create mode 100644 layout.txt
$ git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
2cbf7b9 Try grid layout
If you want to keep it by creating a new branch, this may be a good time
to do so with:
git branch <new-branch-name> 2cbf7b9
Switched to branch 'main'
$ git reflog -5
c78d7eb (HEAD -> main, fix/cart-total) HEAD@{0}: checkout: moving from 2cbf7b91df472dd80f0b9e90329a8f0977a75b66 to main
2cbf7b9 HEAD@{1}: commit: Try grid layout
ee98167 HEAD@{2}: checkout: moving from main to ee98167
c78d7eb (HEAD -> main, fix/cart-total) HEAD@{3}: checkout: moving from feature/payment to main
fc2d7cf (feature/payment) HEAD@{4}: checkout: moving from main to feature/payment
$ git branch experiment/grid 2cbf7b9
$ git log --oneline --graph --all
* 2cbf7b9 (experiment/grid) Try grid layout
| * c78d7eb (HEAD -> main, fix/cart-total) Fix cart total rounding
|/
| * fc2d7cf (feature/payment) Validate phone number
| * 9c3478e Add payment form
|/
* ee98167 Add cart page
* 1195e12 Add product listReflog নতুন থেকে পুরোনো ক্রমে পড়ুন: HEAD@{2}-এ main থেকে ee98167-এ যাওয়া (detached), HEAD@{1}-এ সেখানে commit, HEAD@{0}-এ main-এ ফেরা। উদ্ধারের পর graph-এ তিনটি শাখা — সবগুলোর গোড়া ee98167. সবচেয়ে সহজ পথ অবশ্য ধাপ ৩-এর পরেই, যাওয়ার আগে: detached অবস্থায় দাঁড়িয়েই git switch -c experiment/grid চালালে HEAD-এর বর্তমান commit-এ branch তৈরি হয়ে তাতে চলে যেত — বার্তাটি ঠিক এই পরামর্শই দিয়েছিল। Reflog-এর আরও ব্যবহার পর্ব ১১-এ (ভুল শোধরানো), আর ref ও reflog-এর ভেতরের গঠন পর্ব ৫৩-এ।
$ git switch --detach main~1
HEAD is now at ee98167 Add cart page
$ git status
HEAD detached at ee98167
nothing to commit, working tree clean
$ git switch -c hotfix/old-cart
Switched to a new branch 'hotfix/old-cart'main~1 মানে “main-এর এক ধাপ আগের commit”. git switch-এ hash বা ~1-এর মতো কিছু দিলে --detach লাগে। git status-এর প্রথম লাইন সবসময় বলে দেয় আপনি detached কি না — তাই অচেনা অবস্থায় প্রথমে status. তারপর switch -c সেই commit-এ নতুন branch বানিয়ে HEAD-কে তাতে জুড়ে দিল; এখন থেকে commit করলে hotfix/old-cart এগোবে।
হারানো commit-গুলো চিরকাল থাকে না। Reflog-এর ঘর একসময় মেয়াদোত্তীর্ণ হয় — default হিসেবে, এখন কোনো branch থেকে পৌঁছানো যায় না এমন commit-এর ঘর ৩০ দিন পর (gc.reflogExpireUnreachable), বাকি ঘর ৯০ দিন পর (gc.reflogExpire). তারপর garbage collection (git gc, যা Git মাঝে মাঝে নিজেই চালায়) কোথাও থেকে পৌঁছানো যায় না এমন পুরোনো object মুছে দিতে পারে। আর reflog শুধু আপনার কম্পিউটারের — clone করা অন্য কপিতে এটি যায় না। নিচের মডেলে পুরো ঘটনাটি: কোন commit কোনো branch থেকে reachable নয়, তা হিসাব করে বের করা হয়, তারপর reflog থেকে উদ্ধার:
// Detached HEAD: commit, walk away, find the commit again through the reflog.
// Ids come from FNV-1a 32-bit, a small stand-in for Git's SHA-1 (not secure).
function fnv1a(s) {
let h = 0x811c9dc5;
for (const ch of s) { h ^= ch.charCodeAt(0); h = Math.imul(h, 0x01000193) >>> 0; }
return h.toString(16).padStart(8, "0").slice(0, 7);
}
const commits = {}, branches = {}, reflog = []; let HEAD = { ref: "main" };
const tip = () => (HEAD.ref ? branches[HEAD.ref] : HEAD.id);
function commit(msg) {
const id = fnv1a((tip() || "") + "|" + msg);
commits[id] = { parent: tip() || null };
if (HEAD.ref) branches[HEAD.ref] = id; else HEAD.id = id;
reflog.unshift({ id, why: "commit: " + msg });
}
function move(to, detached) {
const from = HEAD.ref || HEAD.id;
HEAD = detached ? { id: to } : { ref: to };
reflog.unshift({ id: tip(), why: "checkout: moving from " + from + " to " + to });
}
function orphans() { // commits no branch can reach any more
const seen = new Set();
for (const t of Object.values(branches))
for (let id = t; id && !seen.has(id); id = commits[id].parent) seen.add(id);
return Object.keys(commits).filter((id) => !seen.has(id));
}
commit("Add product list"); commit("Add cart page"); commit("Fix cart total");
move(commits[branches.main].parent, true); // git checkout <main~1>: detached
commit("Try grid layout");
move("main", false); // git switch main
console.log("commits:", Object.keys(commits).join(" "), "| orphaned:", orphans().join(" "));
reflog.slice(0, 3).forEach((e, i) => console.log(`${e.id} HEAD@{${i}}: ${e.why}`));
const found = reflog.find((e) => e.why === "commit: Try grid layout").id;
branches["experiment/grid"] = found; // git branch experiment/grid <id>
console.log("rescued", found, "| orphaned now:", orphans().length);আউটপুট:
commits: 7e653bd 692e270 860d6cd 4fbc1bd | orphaned: 4fbc1bd
860d6cd HEAD@{0}: checkout: moving from 4fbc1bd to main
4fbc1bd HEAD@{1}: commit: Try grid layout
692e270 HEAD@{2}: checkout: moving from main to 692e270
rescued 4fbc1bd | orphaned now: 0
এখানে id আসল Git-এর SHA-1 নয় — parent-এর id আর বার্তা মিলিয়ে FNV-1a নামের একটি ছোট hash, শুধু “বিষয়বস্তু থেকে পরিচয়” ধারণাটি দেখাতে। চারটি commit-এর মধ্যে 4fbc1bd (Try grid layout) কোনো branch থেকে পৌঁছানো যায় না — orphans() প্রতিটি branch থেকে parent ধরে হেঁটে যা ছোঁয় তা বাদ দিয়ে বাকিটা দেয়। Reflog-এর তিনটি লাইন আসল Git-এর মতোই সাজানো। শেষে experiment/grid নামটি লাগাতেই অনাথ commit-এর সংখ্যা শূন্য। খেয়াল করুন, মডেলে commit কখনো মোছা হয়নি — শুধু তার দিকের নামটি হারিয়েছিল। আসল Git-এও তাই: “commit হারানো” মানে প্রায় সবসময় “নাম হারানো”।
৭ · Merged কি না, আর নিরাপদে branch মোছা
Branch মোছা মানে শুধু ref ফাইলটি মোছা — commit-গুলো মোছা নয়। কিন্তু সেই branch-ই যদি কোনো commit-এ পৌঁছানোর একমাত্র পথ হয়, তবে নামটি মুছলে commit-গুলো ঠিক ওপরের F-এর মতো অনাথ হবে। তাই Git দুটি আলাদা বিকল্প রাখে:
🛡️ git branch -d (নিরাপদ)
শুধু তখনই মোছে, যখন branch-টি “fully merged”: Git ২.৫৩-এর documentation অনুযায়ী, branch-এর upstream থাকলে তাতে, না থাকলে HEAD-এ। অর্থাৎ ওর commit অন্য পথে এখনো reachable.
--delete⚠️ git branch -D (জোর করে)
--delete --force-এর সংক্ষেপ। কোনো যাচাই নেই। Merge না হওয়া কাজের দিকে আর কোনো নাম না থাকলে সেই commit শুধু reflog-এ টিকে থাকে, মেয়াদ পর্যন্ত।
| স্থানীয় branch | |
| কমান্ড | |
| Git-এর যাচাই | |
| ফল |
- শুরু। চারটি branch. fix/cart-total আর main একই commit-এ (
c78d7eb); feature/payment (fc2d7cf) আর experiment/grid (2cbf7b9) main-এর ইতিহাসে নেই। -d fix/cart-total— সফল। এর commit main থেকে পৌঁছানো যায়, তাই নাম মুছলে কিছুই হারায় না। Git বার্তায় পুরোনো hash-ও বলে দেয় (was c78d7eb) — ভুল হলে সেই hash দিয়ে আবার বানানো যায়।-d experiment/grid— প্রত্যাখ্যাত।2cbf7b9main-এর ইতিহাসে নেই। নাম মুছলে commit অনাথ হতো, তাই Git থামাল আর পরামর্শ দিল: নিশ্চিত হলে-D.-D experiment/grid— জোর করে মোছা। Grid layout-এর পরীক্ষা বাতিল, তাই সজ্ঞানে মোছা হলো। Commit-টি এখন শুধু reflog আর বার্তার hash-এর ভরসায়।-d feature/payment— প্রত্যাখ্যাত, আর এখানে সেটাই ঠিক। Payment-এর কাজ এখনো merge হয়নি। এখানে -D চালানো হতো আসল ভুল। একটি সতর্কতা: “main-এ merged নয়” মানে সবসময় “কাজ হারাবে” নয় — অন্য কোনো branch-এ বা remote-এ সেটি থাকতে পারে। -D-র আগেgit log main..feature/paymentদিয়ে দেখে নিন কোন commit-গুলো শুধু এই branch-এ।-mদিয়ে নাম বদল। মোছার বদলে নামটি আরও স্পষ্ট করা হলো।-mনতুন নামে আগে থেকে branch থাকলে অস্বীকার করে;-Mজোর করে চাপিয়ে দেয়। বর্তমান branch-এর নাম বদলাতে শুধুgit branch -m নতুন-নামলিখলেই হয়।
$ git branch -d fix/cart-total
Deleted branch fix/cart-total (was c78d7eb).
$ git branch -d experiment/grid
error: the branch 'experiment/grid' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D experiment/grid'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
$ git branch -D experiment/grid
Deleted branch experiment/grid (was 2cbf7b9).
$ git branch -d feature/payment
error: the branch 'feature/payment' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feature/payment'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
$ git branch -m feature/payment feature/mobile-payment
$ git branch -v
feature/mobile-payment fc2d7cf Validate phone number
* main c78d7eb Fix cart total rounding
$ git branch -d main
error: cannot delete branch 'main' used by worktree at '…/repo'শেষ লাইনটিও শিক্ষণীয়: আপনি যে branch-এ দাঁড়িয়ে আছেন, সেটি মোছা যায় না — আগে অন্য branch-এ যেতে হয়। (Folder-এর পুরো ঠিকানা এখানে … দিয়ে ছোট করা হয়েছে।) hint: লাইনগুলো Git ২.৫৩-এর; পুরোনো সংস্করণে hint-এর ভাষা বা উপস্থিতি আলাদা হতে পারে, কিন্তু not fully merged-এর মূল বার্তাটি বহু বছর ধরে একই।
--merged-এর পেছনের হিসাব — reachability
“X কি T-তে merged?” প্রশ্নের নিখুঁত অর্থ: X-এর commit কি T-এর commit থেকে parent-এর তীর ধরে পৌঁছানো যায়? এটি গ্রাফে একটি সাধারণ reachability পরীক্ষা — DFS বা BFS (DSA কোর্সের DFS পর্ব দেখুন)। Merge commit-এর দুটি parent থাকে, তাই হাঁটার সময় দুটি পথই ধরতে হয়। নিচের মডেলে একটি ছোট্ট কাল্পনিক graph: feature/login আগে main-এ merge হয়েছে (merge commit M), feature/payment হয়নি।
// git branch --merged / --no-merged as a reachability test on the commit graph.
// A branch is "merged into T" when its tip can be reached from T's tip by
// following parent links (a merge commit has two parents).
const parents = {
A: [], B: ["A"], C: ["B"], D: ["C"], // main line
E: ["B"], F: ["E"], // feature/login, later merged
M: ["D", "F"], // merge commit on main (two parents)
G: ["M"], // main moves on
H: ["C"], I: ["H"], // feature/payment, never merged
};
const tips = { main: "G", "feature/login": "F", "feature/payment": "I", "fix/typo": "D", "old-spike": "B" };
function ancestors(start) { // all commits reachable from start
const seen = new Set(), stack = [start];
while (stack.length) {
const c = stack.pop();
if (seen.has(c)) continue;
seen.add(c);
stack.push(...parents[c]);
}
return seen;
}
function report(target) {
const reach = ancestors(tips[target]);
const merged = [], notMerged = [];
for (const [name, tip] of Object.entries(tips))
(reach.has(tip) ? merged : notMerged).push(name);
console.log(`into ${target} (${reach.size} reachable: ${[...reach].sort().join("")})`);
console.log(" --merged: " + merged.join(", "));
console.log(" --no-merged: " + notMerged.join(", "));
}
report("main");
report("feature/payment");আউটপুট:
into main (8 reachable: ABCDEFGM)
--merged: main, feature/login, fix/typo, old-spike
--no-merged: feature/payment
into feature/payment (5 reachable: ABCHI)
--merged: feature/payment, old-spike
--no-merged: main, feature/login, fix/typo
main-এর commit G থেকে হাঁটা শুরু: G → M, তারপর M-এর দুই parent — D আর F. F-এর পথে E, তারপর B, A. তাই feature/login-এর F reachable — সে “merged”, যদিও তার commit-গুলো main-এর সরলরেখায় নেই। fix/typo-র D আর old-spike-এর B-ও পথে পড়ে। কিন্তু H আর I কোনো পথে নেই — feature/payment থেকে যায় “no-merged” তালিকায়। দ্বিতীয় প্রশ্নে লক্ষ্য বদলালে উত্তরও বদলায়: feature/payment থেকে হাঁটলে মাত্র ৫টি commit (I, H, C, B, A) পৌঁছায়, তাই শুধু old-spike তার ভেতরে merged. প্রতিটি branch নিজের মধ্যে তো merged বটেই। আর লক্ষ্যটি না দিলে Git ধরে নেয় HEAD — git branch --merged মানে --merged HEAD.
৮ · Branch-এর নাম — Git-এর নিয়ম আর দলের রীতি
Branch-এর নাম আসলে একটি ফাইলের ঠিকানা (refs/heads/…)। তাই নামে স্ল্যাশ দিলে ভেতরে একটি folder তৈরি হয়: feature/payment থাকে refs/heads/feature/ folder-এ payment নামে। দলগুলো এই সুবিধা কাজে লাগিয়ে নামগুলোকে দলে ভাগ করে — একে বলা যায় namespace. কোনো একক মানদণ্ড নেই; নিচেরগুলো বহুল প্রচলিত রীতি:
| উপসর্গ | কাজ | উদাহরণ |
|---|---|---|
feature/ | নতুন সুবিধা — feature branch | feature/mobile-payment |
fix/ বা bugfix/ | Bug সংশোধন | fix/cart-total |
hotfix/ | Production-এ জরুরি সংশোধন | hotfix/login-crash |
release/ | প্রকাশের প্রস্তুতি | release/1.4 |
experiment/, spike/ | ফেলে দেওয়ার মতো পরীক্ষা | experiment/grid |
| ticket নম্বরসহ | Issue tracker-এর সাথে যোগ | feature/BAZ-142-search |
রীতির বাইরে Git-এর নিজের কিছু কঠিন নিয়ম আছে, কারণ এই অক্ষরগুলো Git-এর অন্য জায়গায় বিশেষ অর্থ বহন করে: .. দুই commit-এর পরিসর বোঝায়, ~ আর ^ parent বোঝায়, : push-এ উৎস আর গন্তব্য আলাদা করে। কোনো নাম বৈধ কি না, git check-ref-format --branch দিয়ে যাচাই করা যায়:
$ git check-ref-format --branch feature/login
feature/login
$ git check-ref-format --branch "feature/new login"
fatal: 'feature/new login' is not a valid branch name
$ git check-ref-format --branch "fix..cart"
fatal: 'fix..cart' is not a valid branch name
$ git check-ref-format --branch "release/v1.0.lock"
fatal: 'release/v1.0.lock' is not a valid branch name
$ git branch "x~1"
fatal: 'x~1' is not a valid branch name
hint: See 'git help check-ref-format'
hint: Disable this message with "git config set advice.refSyntax false"
$ git branch feature
fatal: cannot lock ref 'refs/heads/feature': 'refs/heads/feature/mobile-payment' exists; cannot create 'refs/heads/feature'বৈধ নাম হলে কমান্ডটি নামটিই ফেরত ছাপে; অবৈধ হলে fatal. অবৈধ হওয়ার কারণ: space, পরপর দুটি ডট, .lock দিয়ে শেষ, আর ~. শেষ উদাহরণটি সবচেয়ে মজার: feature/mobile-payment থাকায় refs/heads/feature ইতিমধ্যে একটি folder — একই নামে ফাইল বানানো যায় না। তাই একটি repository-তে feature আর feature/… একসাথে থাকতে পারে না।
| নিষেধ | উদাহরণ (অবৈধ) |
|---|---|
Space, control অক্ষর, ~ ^ : ? * [ \ | new login, x~1, wip? |
পরপর দুটি ডট .., বা @{ | fix..cart |
| শুরুতে বা শেষে স্ল্যাশ, পরপর দুটি স্ল্যাশ | feature/, /fix |
কোনো অংশ ডট দিয়ে শুরু বা .lock দিয়ে শেষ; পুরো নাম ডট দিয়ে শেষ | feature/.x, v1.lock, fix. |
শুধু @; branch-এর নাম - দিয়ে শুরু | @, -dash |
ভালো নামের কয়েকটি ব্যবহারিক পরামর্শ: ছোট হাতের অক্ষর আর হাইফেন (fix/cart-total, Fix/Cart_Total নয়); নাম পড়েই যেন কাজ বোঝা যায়; ব্যক্তির নামের বদলে কাজের নাম। ছোট হাতের অক্ষরের পেছনে একটি বাস্তব কারণও আছে — পরের বাক্সে।
আরও গভীরে: Branch সবসময় আলাদা ফাইল হয়ে থাকে না। git gc অনেক ref একসাথে .git/packed-refs নামের একটি ফাইলে গুছিয়ে রাখতে পারে, আর Git ২.৪৫ (২০২৪) থেকে reftable নামের সম্পূর্ণ নতুন একটি ref-ভাণ্ডারও বেছে নেওয়া যায় — তাই script-এ cat .git/refs/… নয়, git rev-parse main ব্যবহার করুন। ফাইলভিত্তিক ref-এর একটি সমস্যা: Windows আর macOS-এর ফাইল সিস্টেম সাধারণত ছোট-বড় হাতের অক্ষর আলাদা করে না, তাই Fix/x আর fix/x দুটি branch সেখানে একে অপরের সাথে গোলমাল পাকাতে পারে, যা Linux-এ ঘটে না। পুরোনো Git-এ HEAD নিজেই ছিল একটি symbolic link (OS কোর্সের link-এর পর্ব); পরে বহনযোগ্যতার জন্য আজকের ref: … লেখা ফাইলটি আসে। এসবের বিস্তারিত পর্ব ৫২ (Git-এর ভেতরে) আর পর্ব ৫৩-এ (Ref, reflog আর উদ্ধার)।
৯ · Long-lived বনাম short-lived branch
সব branch এক রকম নয়। কিছু branch বছরের পর বছর টিকে থাকে — long-lived branch: main, কোনো দলে develop, বা পুরোনো সংস্করণের রক্ষণাবেক্ষণের জন্য release/1.x. বাকিগুলো জন্মায় একটি কাজের জন্য, কয়েক ঘণ্টা বা দিন পরে মিশে গিয়ে মুছে যায় — short-lived branch.
রেলের মূল লাইন আর পাশের লুপ লাইনের কথা ভাবুন। মূল লাইন (main) সবসময় চালু থাকতে হবে। কোনো ট্রেন মেরামত বা অপেক্ষার জন্য লুপ লাইনে সরে, কাজ শেষে আবার মূল লাইনে ফেরে। লুপ লাইনে একটি ট্রেন যত বেশি সময় থাকে, মূল লাইনের সময়সূচি ততই বদলে যায় — ফিরে আসার সময় মেলানো কঠিন হয়। Branch-ও তাই: যত দিন আলাদা থাকে, main তত এগিয়ে যায়, আর মেলানোর সময় conflict তত বাড়ে।
এজন্যই আধুনিক অনেক দল feature branch ছোট রাখে, আর কাজ অসম্পূর্ণ হলেও ঘন ঘন main-এ মেলায় — অসম্পূর্ণ সুবিধা লুকিয়ে রাখে feature flag দিয়ে (System Design কোর্সের deployment ও release পর্ব)। এই চর্চার নাম trunk-based development, আর এটি continuous integration-এর মূল ভিত্তি। অন্যদিকে Git Flow-এর মতো পদ্ধতি একাধিক long-lived branch রাখে। কোন দলের জন্য কোনটি ঠিক — পর্ব ১৭-এর (Branching strategy) পুরো বিষয়। এই পর্বের জন্য মনে রাখার কথা: Git-এর কাছে long-lived আর short-lived-এর কোনো পার্থক্য নেই — দুটোই একই রকম ৪১ বাইটের pointer. পার্থক্যটা দলের নিয়মে।
পরীক্ষা ও ইন্টারভিউতে:
- Git branch আসলে কী? — একটি চলমান pointer:
refs/heads/-এর নিচে একটি ref, যাতে একটি commit-এর hash লেখা; নতুন commit-এ এটি সামনে সরে। - HEAD কী? — বর্তমান অবস্থানের symbolic ref; সাধারণত একটি branch-এর নাম রাখে, detached অবস্থায় সরাসরি একটি commit-এর hash.
- Detached HEAD-এ করা commit কীভাবে বাঁচাবেন? — যাওয়ার আগে
git switch -c নতুন-নাম; চলে গেলেgit reflogথেকে hash নিয়েgit branch নাম hash. -dআর-D-এর পার্থক্য? —-dশুধু fully merged branch মোছে (upstream থাকলে তাতে, নইলে HEAD-এ);-Dযাচাই ছাড়া মোছে।git switch -cআরgit checkout -b? — একই কাজ; switch এসেছে Git ২.২৩-এ, checkout-এর বহু কাজ ভাগ করে দিতে।
১০ · AI-এর যুগে — branch আর HEAD নিয়ে AI-এর পরামর্শ যাচাই
“Detached HEAD থেকে কীভাবে বের হব?” — এমন প্রশ্নে AI সহকারী সাধারণত দ্রুত, আত্মবিশ্বাসী উত্তর দেয়। সমস্যা হলো, আত্মবিশ্বাস আর সঠিকতা এক জিনিস নয়, আর Git-এ কিছু ভুল উত্তর কাজ মুছে দেয়। কয়েকটি পরিচিত ধরন:
| AI যা বলতে পারে | ঝুঁকি | নিরাপদ বিকল্প |
|---|---|---|
“Detached HEAD? শুধু git checkout main চালান।” | সেখানে করা commit-গুলোর কথা বলে না — সেগুলো অনাথ হয় | আগে git status আর git log --oneline -3; commit থাকলে আগে git switch -c নাম |
“Switch আটকাচ্ছে? git checkout -f বা git reset --hard করুন।” | Commit না করা কাজ চিরতরে মুছে যায়; reflog-ও ফেরাতে পারে না | git stash বা একটি WIP commit; মুছতে হলে নিজে git diff দেখে সিদ্ধান্ত |
“git branch -D দিয়ে মুছে ফেলুন।” | Merge না হওয়া কাজ হারায় | আগে -d; প্রত্যাখ্যান হলে git log main..branch দেখে তবে -D |
অস্তিত্বহীন flag, যেমন “git switch --recover” | Error, নয়তো ভুল ধারণা | git help switch বা git switch -h দিয়ে মিলিয়ে নেওয়া |
| পুরোনো সংস্করণের বা ভিন্ন সংস্করণের আচরণকে আজকের বলে চালানো | আপনার Git-এ বার্তা বা default আলাদা | git --version জেনে সেই সংস্করণের documentation পড়া |
AI-কে প্রশ্ন করার সময় পুরো অবস্থা দিন, আর ধ্বংসাত্মক কমান্ড নিষেধ করে দিন:
AI-এর Git পরামর্শ চালানোর আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১CVS আর SVN-এর যুগে দলগুলো branch এড়িয়ে চলত, অথচ Git-এ দিনে দশটি branch বানানোও স্বাভাবিক। এই পরিবর্তন কি শুধু প্রযুক্তির, না কাজের সংস্কৃতিরও?
দুটোরই — আর প্রথমটি দ্বিতীয়টিকে সম্ভব করেছে। CVS-এ branch লাগানো হতো প্রতিটি ফাইলে আলাদা করে; হাজার ফাইলের project-এ সেটি ধীর, আর ভুল হলে গোলমেলে। Subversion branch বানাত folder-এর “সস্তা কপি” দিয়ে — বানানো দ্রুত হলেও কোন পরিবর্তন আগে মেলানো হয়েছে, সেই হিসাব (merge tracking) যোগ হয় ২০০৮ সালের দিকে সংস্করণ ১.৫-এ। তার আগে একই পরিবর্তন দুবার মেলানো বা বাদ পড়া ছিল সাধারণ ঘটনা। ফলে অনেক দলের অভিজ্ঞতা দাঁড়াল: branch বানানো সহজ, ফেরত আনা যন্ত্রণার। মানুষ সেই যন্ত্রণা এড়াতে branch-ই এড়াত, আর বড় পরিবর্তন সবাই এক লাইনে ঢোকাত।
Git-এ দুটি নকশাগত সিদ্ধান্ত এই হিসাব বদলে দিল। প্রথমত, branch একটি ৪১ বাইটের pointer — বানানো, বদলানো, মোছা সবই মুহূর্তে, আর সব কাজ স্থানীয়, server ছাড়াই। দ্বিতীয়ত, প্রতিটি commit তার parent মনে রাখে, তাই দুই branch-এর সাধারণ পূর্বসূরি (merge base) graph থেকেই বের করা যায় — merge-এর হিসাব আলাদা করে রাখতে হয় না (পর্ব ১৩ আর ৫৪)। Linus Torvalds Linux kernel-এর জন্য Git লিখেছিলেন, যেখানে হাজারো মানুষ সমান্তরালে কাজ করেন আর পরিবর্তন অনেক হাত ঘুরে আসে; সেই কাজের ধারার জন্য সস্তা branch আর নির্ভরযোগ্য merge ছিল আবশ্যিক।
প্রযুক্তি বদলাতেই সংস্কৃতিও বদলাল। প্রতিটি কাজের জন্য আলাদা branch, তার ওপর pull request আর code review (পর্ব ১৬) — এই চর্চা আজ স্বাভাবিক, কারণ branch আর বোঝা নয়। কিন্তু নতুন ঝুঁকিও এলো: সস্তা বলে মানুষ branch অনেক দিন ফেলে রাখেন, আর পরে মেলাতে গিয়ে বিপদে পড়েন। তাই আজকের আলোচনা “branch বানাব কি না” নয়, বরং “কত ছোট আর কত অল্প দিনের branch” (পর্ব ১৭)।
মূল উপলব্ধি: কোনো কাজের খরচ কমলে মানুষ সেটি বেশি করে, আর নতুন অভ্যাস গড়ে ওঠে। টুলের নকশা দলের কাজের সংস্কৃতিকে নিঃশব্দে আকার দেয় — তাই টুল বেছে নেওয়াও একটি প্রকৌশল-সিদ্ধান্ত।
প্র ০২Git ২.২৩-এ git switch আর git restore এলো, অথচ git checkout রয়ে গেল। একটি টুলে একই কাজের দুই পথ রাখা কি ভালো নকশা?
git checkout ঐতিহাসিকভাবে অন্তত তিনটি আলাদা কাজ করত: branch বদলানো, নতুন branch বানানো (-b), আর ফাইলকে index বা কোনো commit-এর অবস্থায় ফেরানো (git checkout -- file). প্রথম দুটি নিরাপদ — Git অসংরক্ষিত কাজ মুছে দেওয়ার আগে থামায়। কিন্তু তৃতীয়টি ইচ্ছাকৃতভাবেই অসংরক্ষিত পরিবর্তন মুছে দেয়, কোনো প্রশ্ন ছাড়া। একই কমান্ডের নামে নিরাপদ আর ধ্বংসাত্মক কাজ থাকায়, একটি branch আর একটি ফাইলের নাম এক হলে কমান্ডটি দ্ব্যর্থক হয়ে যেত; তাই -- চিহ্নের মতো সূক্ষ্ম জিনিস মনে রাখতে হতো। নতুনদের জন্য এটি ছিল একটি পরিচিত ফাঁদ।
২০১৯ সালে Git ২.২৩ সমাধান দিল কাজ ভাগ করে: switch শুধু branch-এর জন্য, restore শুধু ফাইলের জন্য। switch আরও কড়া — commit-এর hash দিলে --detach চায়, যাতে কেউ ভুল করে detached না হন। কিন্তু checkout সরানো হয়নি, কারণ লাখো script, CI ব্যবস্থা, বই আর অভ্যাস এর ওপর দাঁড়িয়ে। একটি বহুল ব্যবহৃত টুলে পুরোনো কমান্ড ভেঙে দেওয়ার খরচ বিশাল; তাই নতুন পথ যোগ করে পুরোনোটিকে রাখা — এটাই backward compatibility-র সাধারণ কৌশল। দুটি নতুন কমান্ড শুরুতে “experimental” চিহ্ন নিয়ে আসে, যাতে আচরণ বদলানোর সুযোগ থাকে; Git ২.৫৩-এর documentation-এ সেই চিহ্ন আর নেই।
দাম আছে: দুই পথ মানে দ্বিগুণ শেখা, আর AI বা পুরোনো টিউটোরিয়াল যখন checkout দেখায়, নতুনরা বিভ্রান্ত হন। এই কোর্সে নতুন কাজে switch/restore ব্যবহার করা হয়, কিন্তু checkout পড়তে পারাও জরুরি — বাস্তবের repository আর script-এ এটি সবখানে।
মূল উপলব্ধি: ভালো interface-এ নিরাপদ আর বিপজ্জনক কাজ আলাদা নামে থাকে। আর ব্যবহারকারী অনেক হলে পরিবর্তন আসে যোগ করে, ভেঙে নয় — নতুন নকশার সাথে পুরোনো প্রতিশ্রুতিও রাখতে হয়।
প্র ০৩Detached HEAD-কে অনেকে “ভাঙা অবস্থা” মনে করেন। আসলে কেন Git এই অবস্থা রাখে, আর এর নিরাপত্তা-জাল কতটা নির্ভরযোগ্য?
Detached HEAD Git-এর নকশার একটি স্বাভাবিক ফল, কোনো ত্রুটি নয়। HEAD-এর কাজ হলো বলা “কোন commit-এর ফাইল এখন সাজানো আছে”। সাধারণত সেটি একটি branch-এর মাধ্যমে বলা হয়, কিন্তু অনেক কাজে কোনো branch-এর মাথায় নয়, ইতিহাসের মাঝখানের কোনো commit-এ দাঁড়াতে হয়: একটি পুরোনো release-এর tag থেকে build বানানো, git bisect দিয়ে binary search করে কোন commit-এ bug ঢুকেছে খোঁজা (পর্ব ৫৫), rebase চলার মাঝখানে প্রতিটি commit নতুন করে বসানো (পর্ব ১৫), কিংবা CI ব্যবস্থায় ঠিক যে commit-টি পরীক্ষা করতে হবে সেটিই checkout করা। এসব জায়গায় একটি branch বানাতে বাধ্য করলে অপ্রয়োজনীয় নামের ভিড় জমত।
ঝুঁকিটা একটাই: detached অবস্থায় commit করে branch না বানিয়ে চলে যাওয়া। এর বিরুদ্ধে Git-এর কয়েক স্তরের জাল আছে। প্রথম স্তর — ঢোকার সময় লম্বা পরামর্শ-বার্তা। দ্বিতীয় — বেরোনোর সময় “you are leaving 1 commit behind” আর hash-সহ git branch-এর হুবহু কমান্ড। তৃতীয় — reflog, যেখানে HEAD-এর প্রতিটি নড়াচড়া লেখা থাকে। চতুর্থ — garbage collection-এর দেরি: default হিসেবে কোথাও থেকে পৌঁছানো যায় না এমন ঘর reflog-এ থাকে ৩০ দিন, আর git gc সাধারণত দুই সপ্তাহের কম পুরোনো loose object মোছে না।
কিন্তু জালের ফাঁকও জানা দরকার। Reflog শুধু স্থানীয় — ল্যাপটপ নষ্ট হলে বা নতুন clone নিলে সেটি নেই। কেউ নিজে git gc --prune=now বা reflog expire চালালে সময়সীমাও থাকে না। আর সবচেয়ে বড় কথা, commit-ই হয়নি এমন কাজ (working tree-র পরিবর্তন) reflog কখনো দেখে না। তাই নিয়ম সরল: detached অবস্থায় কাজ রাখার ইচ্ছা হলেই সাথে সাথে git switch -c.
মূল উপলব্ধি: ভালো নকশা বিপজ্জনক অবস্থা নিষিদ্ধ না করে তাকে দৃশ্যমান করে, আর ফেরার পথ রাখে। কিন্তু ফেরার পথের সীমা জানা ব্যবহারকারীর দায়িত্ব — নিরাপত্তা-জাল অভ্যাসের বিকল্প নয়।
প্র ০৪“master” থেকে “main” — Git ২.৫৩ নতুন repository-তে এখনো “master” নাম দেয় আর সতর্ক করে যে Git ৩.০-এ এটি বদলাবে। একটি branch-এর নাম বদলানো এত বড় ব্যাপার কেন?
Git-এর কাছে প্রথম branch-এর নামের কোনো বিশেষ মর্যাদা নেই — সেটি শুধু HEAD-এর ফাইলে লেখা একটি নাম, অন্য যেকোনো branch-এর মতোই ৪১ বাইটের ref. ২০২০ সালে সফটওয়্যার জগতে “master” শব্দটি বদলানোর আলোচনা জোরালো হয়; Git ২.২৮-এ (২০২০) init.defaultBranch সেটিং আসে, যাতে প্রত্যেকে নিজের পছন্দের প্রথম নাম ঠিক করতে পারেন, আর GitHub-এর মতো বড় hosting platform লেখার সময়ে নতুন repository-র default “main” করে দেয়। এই কোর্সের scratch repository-তে Git ২.৫৩ সেটিং ছাড়া git init চালালে লম্বা একটি hint দেখায়: “Using 'master' as the name for the initial branch. This default branch name will change to "main" in Git 3.0.” অর্থাৎ Git নিজেও পরিবর্তনের পরিকল্পনা ঘোষণা করেছে, তবে ধীরে।
ধীরে কেন? কারণ নামটি Git-এর ভেতরে ছোট হলেও বাইরে বহু জায়গায় ছড়িয়ে থাকে: CI pipeline-এর শর্ত (“শুধু master-এ deploy”), platform-এর protected branch-এর নিয়ম, script-এ লেখা git push origin master, documentation-এর লিংক, প্রত্যেক সদস্যের স্থানীয় clone-এর upstream সেটিং। একটি বিদ্যমান repository-তে নাম বদলাতে git branch -m master main এক সেকেন্ডের কাজ; কিন্তু remote-এ নতুন নাম push, default branch বদল, সবার clone হালনাগাদ, CI-র শর্ত বদল — এগুলো সমন্বয়ের কাজ।
এখান থেকে branch সম্পর্কে একটি বড় শিক্ষা: Git-এর কাছে কোনো branch “প্রধান” নয়; main-কে প্রধান বানায় দলের নিয়ম, CI-র সেটিং আর platform-এর কনফিগারেশন। HEAD একটি symbolic ref বলেই নাম বদলানো যান্ত্রিকভাবে এত সহজ। এই কোর্সের transcript-এ আমরা -c init.defaultBranch=main দিয়ে শুরু করেছি, যাতে সংস্করণভেদে ফল না বদলায়।
মূল উপলব্ধি: প্রযুক্তিগতভাবে তুচ্ছ পরিবর্তনও বহু মানুষ ও ব্যবস্থার চুক্তি ভাঙতে পারে। নাম হলো interface — বদলানোর আগে কে কে সেই নামের ওপর নির্ভর করে, তার তালিকা করুন।
প্র ০৫Branch যত সস্তা, ততই কি ভালো? Long-lived feature branch নিয়ে গবেষণা আর অভিজ্ঞতা কী বলে?
Branch বানানো সস্তা, কিন্তু branch আলাদা রাখা সস্তা নয়। প্রতিটি দিন একটি feature branch main থেকে দূরে থাকলে main-এ অন্যদের পরিবর্তন জমে। শেষে মেলাতে গেলে একসাথে অনেক conflict, অনেক অনুমান যে আর সত্য নয়, আর এমন bug যা কোনো একটি branch-এ ছিল না — দুটো মিলেই জন্মেছে। Extreme Programming-এর যুগ থেকে (Kent Beck, ১৯৯০-এর দশকের শেষ ভাগ) “continuous integration”-এর মূল কথাই ছিল ঘন ঘন মেলানো; Martin Fowler-এর বহুল পঠিত প্রবন্ধ (প্রথম সংস্করণ ২০০০ সালে, পরে কয়েকবার হালনাগাদ) দিনে অন্তত একবার মূল লাইনে কাজ মেলানোর পরামর্শ দেয়।
গবেষণার দিক থেকে, DORA দলের বছরের পর বছরের জরিপ আর তার ওপর লেখা বই Accelerate (Forsgren, Humble ও Kim, ২০১৮) জানায় যে যেসব দল trunk-based development মানে — খুব অল্পসংখ্যক সক্রিয় branch, যা এক দিনের কম সময়ে মূল লাইনে মেশে — তাদের software delivery-র মাপকাঠিগুলো গড়ে ভালো। এটি জরিপভিত্তিক সম্পর্ক, প্রতিটি দলের জন্য নিশ্চিত কার্যকারণ নয়; আর ছোট branch-এর সাথে ভালো স্বয়ংক্রিয় test, feature flag আর দ্রুত review-ও লাগে — নইলে ঘন ঘন মেলানো মানে ঘন ঘন ভাঙা।
অন্যদিকে long-lived branch-এর বৈধ জায়গাও আছে: একাধিক পুরোনো সংস্করণ একসাথে রক্ষণাবেক্ষণ (যেমন একটি library-র 1.x আর 2.x), কঠোর নিয়ন্ত্রিত release, বা open-source-এ বাইরের অবদানকারীর fork. Git Flow (Vincent Driessen, ২০১০) এমন পরিস্থিতির জন্য কয়েকটি long-lived branch-এর কাঠামো দিয়েছিল; লেখক নিজে পরে লিখেছেন যে ঘন ঘন deploy হওয়া web app-এর জন্য সরলতর পদ্ধতি ভালো হতে পারে। এই তুলনা পর্ব ১৭-এ বিস্তারিত।
মূল উপলব্ধি: Git branch-এর দাম চোকায় না — দাম আসে মেলানোর দেরি থেকে। তাই প্রশ্ন “branch কত সহজে বানাই” নয়, “কত তাড়াতাড়ি ফেরত আনি”।
অনুশীলন
প্রথমে কাগজে উত্তর লিখুন, তারপর নিজের কম্পিউটারে একটি খালি folder-এ চালিয়ে মিলিয়ে নিন।
-
Graph অনুমান করুন: একটি নতুন repository-তে main-এ commit A করা হলো। তারপর পরপর:
git switch -c dev, commit B,git switch main, commit C,git switch dev, commit D. (ক)git log --oneline --graph --allকেমন দেখাবে, কোন লেবেল কোথায়? (খ)git branch --no-merged mainকী দেখাবে? (গ) এরপরgit switch --detach HEAD~1চালালে HEAD কোথায়?Git ২.৫৩-এ (তারিখ ১৬ জানুয়ারি ২০২৬ সকাল ৯টা থেকে, প্রতি commit-এ এক মিনিট; বার্তা A, B, C, D) আসল আউটপুট:
$ git log --oneline --graph --all * 5c4a8c9 (HEAD -> dev) D * c96fa34 B | * f7b8f57 (main) C |/ * b81be97 A $ git branch --no-merged main * dev $ git switch --detach HEAD~1 HEAD is now at c96fa34 B $ git log --oneline --graph --all * 5c4a8c9 (dev) D * c96fa34 (HEAD) B | * f7b8f57 (main) C |/ * b81be97 A(ক) A থেকে দুটি শাখা: dev-এ B → D, main-এ C. আপনি শেষে dev-এ ছিলেন, তাই
HEAD -> dev. খেয়াল করুন, D-এর parent B — কারণ commit D-এর সময় HEAD dev-এ ছিল, আর dev তখন B-তে। (খ) dev-এর D main-এর C থেকে পৌঁছানো যায় না, তাই dev “no-merged”; তারকা মানে dev বর্তমান branch. (গ)HEAD~1মানে D-এর parent, অর্থাৎ B. এবার লেবেলে শুধু(HEAD)— কোনো তীর নেই, কারণ HEAD কোনো branch-এর নাম রাখছে না। dev এখনো D-তে। -
কমান্ড লিখুন: আপনি
feature/search-এ কাজ করছেন,search.js-এ commit না করা পরিবর্তন আছে, আর সেই ফাইল main-এ আলাদা। এখনই main থেকে একটি জরুরি সংশোধনের branchhotfix/price-zeroবানিয়ে সেখানে কাজ শুরু করতে হবে, পরে feature/search-এ ফিরে আগের কাজ চালিয়ে যেতে হবে। সংশোধন main-এ যাওয়ার পর hotfix branch-টি নিরাপদে মুছতে হবে। কমান্ডের ক্রম লিখুন।একটি নিরাপদ ক্রম (stash-এর বদলে WIP commit-ও চলে):
git status # কী কী বদলানো আছে দেখে নেওয়া git stash # search.js-এর কাজ সাময়িকভাবে সরানো git switch main git switch -c hotfix/price-zero # main-এর বর্তমান commit থেকে নতুন branch # ... সংশোধন, git add, git commit ... # ... hotfix main-এ merge (পর্ব ১৩) ... git switch feature/search git stash pop # আগের কাজ ফিরিয়ে আনা git branch -d hotfix/price-zero # merged, তাই নিরাপদে মোছাকেন এভাবে: search.js দুই branch-এ আলাদা, তাই stash ছাড়া
git switch mainআটকে যেত (“Your local changes … would be overwritten”).git switch -c hotfix/price-zero mainলিখলে main-এ না গিয়েও সরাসরি main থেকে branch বানানো যায়, কিন্তু তখনো working tree-র পরিবর্তনের জন্য stash লাগত। শেষে-dব্যবহার করুন,-Dনয় — merge সত্যিই হয়ে থাকলে -d সফল হবে, না হলে Git থামিয়ে আপনাকে ভুলটি দেখাবে। Stash-এর খুঁটিনাটি পর্ব ৫৬-এ। -
সমালোচনা করুন: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের দল branch-এর এই নিয়ম লিখেছে: “(১) প্রত্যেকে নিজের নামে একটি branch রাখবেন, যেমন
karim, আর সব কাজ সেখানেই করবেন। (২) কাজ শেষ হলে branch মুছবেন না, ইতিহাসের জন্য রেখে দেবেন। (৩) নামে বড় হাতের অক্ষর চলবে, যেমনFeature/Result_PDF. (৪) একজনের branch মাসে একবার main-এ মেলানো হবে।” প্রতিটি নিয়মের সমস্যা আর ভালো বিকল্প লিখুন।- (১) ব্যক্তির নামে branch: নাম দেখে কাজ বোঝা যায় না, আর একটি branch-এ অনেক অসম্পর্কিত কাজ জমে, ফলে আলাদা করে review বা বাতিল করা যায় না। বিকল্প: কাজের নামে branch —
feature/result-pdf,fix/gpa-rounding; প্রতিটি কাজে একটি। - (২) কখনো না মোছা: ইতিহাস branch-এ থাকে না, commit-এ থাকে — merge হওয়ার পর commit-গুলো main থেকেই reachable. পুরোনো branch-এর ভিড়ে সক্রিয় কাজ খুঁজে পাওয়া কঠিন হয়। বিকল্প: merge-এর পর
git branch -d; কোনো বিশেষ মুহূর্ত চিহ্নিত রাখতে tag (পর্ব ১৮)। - (৩) বড় হাতের অক্ষর: Windows ও macOS-এর ফাইল সিস্টেম সাধারণত case আলাদা করে না, তাই
Feature/…আরfeature/…একই folder-এ গোলমাল পাকাতে পারে, যা Linux ব্যবহারকারী টেরই পান না। বিকল্প: ছোট হাতের অক্ষর আর হাইফেন। - (৪) মাসে একবার মেলানো: এক মাসের পরিবর্তন একসাথে মেলাতে গিয়ে বিশাল conflict আর দুই কাজের সংঘর্ষে নতুন bug. বিকল্প: ছোট, অল্পদিনের branch, প্রতিটি কাজ শেষ হলেই review আর merge; অসম্পূর্ণ সুবিধা feature flag-এর পেছনে।
নিয়মগুলোর পেছনের মূল ভুল ধারণা একটাই: branch-কে মানুষের “ঘর” ভাবা। Branch আসলে একটি কাজের অস্থায়ী pointer.
- (১) ব্যক্তির নামে branch: নাম দেখে কাজ বোঝা যায় না, আর একটি branch-এ অনেক অসম্পর্কিত কাজ জমে, ফলে আলাদা করে review বা বাতিল করা যায় না। বিকল্প: কাজের নামে branch —
-
হাতে reachability: একটি graph-এ প্রতিটি commit-এর parent দেওয়া আছে: A (নেই), B ← A, C ← B, D ← C, E ← B, F ← E, M ← D ও F (merge commit), G ← M, H ← C, I ← H. Branch-গুলো: main = G, feature/login = F, feature/payment = I, fix/typo = D, old-spike = B. (ক) main থেকে কোন কোন commit পৌঁছানো যায়, কয়টি? (খ)
git branch --merged mainকোন branch-গুলো দেখাবে? (গ) feature/payment-কে লক্ষ্য ধরলে কোনগুলো merged?(ক) G থেকে হাঁটা: G → M → (D → C → B → A) আর (F → E → B …)। পৌঁছানো যায় A, B, C, D, E, F, G, M — মোট ৮টি; H আর I নয়। (খ) যাদের commit এই তালিকায়: main (G), feature/login (F), fix/typo (D), old-spike (B). feature/payment (I) নয়। (গ) I থেকে: I → H → C → B → A, মোট ৫টি। তার ভেতরে আছে feature/payment নিজে আর old-spike (B). main, feature/login, fix/typo নেই। ওপরের
merged_check.js-এর আউটপুট ঠিক এই উত্তরই দেয়:8 reachable: ABCDEFGMআর5 reachable: ABCHI. মনে রাখার বিষয়: merge commit M-এর দ্বিতীয় parent (F) না ধরলে feature/login-কে ভুল করে “no-merged” মনে হতো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি short-lived feature branch-এর জীবন সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
git switch main— শুরুর জায়গায় যাওয়াgit switch -c feature/search— নতুন branch বানিয়ে তাতে যাওয়া- কাজ করে
git addআরgit commit— শুধু feature/search সামনে সরে git log --oneline --graph --all— দুই branch-এর অবস্থা যাচাইgit switch mainআর merge — কাজ main-এ মেলানোgit branch -d feature/search— merged বলে নিরাপদে মোছা
Git-এ git branch feature/x চালালে ভেতরে আসলে কী তৈরি হয়?
- পুরো project-এর একটি কপি, আলাদা folder-এ
- একটি ref, যাতে বর্তমান commit-এর hash লেখা — কোনো ফাইল কপি হয় না
- একটি নতুন খালি commit
- নতুন branch, আর HEAD সাথে সাথে সেখানে চলে যায়
.git/HEAD-এ লেখা ref: refs/heads/feature. main আর feature দুটোই commit B-তে। এখন একটি নতুন commit C করলে কী হবে?
- main আর feature দুটোই C-তে সরবে
- HEAD-এ C-এর hash লেখা হবে, feature B-তে থাকবে
- feature C-তে সরবে, main B-তে থাকবে, HEAD তখনো feature-এর নাম রাখবে
- Git commit করতে দেবে না, কারণ দুই branch একই commit-এ
আপনি main-এ, cart.txt-এ commit না করা পরিবর্তন আছে, আর cart.txt main ও feature-এ আলাদা। git switch feature চালালে Git ২.৫৩ কী করবে?
- Switch বাতিল করে error দেবে; কিছুই বদলাবে না
- আপনার পরিবর্তন মুছে feature-এর cart.txt বসাবে
- পরিবর্তন নিজে থেকে stash করে switch করবে
- পরিবর্তন main-এ নিজে থেকে commit করে switch করবে
Detached HEAD অবস্থায় commit 2cbf7b9 করে git switch main চালিয়েছেন, কোনো branch বানাননি। এখন commit-টি ফেরত পাওয়ার সঠিক উপায় কী?
- git log --all-এ খুঁজে নেওয়া
- উপায় নেই, commit মুছে গেছে
- git reset --hard 2cbf7b9 চালানো
- git reflog থেকে hash নিয়ে git branch নতুন-নাম 2cbf7b9
HEAD = main, main = G. Graph: G-এর parent M; M একটি merge commit, যার parent D ও F; F-এর parent E; E-এর parent B. feature/login = F, feature/pay = I (I-এর parent H, H-এর parent C), C-এর parent B. git branch -d feature/login আর git branch -d feature/pay চালালে কী হবে?
- দুটোই মুছে যাবে
- feature/login মুছবে, feature/pay-তে “not fully merged” error
- feature/login-এ error, কারণ F main-এর সরলরেখায় নেই
- দুটোতেই error, কারণ -d কখনো branch মোছে না
নিচের কোন branch নামটি Git গ্রহণ করে?
fix..cartfeature/new loginfix/cart-totalrelease/v1.0.lock
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Branch হলো একটি commit-এর দিকে চলমান pointer — একটি ছোট্ট ref, তাই বানানো আর মোছা প্রায় বিনা খরচে।
- HEAD সাধারণত একটি branch-এর নাম রাখে; commit করলে শুধু সেই branch সামনে সরে।
git switch -c(বাcheckout -b) দিয়ে নতুন branch; অসংরক্ষিত পরিবর্তন কাজ হারানোর ঝুঁকি থাকলে Git switch আটকে দেয় — commit বা stash.- Detached HEAD-এ commit রাখতে
git switch -c; ভুলে চলে গেলে reflog থেকে hash নিয়ে branch. -dmerged branch মোছে,-Dজোর করে; নামেfeature/,fix/namespace, ছোট হাতের অক্ষর, আর branch রাখুন অল্পদিনের।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৩ · Merge আর conflict — fast-forward, three-way merge আর conflict সমাধানপরবর্তী পর্বআলাদা হয়ে যাওয়া branch আবার মেলানো।
- পর্ব ১১ · ভুল শোধরানো — restore, reset, revert, amend আর reflogআগের পর্বReflog দিয়ে আরও নানা ধরনের উদ্ধার।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DSA · Depth-first searchপাশের ট্র্যাকReachability আর graph-এ হাঁটার মূল কৌশল, যা
--merged-এর পেছনে। - Operating Systems · Directory আর linkগভীরে যানফাইল, folder আর symbolic link — ref ফাইলগুলো কোথায় আর কীভাবে থাকে।