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

ভুল শোধরানো — restore, reset, revert, amend আর reflog

Undoing Things in Git — restore, reset, revert, amend and the reflog safety net

Taught in Bangla, this lesson maps what you want to undo to where it lives, then covers git restore for the working tree and index, git commit --amend, the three git reset modes, git revert for shared history, git clean for untracked files and the reflog as a safety net. Runnable models compare reset with revert on shared history and recover a lost commit, ending with the golden rule for shared branches.

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

  • “কী ফেরাতে চান, কোথায় আছে” — এই দুই প্রশ্নে সঠিক undo command বেছে নেওয়া
  • restore, restore --staged, commit --amend আর reset-এর তিন mode তিনটি এলাকায় ঠিক কী বদলায়
  • কেন শেয়ার করা branch-এ revert নিরাপদ আর reset + force push বিপজ্জনক
  • git clean দিয়ে untracked ফাইল নিরাপদে সরানো আর reflog দিয়ে হারানো commit উদ্ধার
  • AI যখন reset --hard বা push --force পরামর্শ দেয়, তখন কীভাবে যাচাই করবেন

১ · কী ফেরাতে চান, কোথায় আছে — undo-এর মানচিত্র

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

পর্ব ৮-এ আপনি Git-এর তিনটি এলাকা দেখেছেন। এখানে শুধু মনে করিয়ে দিই। Working tree হলো আপনার ফোল্ডারের ফাইল, যেখানে আপনি লেখেন। Index (staging area) হলো পরের commit-এর খসড়া, যা git add দিয়ে ভরে। আর repository-তে থাকে সব commit; বর্তমান branch যে commit-এর দিকে নির্দেশ করে, সেটিই HEAD. Undo করার প্রতিটি command আসলে এই তিন জায়গার একটি বা একাধিক জায়গাকে অন্য একটি জায়গার মতো করে দেয়।

ভুলটি কোথায়উদাহরণCommandঝুঁকি
শুধু working tree-তেফাইল এলোমেলো করে ফেলেছেন, add করেননিgit restore <file>⚠ পরিবর্তন চিরতরে যায়
Index-এ (stage করা)ভুল ফাইল git add করেছেনgit restore --staged <file>নিরাপদ — কাজ থাকে
নতুন untracked ফাইলdebug.log, tmp/git clean -n, তারপর -f⚠ ফাইল মুছে যায়
শেষ commit-এ, push হয়নিবার্তায় বানান ভুল, একটি ফাইল বাদgit commit --amendনতুন commit ID
পুরোনো commit-এ, push হয়নিশেষ তিনটি commit বাতিল করবেনgit reset (--soft/--mixed/--hard)--hard হলে ⚠
Push হয়ে যাওয়া commitmain-এ একটি bug ঢুকেছেgit revert <commit>নিরাপদ — history অটুট
ভুল command-এ commit “হারিয়েছেন”ভুল reset --hardgit reflog → git reset --hard HEAD@{n}শুধু commit করা কাজ ফেরে

এই সারণিটিই নিচের flowchart-এ সিদ্ধান্তের ধাপ হিসেবে আঁকা। ওপর থেকে প্রশ্নগুলোর উত্তর দিতে দিতে নামুন; প্রথম “হ্যাঁ” যেখানে, সেখানেই আপনার command.

কোন undo command — সিদ্ধান্তের ধাপ

Choosing the right undo command: where is the change, and has it been shared?

Undo command বেছে নেওয়ার flowchart পরিবর্তন শুধু ফাইলে, git add করেননি? git restore <ফাইল> ⚠ পরিবর্তন চিরতরে যায় git add করেছেন, commit করেননি? git restore --staged index থেকে নামে, কাজ থাকে commit push হয়ে অন্যদের কাছে গেছে? git revert <commit> নতুন উল্টো commit শুধু শেষ commit-টি ঠিক করবেন? git commit --amend নতুন commit ID হয় পুরোনো, push-না-করা local commit git reset <commit> mode: soft / mixed / hard হ্যাঁহ্যাঁ হ্যাঁহ্যাঁ নানানানা নতুন untracked ফাইল? → git clean -n, তারপর -f কিছু হারিয়েছেন? → git reflog (অংশ ৭)
ছবি ১১.১ — প্রথম প্রশ্ন “কোথায় আছে”, তারপর “অন্যরা কি পেয়ে গেছে”। লাল বাক্স মানে ফেরানো যায় না এমন ক্ষতির ঝুঁকি।
মূল ধারণা

Git-এ “undo” একটি বোতাম নয়, একটি প্রশ্ন: কোন এলাকাকে, কোন উৎস থেকে, আবার লিখবেন? restore ফাইলকে index বা কোনো commit থেকে আবার লেখে। reset branch-এর pointer সরায়, আর mode অনুযায়ী index ও working tree-কে সেই commit-এর মতো করে। revert কিছুই মোছে না — উল্টো পরিবর্তনের একটি নতুন commit বানায়।

যে repository দিয়ে পুরো পর্বের উদাহরণ

এই পর্বের প্রতিটি “উদাহরণ” আউটপুট এই যন্ত্রে Git ২.৫৩ দিয়ে একটি আলাদা scratch ফোল্ডারে চালিয়ে হুবহু কপি করা। প্রজেক্টটি একটি কাল্পনিক অনলাইন বাজার অ্যাপের ছোট অংশ। লেখক, ইমেল আর সময় নির্দিষ্ট রাখা হয়েছে, তাই ফাইলের লেখা হুবহু এক হলে আপনার যন্ত্রেও একই commit ID আসবে:

$ git -c init.defaultBranch=main init -q bazar && cd bazar
$ git config user.name "Rahim"; git config user.email "rahim@example.com"
$ git config core.autocrlf false
$ export GIT_AUTHOR_DATE="2026-01-15T10:00:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:00:00+06:00"
$ printf '# Bazar app\n' > README.md
$ git add README.md; git commit -q -m "Add README"
$ export GIT_AUTHOR_DATE="2026-01-15T10:01:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:01:00+06:00"
$ printf 'function total(items) {\n  return items.reduce((s, it) => s + it.price * it.qty, 0);\n}\nmodule.exports = { total };\n' > cart.js
$ printf '# Bazar app\n- cart total\n' > README.md
$ git add cart.js README.md; git commit -q -m "Add cart total"
$ export GIT_AUTHOR_DATE="2026-01-15T10:02:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:02:00+06:00"
$ printf 'function discount(sum) {\n  return sum >= 1000 ? sum * 0.9 : sum;\n}\nmodule.exports = { discount };\n' > discount.js
$ git add discount.js; git commit -q -m "Add discount rule"
$ git log --oneline
f469880 Add discount rule
f8f0806 Add cart total
df9c5fe Add README

২ · Working tree আর index ফেরানো — git restore

ধরুন আপনি cart.js-এ কিছু পরীক্ষা করতে গিয়ে ফাইলটি এলোমেলো করে ফেলেছেন, আর বুঝছেন এই পরিবর্তন রাখার মতো নয়। git restore ফাইলটিকে index-এ যা আছে, সেই অবস্থায় ফিরিয়ে আনে। Index-এ সাধারণত শেষ commit-এর লেখাই থাকে, যদি না আপনি নতুন কিছু add করে থাকেন।

$ git status --short
 M cart.js
$ git restore cart.js
$ git status
On branch main
nothing to commit, working tree clean

এই command কোনো প্রশ্ন করে না, কোনো নিশ্চিতকরণও চায় না। git restore <file> চালানোর মুহূর্তে ফাইলের যে পরিবর্তন কখনো add বা commit হয়নি, তা Git-এর কোথাও ছিল না — তাই reflog বা অন্য কোনো উপায়ে ফেরে না। সন্দেহ থাকলে আগে git diff cart.js দেখুন (পর্ব ৯), অথবা পরিবর্তনটি একটি আলাদা ফাইলে কপি করে রাখুন। পরিবর্তন সাময়িকভাবে সরিয়ে রাখার আরেক উপায় git stash, যা পর্ব ৫৬-এ দেখবেন।

ভুল ফাইল stage করেছেন — restore --staged

এবার উল্টো ঘটনা। আপনি একটি debug লাইন যোগ করে ভুলে git add করে ফেলেছেন। Commit করার আগে এটিকে index থেকে নামাতে চান, কিন্তু ফাইলের লেখা হারাতে চান না:

$ git add cart.js
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   cart.js

$ git restore --staged cart.js
$ git status --short
 M cart.js

খেয়াল করুন, git status নিজেই পরামর্শ দিচ্ছে কোন command দিয়ে unstage করবেন। আর শেষ লাইনে M-এর আগে একটি ফাঁকা ঘর: বাঁয়ের অক্ষর index-এর অবস্থা, ডানের অক্ষর working tree-র অবস্থা। পরিবর্তন এখন শুধু working tree-তে আছে, index আবার HEAD-এর মতো। --staged index-কে HEAD থেকে আবার লেখে, working tree ছোঁয় না — তাই এটি নিরাপদ।

পুরোনো কোনো commit থেকে একটি ফাইল আনা — --source

কখনো কখনো একটি ফাইলের দুই commit আগের রূপ দরকার হয়, কিন্তু পুরো project পেছনে নিতে চান না। --source=<commit> বলে দেয় কোন commit থেকে লেখা আনতে হবে। ডিফল্টভাবে এটি শুধু working tree-তে বসে; index আর branch যেমন ছিল তেমনই থাকে:

$ git restore --source=HEAD~2 README.md
$ cat README.md
# Bazar app
$ git status --short
 M README.md
$ git restore README.md
$ cat README.md
# Bazar app
- cart total

HEAD~2 মানে HEAD থেকে দুই ধাপ পেছনের commit, এখানে df9c5fe. আনা লেখা পছন্দ হলে add করে commit করবেন; না হলে শেষের মতো আবার git restore দিয়ে বাতিল। দুই জায়গাতেই একসাথে বসাতে চাইলে git restore --source=HEAD~2 --staged --worktree README.md.

পুরোনো command — checkout -- আর reset HEAD

git restore আর git switch এসেছে Git ২.২৩-এ, ২০১৯ সালে। তার আগে একই কাজ হতো git checkout আর git reset দিয়ে। পুরোনো বই, Stack Overflow-এর উত্তর আর অনেক AI এখনো এগুলোই দেখায়। এগুলো আজও কাজ করে:

$ git add cart.js
$ git reset HEAD cart.js
Unstaged changes after reset:
M	cart.js
$ git checkout -- cart.js
$ git status --short
কাজGit ২.২৩ থেকেপুরোনো রূপ
Working tree-র পরিবর্তন বাতিলgit restore fgit checkout -- f
Unstagegit restore --staged fgit reset HEAD f
পুরোনো commit থেকে ফাইলgit restore --source=X fgit checkout X -- f (index-ও বদলায়)
Branch বদলানোgit switch bgit checkout b

কেন ভাঙা হলো? পুরোনো git checkout একই নামে দুটি একদম ভিন্ন কাজ করত — branch বদলানো (নিরাপদ) আর ফাইলের পরিবর্তন মুছে দেওয়া (বিপজ্জনক)। একটি অক্ষর এদিক-ওদিক হলেই একটির বদলে অন্যটি চলত। Branch বদলানো আর switch নিয়ে বিস্তারিত পর্ব ১২-এ।

৩ · শেষ commit ঠিক করা — git commit --amend

Commit করে ফেলার পরেই চোখে পড়ল দুটি ভুল: বার্তায় “delivery”-র বানান ভুল, আর নতুন fee.json ফাইলটি add করতে ভুলে গেছেন। git commit --amend শেষ commit-টিকে বাতিল করে তার জায়গায় একটি নতুন commit বানায় — আগের commit-এর বিষয়বস্তু, তার সাথে index-এ এখন যা আছে, আর নতুন বার্তা মিলিয়ে।

$ export GIT_AUTHOR_DATE="2026-01-15T10:03:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:03:00+06:00"
$ printf 'const { discount } = require("./discount");\nconst FEE = require("./fee.json").fee;\nfunction total(items) {\n  const sum = items.reduce((s, it) => s + it.price * it.qty, 0);\n  return discount(sum) + FEE;\n}\nmodule.exports = { total };\n' > cart.js
$ printf '{ "fee": 60 }\n' > fee.json
$ git add cart.js
$ git commit -m "Add delivry fee"
[main 97611f9] Add delivry fee
 1 file changed, 4 insertions(+), 1 deletion(-)
$ git status --short
?? fee.json
$ export GIT_AUTHOR_DATE="2026-01-15T10:04:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:04:00+06:00"
$ git add fee.json
$ git commit --amend -m "Add delivery fee"
[main d09aad4] Add delivery fee
 Date: Thu Jan 15 10:03:00 2026 +0600
 2 files changed, 5 insertions(+), 1 deletion(-)
 create mode 100644 fee.json
$ git log --oneline
d09aad4 Add delivery fee
f469880 Add discount rule
f8f0806 Add cart total
df9c5fe Add README

তিনটি জিনিস খেয়াল করুন। এক, commit ID বদলে গেছে: 97611f9 থেকে d09aad4. দুই, git log-এ 97611f9 আর নেই, যেন কখনো ছিলই না। তিন, Date: লাইনে ১০টা ৩ মিনিট — amend ডিফল্টভাবে লেখকের (author) মূল সময় রেখে দেয়, শুধু committer-এর সময় নতুন হয়। বার্তা না বদলে শুধু ফাইল যোগ করতে চাইলে git commit --amend --no-edit.

ID কেন বদলায়? একটি commit-এর ID তার পুরো বিষয়বস্তুর hash: snapshot, parent commit, লেখক, সময় আর বার্তা। এর যেকোনো একটি বদলালে hash বদলায় (ভেতরের খুঁটিনাটি পর্ব ৫২-এ)। তাই Git-এ কোনো commit “সম্পাদনা” করা যায় না — শুধু নতুন একটি বানিয়ে branch-কে সেদিকে সরানো যায়। নিচের ডায়াগ্রামে ধাপে ধাপে দেখুন:

Amend-এর আগে ও পরে commit graph f469880 97611f9 main d09aad4 97611f9 main সহকর্মীর clone-এ97611f9 থেকে যায়
অবস্থা
main
Index-এ fee.json
97611f9
  1. ভুল commit. 97611f9-এ বার্তার বানান ভুল, আর fee.json তখনো untracked (??). main এই commit-এর দিকে।
  2. git add fee.json. শুধু index বদলাল; commit আর main আগের মতোই। Amend-এর জন্য index-কে আগে প্রস্তুত করতে হয়।
  3. git commit --amend. Git একটি নতুন commit d09aad4 বানাল, যার parent সেই একই f469880 — অর্থাৎ পুরোনোটির “পরে” নয়, “বদলে”। main সরে গেল নতুনটিতে। 97611f9 মোছেনি; শুধু এখন কোনো branch তার দিকে নির্দেশ করে না, তাই git log-এ দেখা যায় না। Reflog-এ সে এখনো আছে।
  4. যদি আগেই push হয়ে থাকত? সহকর্মী 97611f9 pull করে তার ওপর কাজ শুরু করে থাকতে পারেন। আপনার main এখন অন্য একটি commit-এ, তাই সাধারণ push প্রত্যাখ্যাত হবে, আর জোর করে push করলে দুই history আলাদা হয়ে যাবে। সেজন্য নিয়ম: push-করা commit amend নয়; বার্তার ভুল সহ্য করুন, বা একটি নতুন commit দিন।

৪ · git reset — branch-এর pointer সরানো, তিনটি mode

git reset <commit> সবচেয়ে শক্তিশালী আর সবচেয়ে ভুল বোঝা undo command. এর মূল কাজ একটাই: বর্তমান branch-কে অন্য একটি commit-এর দিকে সরানো। তারপর mode ঠিক করে আরও কতদূর যাবে:

--soft

শুধু branch সরে। Index আর working tree আগের মতো থাকে — বাতিল commit-গুলোর সব পরিবর্তন “staged” অবস্থায় হাজির। কয়েকটি commit মিলিয়ে একটি বানাতে কাজে লাগে।

branch

--mixed (ডিফল্ট)

Branch সরে, আর index সেই commit-এর মতো হয়। Working tree ছোঁয় না — পরিবর্তনগুলো ফাইলে থাকে, কিন্তু unstaged. Mode না লিখলে এটিই চলে।

branch + index

--hard

Branch, index আর working tree — তিনটিই সেই commit-এর মতো। Tracked ফাইলের সব অসংরক্ষিত পরিবর্তন মুছে যায়। Untracked ফাইল ছোঁয় না।

⚠ branch + index + working tree

নিচের ডায়াগ্রামে একই শুরুর অবস্থা থেকে তিনটি mode পাশাপাশি চলছে। শুরুতে main আছে C3-এ (যেখানে cart.js-এর রূপ v3); আপনি ফাইলটি বদলে v4 বানিয়ে stage করেছেন, তারপর আবার বদলে v5 করেছেন। Command: git reset --<mode> HEAD~2.

reset-এ main pointer C3 থেকে C1-এ সরে C1 C2 C3 v1v2v3 main main ORIG_HEAD, reflog
প্রতিটি mode-এর পর: main · index · working tree
--soft
--mixed
--hard
  1. শুরু — তিন mode-এর জন্য একই অবস্থা। main C3-এ, index-এ v4 (stage করা), working tree-তে v5. git status এখন “staged” আর “unstaged” দুই রকম পরিবর্তনই দেখাবে।
  2. প্রথম কাজ, তিন mode-এই: branch সরানো। main C3 থেকে C1-এ গেল। --soft এখানেই থামে: index-এ v4 আর ফাইলে v5 আগের মতো। HEAD এখন v1, তাই v1 থেকে v4 পর্যন্ত সব পরিবর্তন staged দেখায়।
  3. দ্বিতীয় কাজ, --mixed আর --hard: index মেলানো। Index-কে C1-এর মতো (v1) করা হলো। --mixed এখানে থামে। ফাইলে v5 আছে, কিন্তু কিছুই staged নয়। লক্ষ করুন: আপনার stage করা v4 এখন index-এও নেই, ফাইলেও নেই।
  4. তৃতীয় কাজ, শুধু --hard: working tree মেলানো। ফাইলটিও v1 হয়ে গেল। v5 কখনো commit বা add হয়নি, তাই সেটি এখন আর কোথাও নেই। এজন্যই --hard চালানোর আগে git status দেখা অভ্যাস করুন।
  5. C3 কোথায় গেল? কোথাও যায়নি। তিন mode-এর কোনোটিই commit মোছে না; শুধু main আর C3-এর দিকে নির্দেশ করে না। Git সঙ্গে সঙ্গে ORIG_HEAD-এ পুরোনো অবস্থান লিখে রাখে, আর reflog-এও লেখা থাকে। তাই git reset --hard ORIG_HEAD দিলে main আবার C3-এ ফেরে — কিন্তু v4 আর v5 ফেরে না।

এবার আসল Git-এ d09aad4 থেকে এক commit পেছনে, প্রতিবার status দেখে। প্রতিটি পরীক্ষার পর git reset --hard ORIG_HEAD দিয়ে আবার d09aad4-এ ফিরেছি। ORIG_HEAD হলো reset, merge বা rebase-এর আগে HEAD যেখানে ছিল, সেই জায়গার একটি বিশেষ নাম।

$ git reset --soft HEAD~1
$ git log --oneline -1
f469880 Add discount rule
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   cart.js
	new file:   fee.json

$ git reset --hard ORIG_HEAD
HEAD is now at d09aad4 Add delivery fee

Commit বাতিল, কিন্তু তার দুটি পরিবর্তন staged হয়ে অপেক্ষা করছে। এখন git commit দিলে একটি নতুন commit হবে — আবার লেখা বার্তাসহ।

$ git reset HEAD~1
Unstaged changes after reset:
M	cart.js
$ git status
On branch main
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:   cart.js

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

no changes added to commit (use "git add" and/or "git commit -a")
$ git reset --hard ORIG_HEAD
HEAD is now at d09aad4 Add delivery fee

fee.json এবার “untracked” — কারণ index-কে f469880-এর মতো করা হয়েছে, যেখানে এই ফাইল ছিলই না। ফাইলটি কিন্তু ফোল্ডারে আছে। কোন পরিবর্তন কোন commit-এ যাবে, নতুন করে ভাগ করতে চাইলে এই mode.

$ git reset --hard HEAD~1
HEAD is now at f469880 Add discount rule
$ git status
On branch main
nothing to commit, working tree clean
$ git log --oneline
f469880 Add discount rule
f8f0806 Add cart total
df9c5fe Add README
$ ls
README.md
cart.js
discount.js

সব পরিষ্কার — আর ঠিক এটাই বিপদ। fee.json ফোল্ডার থেকেও উধাও, কারণ সেটি d09aad4-এ tracked ছিল। d09aad4 এখন log-এ নেই। অংশ ৭-এ এই অবস্থা থেকেই তাকে ফিরিয়ে আনব।

দুই রূপের git reset: commit দিলে (git reset HEAD~1) branch সরে। ফাইলের নাম দিলে (git reset cart.js বা git reset HEAD cart.js) branch সরে না — শুধু সেই ফাইলের index-এর লেখা HEAD থেকে আবার বসে, অর্থাৎ unstage. এই দুই রূপ গুলিয়ে ফেলাই নতুনদের সবচেয়ে বড় বিভ্রান্তি; restore --staged আসার একটি কারণও এটি। আরও দুটি কম-প্রচলিত mode আছে — --keep আর --merge — যেগুলো অসংরক্ষিত পরিবর্তন মুছে ফেলার বদলে থেমে যায়; বিস্তারিত git help reset-এ।

তিন mode কোন এলাকা ছোঁয়, তা একটি ছোট মডেলে চালিয়ে দেখুন। প্রতিটি command একই শুরুর অবস্থার একটি নতুন কপিতে চলে, যাতে পাশাপাশি তুলনা করা যায়। শেষ দুই লাইন restore-এর দুই রূপ:

// Three areas + a branch pointer. One file, cart.js; "v1".."v5" are its versions.
const commits = { C1: { parent: null, cart: "v1" }, C2: { parent: "C1", cart: "v2" },
                  C3: { parent: "C2", cart: "v3" } };
function start() { // main at C3, v4 staged (index), then edited again to v5 (working tree)
  return { main: "C3", index: "v4", work: "v5" };
}
function back(id, n) { while (n-- > 0) id = commits[id].parent; return id; }
const ops = {
  "git reset --soft HEAD~2":    s => { s.main = back(s.main, 2); },           // branch only
  "git reset HEAD~2 (--mixed)": s => { s.main = back(s.main, 2);              // branch + index
                                       s.index = commits[s.main].cart; },
  "git reset --hard HEAD~2":    s => { s.main = back(s.main, 2);              // all three
                                       s.index = s.work = commits[s.main].cart; },
  "git restore --staged cart.js": s => { s.index = commits[s.main].cart; },   // index <- HEAD
  "git restore cart.js":        s => { s.work = s.index; },                   // work <- index
};
function row(label, s) {
  const head = commits[s.main].cart;
  const staged = s.index !== head ? "yes" : "no", unstaged = s.work !== s.index ? "yes" : "no";
  console.log(label.padEnd(31) + s.main.padEnd(6) + head.padEnd(6) + s.index.padEnd(7) +
              s.work.padEnd(6) + staged.padEnd(8) + unstaged);
}
console.log("command (each on a fresh copy)".padEnd(31) + "main  HEAD  index  work  staged  unstaged");
row("(start)", start());
for (const [cmd, f] of Object.entries(ops)) { const s = start(); f(s); row(cmd, s); }
// Commits are never deleted by reset: C3 still exists, only "main" stopped pointing at it.
console.log("C3 still in object store:", "C3" in commits);

আউটপুট:

command (each on a fresh copy) main  HEAD  index  work  staged  unstaged
(start)                        C3    v3    v4     v5    yes     yes
git reset --soft HEAD~2        C1    v1    v4     v5    yes     yes
git reset HEAD~2 (--mixed)     C1    v1    v1     v5    no      yes
git reset --hard HEAD~2        C1    v1    v1     v1    no      no
git restore --staged cart.js   C3    v3    v3     v5    no      yes
git restore cart.js            C3    v3    v4     v4    yes     no
C3 still in object store: true

“staged” মানে index আর HEAD আলাদা; “unstaged” মানে working tree আর index আলাদা — ঠিক যা git status দেখায়। --soft: শুধু main C1-এ গেল, index v4 আর ফাইল v5 অক্ষত, দুই রকম পরিবর্তনই আছে। --mixed: index v1 হলো, তাই staged “no”, কিন্তু ফাইলে v5 থাকায় unstaged “yes”. --hard: তিন ঘরেই v1, কোনো পরিবর্তন নেই — v4 আর v5 হারিয়েছে। restore --staged: main নড়েনি (C3), index HEAD-এর মতো v3 হলো। restore: ফাইল index-এর মতো v4 হলো, v5 মুছে গেল, আর v4 staged থেকে গেল। শেষ লাইন মনে করিয়ে দেয়: reset commit মোছে না।

৫ · git revert — শেয়ার করা history-তে নিরাপদ undo

ধরুন f469880 Add discount rule commit-টি ইতিমধ্যে দলের main-এ push হয়ে গেছে, আর এখন দেখা গেল ছাড়ের নিয়মটি ভুল। দলের অন্তত দুজন সেটি pull করেছেন। এখানে reset ভুল পথ, কারণ সেটি history-র একটি অংশ মুছে দিতে চায় — অথচ সেই অংশ অন্যদের clone-এ আছে। git revert <commit> উল্টো পথে হাঁটে: ভুল commit-এর ঠিক উল্টো পরিবর্তন দিয়ে একটি নতুন commit বানায়। ব্যাংকের হিসাবের খাতায় ভুল এন্ট্রি কেটে ফেলা হয় না; তার নিচে একটি উল্টো এন্ট্রি লেখা হয় — revert ঠিক সেই হিসাবরক্ষকের নিয়ম।

$ export GIT_AUTHOR_DATE="2026-01-15T10:05:00+06:00" GIT_COMMITTER_DATE="2026-01-15T10:05:00+06:00"
$ git revert --no-edit HEAD~1
[main 5fcff4a] Revert "Add discount rule"
 Date: Thu Jan 15 10:05:00 2026 +0600
 1 file changed, 4 deletions(-)
 delete mode 100644 discount.js
$ git log --oneline
5fcff4a Revert "Add discount rule"
d09aad4 Add delivery fee
f469880 Add discount rule
f8f0806 Add cart total
df9c5fe Add README
$ git show --stat --format="%h %s%n%n%b" HEAD
5fcff4a Revert "Add discount rule"

This reverts commit f469880657c8f5c5040e8ba79bec5a9bd6d72a98.


 discount.js | 4 ----
 1 file changed, 4 deletions(-)

History-তে কিছুই মোছেনি: f469880 যেমন ছিল, তেমনই আছে, আর ওপরে নতুন 5fcff4a. বার্তায় Git নিজেই লিখে দিয়েছে কোন commit উল্টানো হলো — পরে কেউ খুঁজলে পুরো গল্প পাবেন। --no-edit না দিলে Git বার্তা লেখার জন্য editor খোলে; ভালো চর্চা হলো সেখানে কেন উল্টানো হলো তা এক লাইনে লেখা।

খেয়াল করুন, আমরা HEAD~1 উল্টিয়েছি, অর্থাৎ শেষটির আগের commit. Revert যেকোনো পুরোনো commit-এ চলে, যদি পরের commit-গুলো একই লাইন না ছুঁয়ে থাকে। ছুঁয়ে থাকলে conflict হতে পারে, যা merge-এর conflict-এর মতো করেই মেটাতে হয় (পর্ব ১৩)। কয়েকটি commit একসাথে উল্টাতে git revert --no-commit A..B, তারপর একটি commit.

Merge commit উল্টানো — -m লাগে

একটি merge commit-এর দুটি parent থাকে। “উল্টো পরিবর্তন” মানে কোন parent-এর তুলনায়? Git নিজে ঠিক করে না, আপনাকে বলতে হয়। একটি আলাদা ছোট repository-তে:

$ git log --oneline --graph
*   075c171 Merge branch 'feature'
|\
| * e82c547 Add feature
* | 0f0d1e0 Main work
|/
* e3e3874 Base
$ git revert HEAD
error: commit 075c1716991ddc948cbac2499e86b0d425698972 is a merge but no -m option was given.
fatal: revert failed
$ git revert -m 1 --no-edit HEAD
[main 897a62d] Revert "Merge branch 'feature'"
 Date: Thu Jan 15 10:14:00 2026 +0600
 1 file changed, 1 deletion(-)
 delete mode 100644 f.txt

-m 1 মানে প্রথম parent (merge-এর সময় যে branch-এ ছিলেন, এখানে main) হলো মূল ধারা; feature থেকে যা এসেছিল, শুধু সেটুকু উল্টানো হবে। একটি সূক্ষ্ম ফাঁদ: এরপর একই feature branch আবার merge করলে Git মনে করে ওই commit-গুলো আগেই মেলানো হয়েছে, তাই পরিবর্তনগুলো আর ফেরে না। তখন revert-টিকেই revert করতে হয়। Git-এর নিজস্ব documentation-এ এ নিয়ে একটি পুরো লেখা আছে (“How to revert a faulty merge”); merge-এর ভেতরের যুক্তি পর্ব ৫৪-এ।

Reset + force push বনাম revert — দুই clone-এ কী ঘটে

এবার মডেলটি দুই মানুষের। Origin-এ A-B-C, আর C-তে একটি bug. করিম C pull করে তার ওপর D (search ফিচার) বানিয়েছেন, এখনো push করেননি। রহিম দুইভাবে bug সরাতে পারেন। প্রথম পথে তিনি reset --hard B করে একটি নতুন fix E বানিয়ে force push করেন। দ্বিতীয় পথে তিনি git revert C দিয়ে R বানিয়ে সাধারণ push করেন। তারপর করিম pull করেন (merge):

// Shared history: A-B-C is on origin; C added a bug. Karim already has C and adds D.
const C = {}; // id -> { parents, files: Set }
function commit(id, parents, files) { C[id] = { parents, files: new Set(files) }; return id; }
function ancestors(id, seen = new Set()) {
  if (!seen.has(id)) { seen.add(id); for (const p of C[id].parents) ancestors(p, seen); } return seen;
}
function mergeBase(a, b) { // nearest common ancestor (enough for this small graph)
  const A = ancestors(a); let best = null;
  for (const x of ancestors(b)) if (A.has(x) && (!best || ancestors(x).has(best))) best = x;
  return best;
}
function merge(id, ours, theirs) { // three-way merge, file by file
  const base = C[mergeBase(ours, theirs)].files, o = C[ours].files, t = C[theirs].files;
  const all = new Set([...base, ...o, ...t]), out = [];
  for (const f of all) { const keep = o.has(f) === base.has(f) ? t.has(f) : o.has(f); if (keep) out.push(f); }
  return commit(id, [ours, theirs], out.sort());
}
const log = (name, id) => console.log(name.padEnd(9), ("{" + [...ancestors(id)].sort().join(",") + "}").padEnd(16),
                                      "files:", [...C[id].files].join(", "));
commit("A", [], ["cart"]); commit("B", ["A"], ["cart", "fee"]); commit("C", ["B"], ["cart", "fee", "BUG"]);
commit("D", ["C"], ["cart", "fee", "BUG", "search"]);          // Karim's local work on top of C

console.log("--- way 1: Rahim runs reset --hard B, adds fix E, push --force");
commit("E", ["B"], ["cart", "fee", "coupon"]);                  // origin/main is now E
console.log("Karim ahead/behind:", [...ancestors("D")].filter(x => !ancestors("E").has(x)).length,
            "/", [...ancestors("E")].filter(x => !ancestors("D").has(x)).length);
log("pull->M1", merge("M1", "D", "E"));

console.log("--- way 2: Rahim runs git revert C (new commit R), normal push");
commit("R", ["C"], ["cart", "fee"]);                            // origin/main is now R
console.log("Karim ahead/behind:", [...ancestors("D")].filter(x => !ancestors("R").has(x)).length,
            "/", [...ancestors("R")].filter(x => !ancestors("D").has(x)).length);
log("pull->M2", merge("M2", "D", "R"));

আউটপুট:

--- way 1: Rahim runs reset --hard B, adds fix E, push --force
Karim ahead/behind: 2 / 1
pull->M1  {A,B,C,D,E,M1}   files: BUG, cart, coupon, fee, search
--- way 2: Rahim runs git revert C (new commit R), normal push
Karim ahead/behind: 1 / 1
pull->M2  {A,B,C,D,M2,R}   files: cart, fee, search

প্রথম পথ: force push-এর পর origin-এ C নেই, কিন্তু করিমের কাছে আছে। তাঁর branch ২টি commit এগিয়ে (C, D), ১টি পিছিয়ে (E). Pull-এর merge-এ base হলো B; করিমের দিকে BUG “নতুন যোগ” দেখায়, তাই merge সেটি রেখে দেয়। ফল: files-এ আবার BUG, আর history-তে C ফিরে এসেছে। পরের push-এ bug আবার origin-এ। দ্বিতীয় পথ: R-এর parent C, তাই করিম শুধু ১ এগিয়ে, ১ পিছিয়ে। Merge-এর base C; R BUG সরিয়েছে আর করিম সেটি ছোঁননি, তাই ফলাফলে BUG নেই, করিমের search আছে। History-তে C আর R দুটিই — সৎ ইতিহাস। (মডেলটি ফাইলের নাম দিয়ে merge করে; আসল Git লাইন ধরে মেলায়, পর্ব ১৩।)

“Public history নতুন করে লিখবেন না” — এই নিয়মটি Git-এর শুরুর দিন থেকেই আলোচিত। Linus Torvalds Linux kernel-এর mailing list-এ বহুবার লিখেছেন যে অন্যরা যে history-র ওপর কাজ করছেন, তা rebase বা reset করে বদলানো উচিত নয়; নিজের এখনো-প্রকাশ-না-করা কাজ যত খুশি গুছিয়ে নিন। একারণেই বহু দলের hosting platform-এ main-এর মতো branch-এ force push বন্ধ করে রাখা হয় (“protected branch”, platform-ভেদে নাম আলাদা)। Branching strategy-তে এ নিয়ম কীভাবে বসে, তা পর্ব ১৭-এ।

৬ · git clean — untracked ফাইল পরিষ্কার

restore আর reset শুধু tracked ফাইল নিয়ে কাজ করে। Build-এর পর জমে থাকা debug.log বা tmp/ ফোল্ডারের মতো যেসব ফাইল Git কখনো track করেনি (untracked), সেগুলো তারা ছোঁয় না। এগুলো সরাতে git clean. নিয়ম একটাই: আগে -n (dry run), তারপর -f.

$ git status --short
?? debug.log
?? tmp/
$ git clean -n
Would remove debug.log
$ git clean -nd
Would remove debug.log
Would remove tmp/
$ git clean -fd
Removing debug.log
Removing tmp/
$ git status --short
Flagঅর্থ
-nশুধু দেখায় কী মুছবে, কিছুই মোছে না — সবসময় আগে এটি
-fসত্যিই মোছা। ডিফল্ট সেটিং (clean.requireForce) এটি ছাড়া clean চলতে দেয় না
-dUntracked ফোল্ডারও ধরে; না দিলে শুধু ফাইল (ওপরে -n-এ tmp/ আসেনি)
-x.gitignore-এ লেখা ফাইলও মোছে — যেমন node_modules/, .env. খুব সাবধানে
-iপ্রতিটি ফাইল ধরে ধরে জিজ্ঞেস করে

Untracked ফাইল Git-এর কাছে কখনো ছিল না, তাই git clean-এ মোছা ফাইল reflog বা অন্য কোনো Git command দিয়ে ফেরে না — সাধারণত recycle bin-এও যায় না। -x বিশেষ বিপজ্জনক: .env-এর মতো ignore করা ফাইলে প্রায়ই এমন গোপন তথ্য থাকে যা repository-তে কখনো রাখা হয়নি। কোন ফাইল ignore হবে, তা পর্ব ১০-এ দেখেছেন।

৭ · Reflog — Git-এর নিরাপত্তা জাল

অংশ ৪-এর শেষে আমরা git reset --hard HEAD~1 দিয়ে d09aad4-কে log থেকে “হারিয়ে” ফেলেছিলাম। এখন কি তাকে ফেরানো যায়? যায়। Git প্রতিবার HEAD বা কোনো branch সরালে একটি local খাতায় লিখে রাখে: আগে কোথায় ছিল, এখন কোথায়, কোন command-এর কারণে। এই খাতার নাম reflog. মুদি দোকানের হিসাবের খাতার মতো — দিনের প্রতিটি লেনদেন ক্রমানুসারে লেখা থাকে, ভুল লেনদেনও।

$ git reflog
f469880 HEAD@{0}: reset: moving to HEAD~1
d09aad4 HEAD@{1}: reset: moving to ORIG_HEAD
f469880 HEAD@{2}: reset: moving to HEAD~1
d09aad4 HEAD@{3}: reset: moving to ORIG_HEAD
f469880 HEAD@{4}: reset: moving to HEAD~1
d09aad4 HEAD@{5}: commit (amend): Add delivery fee
97611f9 HEAD@{6}: commit: Add delivry fee
f469880 HEAD@{7}: commit: Add discount rule
f8f0806 HEAD@{8}: commit: Add cart total
df9c5fe HEAD@{9}: commit (initial): Add README
$ git branch rescue d09aad4
$ git log --oneline -1 rescue
d09aad4 Add delivery fee
$ git reset --hard HEAD@{1}
HEAD is now at d09aad4 Add delivery fee
$ git log --oneline
d09aad4 Add delivery fee
f469880 Add discount rule
f8f0806 Add cart total
df9c5fe Add README
$ git branch -d rescue
Deleted branch rescue (was d09aad4).

ওপর থেকে নিচে নতুন থেকে পুরোনো। HEAD@{0} এখনকার অবস্থান, HEAD@{1} তার ঠিক আগে HEAD যেখানে ছিল, এভাবে। অংশ ৪-এর soft আর mixed পরীক্ষার পর ORIG_HEAD দিয়ে ফেরার চলাচলও লেখা আছে — reflog কিছুই বাদ দেয় না। নিচে 97611f9-ও আছে, amend-এর আগের সেই ভুল-বানানের commit. উদ্ধারের দুটি পথ দেখানো হয়েছে: git branch rescue d09aad4 একটি নতুন branch বসিয়ে commit-টিকে নিরাপদ করে, কিছুই না বদলে; তারপর git reset --hard HEAD@{1} main-কে সেখানে ফেরায়। Working tree-তে অসংরক্ষিত কাজ থাকলে --hard-এর বদলে আগে branch বসানোই নিরাপদ।

Repository-র অবস্থা
main
git log-এ d09aad4
fee.json ফোল্ডারে
HEAD@{1}
rescue branch
  1. সব ঠিক আছে। main d09aad4-এ, fee.json ফোল্ডারে।
  2. ভুল: git reset --hard HEAD~1. main f469880-এ নামল, fee.json ফোল্ডার থেকে উধাও, git log-এ d09aad4 নেই। আতঙ্কের মুহূর্ত — কিন্তু commit-টি object store-এ এখনো আছে।
  3. git reflog. খাতার দ্বিতীয় লাইনে d09aad4 HEAD@{1}: “এক চলাচল আগে HEAD এখানে ছিল”। বার্তা দেখে নিশ্চিত হোন এটিই সেই commit. আর কোনো command চালানোর আগে hash-টি লিখে রাখুন, কারণ পরের প্রতিটি চলাচলে HEAD@{1}-এর অর্থ বদলায়।
  4. git branch rescue d09aad4. আগে একটি branch বসিয়ে commit-টিকে “reachable” করলেন। এখন garbage collection-ও তাকে ছোঁবে না, আর আপনার working tree অক্ষত।
  5. git reset --hard HEAD@{1}. main ফিরল d09aad4-এ, fee.json ফোল্ডারে ফিরল। Reflog-এ নতুন একটি লাইন যোগ হলো, তাই এখন HEAD@{1} মানে f469880. কাজ শেষে rescue মুছে দিতে পারেন।

নিচের মডেলে একটি “ব্যস্ত, একটু অসাবধান দিন” তৈরি হয় seed-দেওয়া এলোমেলো সংখ্যা দিয়ে (PRNG mulberry32, তাই প্রতিবার একই ফল): প্রতি ধাপে হয় নতুন commit, নয়তো একটি reset --hard HEAD~1. তারপর প্রোগ্রাম খোঁজে কোন commit reflog-এ আছে কিন্তু main থেকে আর পৌঁছানো যায় না, আর সেগুলো ফেরায়। Hash এখানে FNV-1a, Git-এর SHA-1-এর বদলে একটি ছোট হিসাব:

// Reflog: every move of HEAD is written down, so a "lost" commit can be found again.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
function fnv1a(s) { let h = 0x811c9dc5; for (const ch of s) { h ^= ch.charCodeAt(0); h = Math.imul(h, 0x01000193); }
  return (h >>> 0).toString(16).padStart(8, "0").slice(0, 7); }   // stands in for Git's SHA-1
const parentOf = {}, reflog = [];        // reflog[0] is the newest entry, like HEAD@{0}
let head = null, n = 0;
function move(to, why) { head = to; reflog.unshift({ id: to, why }); }
function commit(msg) { const id = fnv1a(msg + (head || "") + n++); parentOf[id] = head; move(id, "commit: " + msg); }
const rnd = mulberry32(15);
commit("Add README");
for (let i = 1; i <= 7; i++) {                            // a busy, slightly careless day
  if (rnd() < 0.6 || !parentOf[head]) commit("work " + i);
  else move(parentOf[head], "reset: moving to HEAD~1");     // like git reset --hard HEAD~1
}
reflog.forEach((e, i) => console.log(`${e.id} HEAD@{${i}}: ${e.why}`));
const reachable = new Set(); for (let c = head; c; c = parentOf[c]) reachable.add(c);
const lost = [...new Set(reflog.map(e => e.id))].filter(id => !reachable.has(id));
console.log("on main now :", [...reachable].reverse().join(" <- "));
console.log("only in reflog (lost):", lost.join(", ") || "none");
const tip = lost.find(id => !lost.some(x => parentOf[x] === id));   // lost commit with no lost child
const pick = reflog.findIndex(e => e.id === tip);
move(tip, `reset: moving to HEAD@{${pick}}`);              // git reset --hard HEAD@{n}
const back = []; for (let c = head; c; c = parentOf[c]) back.push(c);
console.log(`git reset --hard HEAD@{${pick}} -> HEAD = ${head}`);
console.log("still lost:", lost.filter(id => !back.includes(id)).join(", ") || "none");

আউটপুট:

345a3eb HEAD@{0}: reset: moving to HEAD~1
f0bbec3 HEAD@{1}: reset: moving to HEAD~1
68f8092 HEAD@{2}: commit: work 5
f0bbec3 HEAD@{3}: commit: work 4
345a3eb HEAD@{4}: commit: work 3
4286e30 HEAD@{5}: commit: work 2
04f1121 HEAD@{6}: commit: work 1
69c1b2e HEAD@{7}: commit: Add README
on main now : 69c1b2e <- 04f1121 <- 4286e30 <- 345a3eb
only in reflog (lost): f0bbec3, 68f8092
git reset --hard HEAD@{2} -> HEAD = 68f8092
still lost: none

দিনের শেষে পরপর দুটি reset main-কে 345a3eb-এ নামিয়েছে; f0bbec3 (work 4) আর 68f8092 (work 5) এখন শুধু reflog-এ। দুটি হারানো commit-এর মধ্যে কোনটিতে ফিরবেন? যেটির কোনো হারানো সন্তান নেই — 68f8092, কারণ তার parent f0bbec3. সেখানে ফিরলে দুটিই আবার main থেকে পৌঁছানো যায়, তাই “still lost: none”. লক্ষ করুন, উদ্ধারের সংখ্যা HEAD@{2}, HEAD@{1} নয় — reflog পড়ে বুঝে নম্বর বাছতে হয়, অনুমানে নয়। seed বদলে চালিয়ে দেখুন অন্য দিনে কী হয়।

যা ফেরানো যায় না

Reflog শুধু commit-এর হিসাব রাখে। নিচের জিনিসগুলো সে বাঁচাতে পারে না:

  • যে পরিবর্তন কখনো add বা commit হয়নি, আর git restore, git checkout -- f বা reset --hard-এ মুছে গেছে। (আপনার editor-এর নিজস্ব “local history” থাকলে সেখানে ভাগ্য পরীক্ষা করতে পারেন — সেটি Git নয়।)
  • git clean-এ মোছা untracked ফাইল।
  • অন্য যন্ত্রের reflog: reflog local, push বা clone হয় না। Server-এ কেউ force push করলে আপনার যন্ত্রের reflog শুধু আপনার নিজের চলাচল জানে।
  • অনেক পুরোনো entry: ডিফল্টভাবে reflog-এর entry ৯০ দিন, আর কোনো branch থেকে পৌঁছানো যায় না এমন commit-এর entry ৩০ দিন পর garbage collection-এ মুছতে পারে (gc.reflogExpire, gc.reflogExpireUnreachable).

একটি মাঝামাঝি ঘটনা: যা add করেছিলেন কিন্তু commit করেননি, তার লেখা একটি blob হিসেবে object store-এ কিছুদিন থেকে যায়, git fsck --lost-found দিয়ে খোঁজা যায়। Ref, reflog আর garbage collection-এর পুরো গল্প পর্ব ৫৩-এ।

৮ · শেয়ার করা branch-এর সোনালি নিয়ম

পুরো পর্বকে একটি সারণিতে আনা যায়: কোন command কোন এলাকা বদলায়, আর কোনটি history নতুন করে লেখে।

CommandBranchIndexWorking treeHistory বদলায়?
restore f——index থেকে ⚠না
restore --staged f—HEAD থেকে—না
commit --amendনতুন commit-এ——হ্যাঁ (শেষ commit)
reset --soft XX-এ——হ্যাঁ
reset X (mixed)X-এX থেকে—হ্যাঁ
reset --hard XX-এX থেকেX থেকে ⚠হ্যাঁ
revert Xনতুন commit যোগনতুন commit-এর মতোনতুন commit-এর মতোনা — শুধু যোগ করে
clean -f——untracked মোছে ⚠না
যা শুধু আপনার কাছে, তা যত খুশি গুছিয়ে নিন।
যা অন্যদের কাছে গেছে, তাতে শুধু নতুন commit যোগ করুন।
  1. Push-করা commit-এ --amend, reset বা rebase নয়। উল্টাতে হলে git revert.
  2. Shared branch-এ force push নয়। নিজের feature branch-এ একান্ত দরকারে git push --force-with-lease — এটি শেষ fetch-এর পর অন্য কেউ push করে থাকলে থেমে যায়, সাধারণ --force থামে না (remote-এর বিস্তারিত পর্ব ১৪-এ)।
  3. বিপজ্জনক command-এর আগে git status আর git stash বা একটি backup branch. git branch backup-before-reset লিখতে দুই সেকেন্ড লাগে।
  4. git clean-এ সবসময় আগে -n.
  5. কিছু হারালে থামুন। আর command না চালিয়ে git reflog পড়ুন, hash লিখে রাখুন, branch বসান।
  6. Secret ভুলে push হয়ে গেলে revert যথেষ্ট নয়। সেটি history-তে থেকে যায়; আগে secret বাতিল করে নতুন বানান, তারপর history পরিষ্কারের কথা ভাবুন (পর্ব ৫৭)।

পুরোনো local commit গুছিয়ে নেওয়ার আরও সূক্ষ্ম হাতিয়ার আছে — কয়েকটি commit মেলানো, ক্রম বদলানো, মাঝের একটির বার্তা বদলানো। সেটি interactive rebase, পর্ব ১৫-এ। আর push হওয়ার আগে কাজ গুছিয়ে pull request খোলার ধারা পর্ব ১৬-এ।

আরও গভীরে: reset-এর আসল সংজ্ঞা Git-এর ভেতরের তিনটি “tree”-র ভাষায় — HEAD যে commit-এর tree দেখায়, index-এর flat তালিকা আর working directory. Scott Chacon ও Ben Straub-এর বিনামূল্যের বই Pro Git (২য় সংস্করণ, ২০১৪)-এর “Reset Demystified” অধ্যায়টি ঠিক এই তিন tree দিয়ে reset ব্যাখ্যা করে। Reflog আসলে .git/logs/ ফোল্ডারের সাধারণ text ফাইল; একটি commit “হারায়” তখনই, যখন কোনো ref বা reflog entry তার দিকে নির্দেশ করে না আর git gc তাকে prune করে।

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

  • git reset আর git revert-এর পার্থক্য? — reset branch-কে পেছনে সরিয়ে history বদলায়; revert একটি নতুন উল্টো commit যোগ করে, history অটুট রাখে।
  • --soft, --mixed, --hard? — যথাক্রমে শুধু branch; branch ও index; branch, index ও working tree.
  • ভুল reset --hard-এর পর commit ফেরাবেন কীভাবে? — git reflog-এ hash খুঁজে git branch rescue <hash> বা git reset --hard HEAD@{n}.
  • Unstage করবেন কীভাবে? — git restore --staged f (পুরোনো রূপ git reset HEAD f).
  • Amend-এর পর commit ID কেন বদলায়? — ID বিষয়বস্তু, parent, সময় আর বার্তার hash; amend একটি নতুন commit বানায়।

৯ · AI-এর যুগে — undo-র পরামর্শ যাচাই

“Git-এ গোলমাল করে ফেলেছি, কীভাবে ঠিক করব?” — এই প্রশ্নে AI প্রায়ই সবচেয়ে ছোট উত্তরটি দেয়: git reset --hard origin/main বা git push --force. দুটিই কাজ করে, আর দুটিই বিনা সতর্কতায় কাজ মুছে দিতে পারে। AI আপনার working tree দেখতে পায় না, জানে না কোন branch শেয়ার করা, কে কী pull করেছেন। Undo-র ক্ষেত্রে AI-এর সাধারণ ভুলগুলো:

AI যা বলতে পারেসমস্যাযা করবেন
git reset --hard, কোনো সতর্কতা ছাড়াঅসংরক্ষিত কাজ চিরতরে যায়আগে git status, দরকারে git stash বা backup branch
Shared main-এ git push --forceসহকর্মীদের history ভাঙে, bug ফিরে আসেgit revert; নিজের branch-এ হলে --force-with-lease
না থাকা flag, যেমন git revert --hardCommand ব্যর্থ, বা বিভ্রান্তিgit help revert বা অফিশিয়াল documentation দেখুন
শুধু পুরোনো checkout --Branch আর ফাইলের কাজ গুলিয়ে যায়restore/switch (Git ২.২৩+)
“reflog-এ সব আছে”Add-না-করা কাজ reflog-এ থাকে নাবিপজ্জনক command-এর আগে commit বা stash
git clean -fdx এক লাইনে.env-সহ ignore করা ফাইল মোছেআগে git clean -nd, -x শুধু জেনে-বুঝে

AI-কে প্রশ্ন করার সময় অবস্থা পুরোটা দিন, আর বলুন প্রতিটি command কোন এলাকা বদলাবে তা ব্যাখ্যা করতে। Command নিজের আসল repository-তে চালানোর আগে একটি throwaway clone-এ (git clone bazar /tmp/bazar-test) চালিয়ে দেখুন:

আমার Git repository-র অবস্থা নিচে দিলাম (git status, git log --oneline -5, git branch -vv আর git reflog -10 এর আউটপুট): [এখানে আউটপুট পেস্ট করুন] আমি শেষ commit-টি বাতিল করতে চাই। এই branch-টি দলের সাথে শেয়ার করা, আর commit-টি push হয়ে গেছে। ১. কোন command দেবে, আর কেন — reset না revert? ২. প্রতিটি command branch, index আর working tree-র কোনটি বদলাবে, আলাদা করে বলো। ৩. কোনো command-এ অসংরক্ষিত কাজ হারানোর ঝুঁকি থাকলে স্পষ্ট করে সতর্ক করো, আর আগে কী backup নেব বলো। ৪. --force বা --hard ব্যবহার কোরো না, যদি না না-করে উপায় না থাকে; থাকলে কারণ বলো। ৫. যে flag ব্যবহার করবে, তা Git-এর কোন version থেকে আছে বলো, যাতে আমি git help দিয়ে মিলিয়ে নিতে পারি।

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

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

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

প্র ০১Git ২.২৩-এ কেন git checkout-কে ভেঙে switch আর restore বানানো হলো? পুরোনো command তো কাজ করছিল।

কাজ করছিল ঠিকই, কিন্তু একটি command-এর ঘাড়ে অনেকগুলো ভিন্ন কাজ ছিল। git checkout main branch বদলায়। git checkout -b new নতুন branch বানিয়ে সেখানে যায়। git checkout a1b2c3d একটি commit-এ “detached HEAD” অবস্থায় যায়। আর git checkout -- cart.js ফাইলের অসংরক্ষিত পরিবর্তন কোনো প্রশ্ন ছাড়াই মুছে দেয়। প্রথম তিনটি সাধারণত নিরাপদ — Git অসংরক্ষিত কাজ ঝুঁকিতে পড়লে থেমে যায়। চতুর্থটি ধ্বংসাত্মক। অথচ সবগুলোর নাম এক।

সমস্যাটা বাস্তব ছিল: যদি cart.js নামে একটি branch-ও থাকে? git checkout cart.js তখন branch বদলাবে, না ফাইল মুছবে? Git একটি নিয়ম মেনে সিদ্ধান্ত নেয়, আর -- চিহ্ন দিয়ে অস্পষ্টতা দূর করতে হয় — কিন্তু নতুনদের কাছে এটা ধাঁধা। বহু বছর ধরে Git-এর ব্যবহারযোগ্যতা নিয়ে সমালোচনার বড় অংশ ছিল এই command ঘিরে।

২০১৯ সালের আগস্টে প্রকাশিত Git ২.২৩-এ দুটি নতুন command আসে: git switch শুধু branch-এর কাজ করে, git restore শুধু ফাইলের। Git mailing list-এর প্রকাশ্য আলোচনা অনুযায়ী মূল কাজটি করেছিলেন দীর্ঘদিনের contributor Nguyễn Thái Ngọc Duy. শুরুতে এগুলোকে documentation-এ “experimental” বলা হয়েছিল, যাতে প্রয়োজনে আচরণ বদলানো যায়। পুরোনো checkout সরানো হয়নি — লাখো script আর অভ্যাস ভেঙে যেত বলে। Git-এর নীতি সাধারণত এমনই: পুরোনো আচরণ রেখে নতুন, পরিষ্কার পথ যোগ করা।

এখানে software engineering-এর একটি বড় শিক্ষা আছে। একটি interface-এর নকশা শুধু “কাজ করে কি না” দিয়ে বিচার হয় না; ব্যবহারকারী কত সহজে ভুল করেন, সেটিও মাপকাঠি। নিরাপদ আর ধ্বংসাত্মক কাজকে আলাদা নাম দেওয়া, ধ্বংসাত্মক কাজকে লম্বা বা স্পষ্ট flag-এর পেছনে রাখা — ভালো API নকশার সাধারণ নীতি (পর্ব ৩১-এ interface আর contract).

মূল উপলব্ধি: এক নামে দুটি ভিন্ন ঝুঁকির কাজ রাখলে ভুল হবেই। নতুন command এসেছে মানুষের ভুল কমাতে, ক্ষমতা বাড়াতে নয় — তাই নতুন রূপটিই অভ্যাস করুন, আর পুরোনোটি পড়ে চিনতে শিখুন।

প্র ০২History নতুন করে লেখা কি সবসময় খারাপ? তাহলে reset, amend আর rebase আছেই বা কেন?

না, খারাপ নয় — প্রশ্নটা কার history. নিজের যন্ত্রে, এখনো কাউকে না দেখানো commit-গুলো আপনার খসড়া খাতা। সেখানে “WIP”, “fix typo”, “আবার চেষ্টা” ধরনের দশটি commit গুছিয়ে তিনটি পরিষ্কার, যুক্তিসঙ্গত commit বানানো ভালো চর্চা। Reviewer সহজে বোঝেন, পরে git bisect দিয়ে bug খুঁজতে সুবিধা হয় (পর্ব ৫৫), আর history একটি পরিষ্কার গল্প বলে। এই কাজের জন্যই amend, reset --soft আর interactive rebase.

সমস্যা শুরু হয় যখন commit অন্যদের কাছে চলে যায়। Git-এ commit ID তার parent-এর ID-র ওপর নির্ভর করে, তাই একটি commit বদলালে তার পরের সব commit-এর ID বদলায়। আপনার সহকর্মীর কাছে পুরোনো ID-গুলো আছে, তার ওপর তিনি নতুন কাজ করেছেন। আপনি force push করলে তাঁর পরের pull-এ Git দুটি আলাদা history দেখে মেলাতে চায় — কখনো conflict, কখনো মুছে ফেলা commit-এর পুনরাগমন, যেমন ওপরের মডেলে BUG ফিরে এসেছিল। আরও খারাপ: কেউ যদি নিজের কাজ push করার আগে force push হওয়া branch-এর ওপর reset --hard origin/main করেন, তাঁর commit সেখান থেকে হারাতে পারে।

Linux kernel-এর মতো বড় project-এ এই নিয়ম লিখিত সংস্কৃতি: maintainer-রা প্রকাশিত tree rebase করেন না, আর Linus Torvalds বহুবার mailing list-এ এ নিয়ে স্পষ্ট মত দিয়েছেন। অন্যদিকে অনেক দল pull request merge করার আগে branch “squash” বা rebase করে — কারণ সেই branch তখনো শুধু একজনের কাজ। অর্থাৎ নিয়মটি একই, প্রয়োগ পরিস্থিতিভেদে।

Platform-গুলো এই নিয়ম প্রযুক্তি দিয়ে চাপিয়ে দিতে পারে: main-এ force push বন্ধ, merge শুধু pull request দিয়ে, review বাধ্যতামূলক। আর ব্যক্তিগত স্তরে --force-with-lease একটি নিরাপত্তা বেল্ট: “শেষবার আমি যা দেখেছিলাম, server-এ যদি এখনো তাই থাকে, তবেই চাপিয়ে দেওয়া হবে”।

মূল উপলব্ধি: নিজের খসড়া গুছিয়ে নিন, অন্যের হাতে যাওয়া ইতিহাসে শুধু যোগ করুন। “History বদলানো যাবে কি না” প্রযুক্তির প্রশ্ন নয়, দলের সাথে চুক্তির প্রশ্ন।

প্র ০৩“Git-এ commit করা কাজ প্রায় কখনো হারায় না” — কথাটা কতটা সত্যি, আর কোথায় এর সীমা?

কথাটা মূলত সত্যি, আর এর কারণ Git-এর নকশায়। Git-এর object store-এ একবার লেখা commit, tree বা blob কখনো “সম্পাদিত” হয় না; নতুন কিছু হলে নতুন object লেখা হয়। Branch, tag, HEAD — এগুলো শুধু নির্দেশক (ref), যারা কোনো commit-এর ID ধরে রাখে। reset, amend, rebase কোনো object মোছে না; শুধু নির্দেশক সরায়। আর প্রতিটি সরানো reflog-এ লেখা হয়।

তাহলে object কখন সত্যিই মোছে? git gc (garbage collection) যখন চলে — হাতে চালালে, বা অনেক command-এর পর Git নিজেই মাঝেমধ্যে চালায় — তখন সে দেখে কোন object কোনো ref বা reflog entry থেকে পৌঁছানো যায় না। এরকম object-ও একটি সময়সীমা পার হলে তবেই মোছে (gc.pruneExpire, ডিফল্ট প্রায় দুই সপ্তাহ)। তার ওপর reflog entry নিজেই ডিফল্টভাবে ৯০ দিন, আর unreachable commit-এর entry ৩০ দিন থাকে। অর্থাৎ ভুল reset --hard-এর পর সাধারণত কয়েক সপ্তাহ সময় পান।

সীমাগুলো কোথায়? এক, reflog local — অন্য যন্ত্রে আপনার চলাচলের কোনো খাতা নেই, আর server-এ সাধারণত reflog বন্ধ বা সীমিত থাকে (platform-ভেদে আলাদা)। নতুন clone-এ reflog খালি শুরু হয়। দুই, যে কাজ commit হয়নি, তা object store-এ ঢোকেইনি — Git-এর গ্যারান্টি সেখানে খাটে না। তিন, কেউ জেনে-বুঝে git reflog expire --expire=now --all আর git gc --prune=now চালালে সব নিরাপত্তা জাল একসাথে সরে যায়; secret মুছে ফেলার সময় ঠিক এটাই উদ্দেশ্য থাকে (পর্ব ৫৭)।

এই নকশার একটি সুন্দর দিক: undo নিরাপদ হওয়ার কারণ “পরিবর্তনযোগ্য নয় এমন data + সরানো যায় এমন নির্দেশক”। Database-এর append-only log, event sourcing বা হিসাবের খাতার double-entry — অনেক নির্ভরযোগ্য ব্যবস্থা একই কৌশলে দাঁড়িয়ে (System Design কোর্সের event sourcing দেখুন)।

মূল উপলব্ধি: Git-এর নিরাপত্তা জাল শুধু commit-এর জন্য, আর সেটিও সময়সীমিত ও local. তাই ঝুঁকির কাজের আগে ছোট একটি commit বা stash — সবচেয়ে সস্তা বীমা।

প্র ০৪Git-এ একটি সাধারণ “undo” বোতাম নেই কেন? অন্য version control system কীভাবে করে?

Word processor-এ Ctrl+Z শেষ কাজটি উল্টায়, কারণ সেখানে একটিই অবস্থা — খোলা নথি। Git-এ অবস্থা অন্তত চার জায়গায়: working tree, index, local branch আর remote. “শেষ কাজ উল্টানো” বললে প্রশ্ন ওঠে: শেষ কাজ কোনটি — শেষ add, শেষ commit, না শেষ push? আর উল্টানো মানে কি মুছে ফেলা, না উল্টো একটি commit যোগ? ভিন্ন উত্তরের ঝুঁকি ভিন্ন। তাই Git আলাদা command দেয়, প্রতিটির স্পষ্ট অর্থসহ।

অন্য ব্যবস্থাগুলোও এই দ্বিধায় পড়েছে। Subversion-এর মতো centralized ব্যবস্থায় (পর্ব ৭) commit সরাসরি server-এ যায়, তাই সেখানে commit “মোছা” সাধারণ কাজ নয়; একটি পুরোনো পরিবর্তন উল্টাতে উল্টো merge করে নতুন commit দেওয়া হয় — Git-এর revert-এর মতো। Mercurial-এ hg backout প্রায় revert-এর কাজ করে; তার পুরোনো hg rollback শেষ transaction উল্টাত, কিন্তু ঝুঁকির কারণে পরে নিরুৎসাহিত করা হয় — যতদূর জানা যায়, Mercurial-এর documentation-এ এটি deprecated বলা আছে।

Git-এর জগতে কিছু তৃতীয় পক্ষের টুল সত্যিকারের “undo” দেওয়ার চেষ্টা করেছে — reflog আর নিজস্ব হিসাব পড়ে আগের অবস্থায় ফেরা। কিছু GUI client-এও undo বোতাম আছে। কিন্তু এগুলো working tree-র অসংরক্ষিত কাজ ফেরাতে পারে না, কারণ সেই তথ্য কোথাও লেখা থাকে না।

এখানে নকশার একটি গভীর trade-off আছে। একটি জাদুর বোতাম শেখা সহজ, কিন্তু কী ঘটছে লুকিয়ে রাখে; দলের shared history-তে ভুল জাদু বড় ক্ষতি করতে পারে। আলাদা আলাদা স্পষ্ট command শেখা কঠিন, কিন্তু প্রতিটির পরিণাম আগে থেকে বলা যায়। পেশাদার টুলের নকশায় এই পূর্বানুমেয়তা প্রায়ই বেশি মূল্যবান।

মূল উপলব্ধি: “Undo” তখনই সহজ, যখন অবস্থা এক জায়গায়। Git-এ অবস্থা কয়েক স্তরে, তাই undo-ও কয়েক রকম — আর সঠিকটি বাছার চাবি হলো জানা যে ভুলটি কোন স্তরে।

প্র ০৫একটি API key ভুলে commit হয়ে push হয়ে গেছে। git revert দিলেই কি সমস্যা মিটবে?

না — আর এটাই revert-এর সীমা বোঝার সবচেয়ে ভালো উদাহরণ। Revert একটি নতুন commit যোগ করে, যেখানে key-টি ফাইল থেকে সরানো। কিন্তু পুরোনো commit history-তে অক্ষত থাকে; যে কেউ git log -p বা git show দিয়ে key-টি পড়তে পারেন। Repository public হলে ধরে নিতে হবে key-টি ইতিমধ্যে কেউ দেখে ফেলেছে — বিভিন্ন নিরাপত্তা গবেষণা ও platform-এর প্রকাশিত অভিজ্ঞতায় দেখা গেছে যে public repository-তে ফাঁস হওয়া key স্বয়ংক্রিয় টুল খুব দ্রুত খুঁজে নিতে পারে।

তাই সঠিক ক্রম হলো:

  • প্রথমে key বাতিল (revoke/rotate). যে সেবার key, সেখানে গিয়ে পুরোনোটি বন্ধ করে নতুন একটি বানান। এটিই একমাত্র ধাপ যা সত্যিকারের ক্ষতি থামায়; history পরিষ্কার করার আগেই করুন।
  • লগ দেখা: সেবার access log-এ সন্দেহজনক ব্যবহার হয়েছে কি না দেখুন।
  • ভবিষ্যৎ ঠেকানো: key environment variable বা secret manager-এ রাখুন, ফাইলটি .gitignore-এ যোগ করুন (পর্ব ১০), আর commit-এর আগে secret খোঁজার টুল বা hook চালান।
  • History পরিষ্কার (যদি দরকার হয়): git filter-repo-এর মতো টুল দিয়ে পুরো history থেকে ফাইলটি সরানো যায়, কিন্তু এটি সব commit ID বদলায়, সব সহকর্মীকে নতুন করে clone করতে হয়, আর hosting platform-এর cache বা fork-এ কপি থেকে যেতে পারে। পদ্ধতিটি পর্ব ৫৭-এ।

এখানে undo-র দুই ধারণা মুখোমুখি। Revert “কার্যকারিতা” উল্টায় — পরের build-এ key আর থাকে না। কিন্তু নিরাপত্তার প্রশ্ন “কে জানে” — আর তথ্য একবার বেরিয়ে গেলে কোনো Git command সেটি ফেরাতে পারে না।

মূল উপলব্ধি: কোড উল্টানো যায়, প্রকাশিত তথ্য ফেরানো যায় না। Secret ফাঁস হলে Git-এর আগে secret নিজেই বদলান; history পরিষ্কার দ্বিতীয় ধাপ, প্রথম নয়।

অনুশীলন

আউটপুট অনুমান, পরিস্থিতি বিচার, command-এর ক্রম আর নিয়মের সমালোচনা — প্রতিটির পূর্ণ উত্তর দেওয়া আছে। নিজে চালিয়ে দেখতে চাইলে একটি scratch ফোল্ডার ব্যবহার করুন।

  1. তিন এলাকার অবস্থা অনুমান করুন: একটি repository-তে commit C1 → C2 → C3; প্রতিটিতে cart.js-এর রূপ যথাক্রমে v1, v2, v3. main C3-এ। আপনি ফাইলটি বদলে v4 করে git add দিলেন, তারপর আবার বদলে v5 করলেন (add করেননি)। এবার পরপর তিনটি command: (ক) git reset --soft HEAD~1 (খ) git restore --staged cart.js (গ) git reset --hard. প্রতিটির পর main, index আর working tree-তে কী থাকবে? শেষে v3, v4 আর v5-এর কোনগুলো ফেরানো সম্ভব, আর কীভাবে?

    (ক) main = C2, index = v4, working tree = v5. Soft শুধু branch সরায়। git status: staged পরিবর্তন (v2 → v4) আর unstaged পরিবর্তন (v4 → v5).

    (খ) main = C2, index = v2 (এখনকার HEAD থেকে), working tree = v5. শুধু unstaged পরিবর্তন (v2 → v5).

    (গ) commit না দিলে git reset --hard মানে git reset --hard HEAD: main = C2, index = v2, working tree = v2. সব পরিষ্কার।

    কী ফেরে: v3 ফেরে — সেটি C3-এ commit করা, reflog-এ C3 আছে: git reset --hard HEAD@{1}-এর মতো কিছু (নম্বর reflog দেখে), বা git branch rescue <C3-এর hash>. v4 সম্ভবত ফেরে, তবে কষ্টে — add করার সময় তার blob object store-এ লেখা হয়েছিল, কোনো commit নেই, তাই reflog-এ নেই; git fsck --lost-found দিয়ে dangling blob খুঁজে পড়া যায়, যতদিন gc মুছে না দেয় (পর্ব ৫৩)। v5 ফেরে না — কখনো add বা commit হয়নি, Git সেটি দেখেইনি। এই অনুশীলনের ধাপ (ক) আর (খ) অংশ ৪-এর JavaScript মডেলে একটু বদলে মিলিয়ে নিতে পারেন।

  2. কোন command, আর কেন: প্রতিটি পরিস্থিতিতে সবচেয়ে উপযুক্ত command আর এক লাইনে কারণ লিখুন। (ক) git add . দিয়ে ভুলে secrets.txt-ও stage করেছেন; commit করেননি। (খ) এইমাত্র commit করেছেন, push করেননি; বার্তায় issue নম্বর লিখতে ভুলেছেন। (গ) শেষ চারটি commit local, push হয়নি; সেগুলো মিলিয়ে একটি commit বানাতে চান। (ঘ) দুই দিন আগে main-এ push হওয়া একটি commit production-এ bug ঢুকিয়েছে; তার পরে আরও ছয়টি commit এসেছে। (ঙ) একটি পরীক্ষামূলক কাজে শুধু config.js এলোমেলো হয়েছে, অন্য ফাইলের কাজ রাখতে চান।

    (ক) git restore --staged secrets.txt — শুধু index থেকে নামে, ফাইল থাকে। তারপর ফাইলটি .gitignore-এ যোগ করুন, যাতে আবার না আসে।

    (খ) git commit --amend (editor-এ বার্তা ঠিক করুন, বা -m) — শেষ commit, push হয়নি, তাই history বদলানো নিরাপদ।

    (গ) git reset --soft HEAD~4, তারপর git commit — চারটির সব পরিবর্তন staged থাকে, একটি commit-এ ঢোকে। (একই কাজ interactive rebase-এর squash দিয়েও হয়, পর্ব ১৫।)

    (ঘ) git revert <সেই commit> — শেয়ার করা history; পরের ছয়টি commit আর অন্যদের clone অক্ষত থাকে। Conflict হলে মিটিয়ে commit করুন, আর বার্তায় কারণ লিখুন।

    (ঙ) git restore config.js — শুধু সেই ফাইল index-এর অবস্থায় ফেরে, বাকি ফাইল অক্ষত। reset --hard এখানে ভুল, কারণ সেটি সব ফাইলের কাজ মুছত। চালানোর আগে git diff config.js দেখে নিন যে সত্যিই কিছু রাখার মতো নেই।

  3. উদ্ধারের command-ক্রম: একজন সহকর্মী ক্লান্ত হয়ে feature branch-এ git reset --hard HEAD~3 দিয়ে ফেলেছেন। তিনটি commit-ই local ছিল, push হয়নি। Working tree-তে তখন কোনো অসংরক্ষিত কাজ ছিল না। তাঁকে ধাপে ধাপে কী কী command দিতে বলবেন, আর প্রতিটির পর কী দেখে নিশ্চিত হবেন? কোন ভুল এড়াতে বলবেন?
    1. থামা। আর কোনো commit, reset বা checkout নয় — প্রতিটি নতুন চলাচল reflog-এর নম্বর বদলায়।
    2. git reflog -5 — প্রথম লাইন হবে reset: moving to HEAD~3, তার নিচের লাইনে (HEAD@{1}) reset-এর আগের commit. বার্তা দেখে নিশ্চিত হোন এটি তিন commit-এর শেষটি। Hash লিখে রাখুন।
    3. git branch rescue <hash> — commit-টিকে একটি নামে বাঁধা; এখন আর কিছু হারানোর ভয় নেই।
    4. git log --oneline -4 rescue — তিনটি হারানো commit আর তার নিচে এখনকার HEAD দেখা যাবে। মিলে গেলে ঠিক জায়গা।
    5. git reset --hard rescue — branch আবার আগের জায়গায়। (Working tree পরিষ্কার ছিল বলেই --hard নিরাপদ।) চাইলে ORIG_HEAD-ও কাজ করত, কিন্তু reflog দেখে hash দিয়ে কাজ করা বেশি নিশ্চিত।
    6. git log --oneline -4 আর git status দিয়ে শেষ যাচাই, তারপর git branch -d rescue.

    যে ভুল এড়াবেন: অনুমানে HEAD@{3} লেখা (“তিনটি commit গেছে তাই ৩”) — reflog-এর নম্বর চলাচল গোনে, commit নয়; এখানে ঠিক সংখ্যা ১। আর কোনো অবস্থাতেই এই সময়ে git gc --prune=now নয়।

  4. দলের নিয়মের সমালোচনা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের দল নিচের নিয়মগুলো লিখেছে। প্রতিটির সমস্যা কী, আর কীভাবে লিখলে ভালো হতো? (১) “main-এ ভুল commit গেলে যিনি দিয়েছেন তিনি reset --hard করে push --force দেবেন, যাতে history পরিষ্কার থাকে।” (২) “Commit-এর বার্তা ভুল হলে push-এর পরেও amend করা যাবে।” (৩) “প্রতি শুক্রবার git clean -fdx চালিয়ে সবাই ফোল্ডার পরিষ্কার রাখবেন।” (৪) “Reflog আছে, তাই backup branch লাগবে না।”

    (১) Shared history নতুন করে লেখা — যাঁরা ইতিমধ্যে pull করেছেন, তাঁদের clone থেকে ভুল commit আবার ফিরতে পারে, বা তাঁদের কাজ হারাতে পারে। ভালো রূপ: “main-এ ভুল commit গেলে git revert দিয়ে একটি নতুন commit, বার্তায় কারণসহ। main-এ force push platform-এ বন্ধ থাকবে।”

    (২) Amend নতুন commit ID বানায় — push-এর পর করলে আবার force push লাগে, একই সমস্যা। ভালো রূপ: “Push-এর আগে amend চলবে; push-এর পর বার্তার ছোট ভুল রেখে দেওয়া হবে, বা দরকারে একটি নতুন commit.”

    (৩) -x ignore করা ফাইলও মোছে — .env, local config, ডাউনলোড করা ডেটা; সেগুলো কোথাও ফেরে না। আর -n ছাড়া চালানো অন্ধ মোছা। ভালো রূপ: “দরকার হলে git clean -nd দেখে তারপর git clean -fd; -x শুধু build ফোল্ডারের জন্য, জেনে-বুঝে।”

    (৪) Reflog শুধু commit বাঁচায়, শুধু local, আর সময়সীমিত; অসংরক্ষিত কাজ বা অন্য যন্ত্রের অবস্থা জানে না। ভালো রূপ: “ঝুঁকির কাজের (reset, rebase, history পরিষ্কার) আগে git status, আর একটি backup branch বা stash.”

    সাধারণ শিক্ষা: ভালো দলীয় নিয়ম “কী করবেন” বলে, কারণ লেখে, আর platform-এর সেটিং দিয়ে যতটা সম্ভব স্বয়ংক্রিয়ভাবে চাপিয়ে দেয় — মানুষের মনে রাখার ওপর ছেড়ে দেয় না।

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

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

ক্রমভুল git reset --hard-এর পর একটি local commit উদ্ধারের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন কোনো command না চালিয়ে git reflog পড়া
  2. বার্তা দেখে হারানো commit-এর hash চিহ্নিত করা
  3. git branch rescue <hash> দিয়ে commit-টিকে নামে বাঁধা
  4. git log --oneline rescue দিয়ে যাচাই
  5. git reset --hard rescue দিয়ে branch ফেরানো

একটি branch-এ শেষ commit C3, তার parent C2. Working tree পরিষ্কার। git reset --soft HEAD~1 চালানোর পর কী হবে?

  • C3 মুছে যাবে আর ফাইল C2-এর মতো হয়ে যাবে
  • Branch C2-তে যাবে, C3-এর পরিবর্তনগুলো staged অবস্থায় থাকবে
  • Branch C2-তে যাবে, পরিবর্তনগুলো unstaged থাকবে
  • একটি নতুন উল্টো commit তৈরি হবে

একটি commit দলের shared main-এ push হয়ে গেছে, আর কয়েকজন সহকর্মী তা pull করেছেন। এখন দেখা গেল commit-টিতে bug. সবচেয়ে নিরাপদ পথ কোনটি?

  • git reset --hard HEAD~1 তারপর git push --force
  • git commit --amend দিয়ে commit ঠিক করে push
  • git revert <commit> দিয়ে নতুন commit, তারপর সাধারণ push
  • git clean -fd

cart.js-এ কিছু পরিবর্তন করেছেন, কখনো git add বা commit করেননি। এরপর git restore cart.js চালালেন। সেই পরিবর্তন কি ফেরানো যাবে?

  • না — Git সেটি কখনো সংরক্ষণ করেনি, তাই কোনো Git command দিয়ে ফেরে না
  • হ্যাঁ — git reflog দিয়ে
  • হ্যাঁ — git reset --hard ORIG_HEAD দিয়ে
  • হ্যাঁ — git revert HEAD দিয়ে

git revert HEAD চালালে Git বলল: error: commit 075c171… is a merge but no -m option was given. এর অর্থ আর সমাধান কী?

  • Merge commit কখনো revert করা যায় না, reset ছাড়া উপায় নেই
  • --no-edit যোগ করলেই চলবে
  • Conflict হয়েছে, ফাইল মিটিয়ে git revert --continue
  • এটি merge commit, তাই মূল ধারার parent বলে দিতে হবে — যেমন git revert -m 1 HEAD

শেষ commit 97611f9 (push হয়নি)-এ একটি ফাইল যোগ করে git commit --amend চালালেন, নতুন ID এলো d09aad4. 97611f9-এর কী হলো?

  • একই commit-টিই সম্পাদিত হয়ে নতুন নাম পেয়েছে
  • এটি object store-এ থেকে গেছে, কোনো branch তার দিকে নির্দেশ করে না, reflog-এ পাওয়া যায়
  • d09aad4-এর parent হয়ে history-তে আছে
  • সঙ্গে সঙ্গে স্থায়ীভাবে মুছে গেছে

Working tree-তে untracked debug.log ফাইল আর untracked tmp/ ফোল্ডার আছে। কোন command চালালে কিছু না মুছে দেখা যাবে যে দুটিই মোছা হবে?

  • git clean -n
  • git clean -fd
  • git clean -nd
  • git reset --hard

নিচের কোন বক্তব্যটি reflog সম্পর্কে সঠিক?

  • এটি local — push বা clone হয় না, আর entry একটি সময়সীমার পর মুছে যেতে পারে
  • এটি working tree-র প্রতিটি অসংরক্ষিত পরিবর্তনও লিখে রাখে
  • এটি server-এ রাখা থাকে, তাই সহকর্মীর হারানো commit-ও আপনার reflog-এ পাওয়া যায়
  • HEAD@{3} সবসময় মানে “তিন commit আগে”

শব্দকোষ

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

মনে রাখুন

  1. আগে প্রশ্ন: ভুলটি কোথায় (working tree, index, শেষ commit, পুরোনো commit) আর অন্যরা কি পেয়েছেন.
  2. git restore f ফাইল index থেকে ফেরায় — অসংরক্ষিত কাজ চিরতরে যায়; --staged শুধু unstage করে।
  3. reset: soft = branch, mixed = branch + index (ডিফল্ট), hard = তিনটিই; কোনোটিই commit মোছে না।
  4. Push-করা commit-এ revert, amend বা reset নয়; merge উল্টাতে -m 1.
  5. হারালে git reflog → hash → git branch rescue; কিন্তু add-না-করা কাজ আর clean-এ মোছা ফাইল ফেরে না।

পরের পর্বে: এই পর্বে বারবার “branch একটি pointer” বলেছি। পর্ব ১২-এ দেখবেন branch আসলে কী, HEAD কীভাবে তাকে অনুসরণ করে, git switch দিয়ে branch বদলানো, আর detached HEAD অবস্থায় commit করলে কী ঘটে।

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

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