এই পর্বে যা শিখবেন
- Rebase ভেতরে কী করে — merge base খোঁজা, commit replay, নতুন ID, আর পুরোনো commit-এর কী হয়
- Rebase-এর সময় conflict সমাধান,
--continue/--skip/--abort, আর conflict কেন বারবার আসে git pull --rebase, তিন-argument-এর--ontoআর stacked branch-এর জন্য--update-refs- Interactive rebase-এর প্রতিটি command,
--fixup/--autosquash, আর ভুল rebase থেকে উদ্ধার - Merge বনাম rebase-এর বিনিময়, সোনালি নিয়ম,
--force-with-leaseআর দলের নীতি
১ · Rebase কী — commit নতুন ভিতের ওপর আবার বসানো
পর্ব ১৩-এ (Merge আর conflict) দেখেছেন, দুই branch আলাদা পথে গেলে git merge একটি merge commit বানিয়ে দুই ধারা জোড়া দেয়। দুই ধারার ইতিহাস যেমন ছিল তেমনই থাকে। কিন্তু কাজের ধারা এক করার আরেকটি উপায় আছে: আপনার branch-এর commit-গুলো তুলে নিয়ে অন্য branch-এর সর্বশেষ commit-এর ওপর আবার বসানো, যেন আপনি কাজটি আজই সেখান থেকে শুরু করেছিলেন। এর নাম rebase — “base” বা ভিত বদলে দেওয়া।
রান্নাঘরের রেসিপি খাতার কথা ভাবুন। মায়ের খাতার “খিচুড়ি” পাতাটি আপনি কপি করে নিলেন, তারপর কপিতে দুটি বদল লিখলেন — “মুগ ডাল একটু বেশি” আর “শেষে ঘি”. এর মধ্যে মা নিজের খাতায় মূল রেসিপি হালনাগাদ করলেন: “চাল আধা ঘণ্টা ভিজিয়ে রাখুন”. এখন দুই উপায়। Merge: দুটি পাতা পাশাপাশি রেখে একটি তৃতীয় পাতায় লিখলেন “দুটো মিলিয়ে” — ইতিহাস স্পষ্ট, তবে পাতা বাড়ল। Rebase: মায়ের হালনাগাদ পাতার একটি নতুন কপি নিলেন, তারপর নিজের দুটি বদল একটি একটি করে আবার লিখলেন। ফল একটি সোজা গল্প, যেন আপনি শুরু থেকেই নতুন রেসিপি দেখে কাজ করছিলেন। কিন্তু খেয়াল করুন — আপনার বদলগুলো এখন নতুন পাতায়; পুরোনো কপিটি ড্রয়ারে পড়ে আছে, যতদিন না কেউ ফেলে দেন।
এই উপমার শেষ লাইনটিই rebase বোঝার চাবি। পর্ব ৮ (প্রথম Git repository) থেকে জানেন, একটি commit-এর ID (commit hash) তৈরি হয় commit object-এর পুরো বিষয়বস্তু থেকে: snapshot-এর tree, parent-এর ID, author, committer, সময় আর বার্তা। Rebase একটি commit-এর পরিবর্তন (diff) একই রাখে, কিন্তু তাকে নতুন parent-এর ওপর বসায়। Parent বদলালে hash-এর ইনপুট বদলায়, তাই ID-ও বদলায়। অর্থাৎ rebase কোনো commit “সরায়” না — একই পরিবর্তন দিয়ে নতুন commit বানায়, তারপর branch-এর নামটি নতুন commit-এর দিকে সরিয়ে দেয়।
Git-এ commit অপরিবর্তনীয় (immutable): একবার লেখা object আর বদলায় না। তাই “history নতুন করে লেখা” মানে আসলে নতুন commit তৈরি করে branch-এর pointer সেদিকে সরানো। পুরোনো commit object-এ থেকে যায়, শুধু কোনো branch থেকে আর reachable থাকে না। Reflog আর ORIG_HEAD সেগুলোর ঠিকানা মনে রাখে; অনেকদিন কেউ না ছুঁলে garbage collection সেগুলো মুছে ফেলে।
Rebase-এর আগে আর পরে
The same two changes are replayed on main; the old commits become unreachable "ghosts"
নিচের প্রোগ্রামে একটি খুদে commit DAG বানিয়ে rebase করা হয়েছে। Git-এর আসল hash SHA-1 (নতুন repository-তে ঐচ্ছিকভাবে SHA-256), আর সেটি পুরো commit object থেকে হিসাব হয়। এখানে সরল রাখতে FNV-1a নামের ছোট একটি hash function শুধু “parent-এর ID + পরিবর্তনের বর্ণনা” থেকে ID বানাচ্ছে — ধারণাটি একই: parent বদলালে ID বদলায়।
// Rebase = replay commits on a new base, in a tiny commit DAG. fnv1a stands in for
// Git's SHA-1, which hashes the whole commit object; here only parent + change.
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 = {}, refs = {}; // id -> {parent, change}
function commit(parent, change) {
const id = fnv1a((parent || "root") + "|" + change);
commits[id] = { parent, change };
return id;
}
function history(id) { const h = []; for (; id; id = commits[id].parent) h.push(id); return h; }
const A = commit(null, "add portal.conf"), B = commit(A, "add README");
refs.main = commit(B, "README: add contact"); // E on main
refs.feature = commit(commit(B, "add gpa.txt"), "show GPA"); // C, D on feature
console.log("before: feature =", history(refs.feature).join(" <- "));
function rebase(branch, onto) {
if (history(refs[branch]).includes(refs[onto])) return console.log(" " + branch + " is up to date");
const onBase = new Set(history(refs[onto])), todo = [];
for (let id = refs[branch]; !onBase.has(id); id = commits[id].parent) todo.unshift(id);
let tip = refs[onto];
for (const old of todo) {
tip = commit(tip, commits[old].change); // same change, new parent
console.log(" replay " + old + " -> " + tip + " (" + commits[old].change + ")");
}
refs.ORIG_HEAD = refs[branch]; refs[branch] = tip;
}
rebase("feature", "main");
console.log("after: feature =", history(refs.feature).join(" <- "));
const reach = new Set([...history(refs.main), ...history(refs.feature)]);
const ghosts = Object.keys(commits).filter(id => !reach.has(id));
console.log("unreachable but still stored:", ghosts.join(", "), "| ORIG_HEAD =", refs.ORIG_HEAD);
rebase("feature", "main"); // main is already an ancestor
আউটপুট:
before: feature = b54cb0f <- ca01d08 <- 53e5c41 <- 42acbd0
replay ca01d08 -> 1f91b66 (add gpa.txt)
replay b54cb0f -> a5d4df3 (show GPA)
after: feature = a5d4df3 <- 1f91b66 <- 55a5392 <- 53e5c41 <- 42acbd0
unreachable but still stored: ca01d08, b54cb0f | ORIG_HEAD = b54cb0f
feature is up to date
শুরুতে feature-এর ইতিহাস চারটি commit: দুটি নিজের (b54cb0f, ca01d08) আর দুটি main-এর সাথে ভাগ করা (53e5c41 = B, 42acbd0 = A). rebase() প্রথমে বের করে কোন commit-গুলো main-এর ইতিহাসে নেই — এই দুটি — তারপর পুরোনো থেকে নতুন ক্রমে replay করে। “add gpa.txt” পরিবর্তনটি একই, কিন্তু parent এখন main-এর মাথা 55a5392, তাই ID হলো 1f91b66. শেষে feature নতুন tip a5d4df3-এ, ইতিহাস এখন এক সরলরেখা। পুরোনো দুটি commit কোনো branch থেকে পৌঁছানো যায় না, কিন্তু commits object-এ রয়ে গেছে, আর ORIG_HEAD পুরোনো tip মনে রেখেছে। দ্বিতীয়বার rebase চাইলে main ইতিমধ্যে feature-এর ancestor, তাই কিছু করার নেই — আসল Git-ও তখন বলে branch “up to date”.
২ · git rebase main — ধাপে ধাপে
এবার আসল Git. এই পর্বের সব Git আউটপুট একটি খালি পরীক্ষার folder-এ Git ২.৫৩ দিয়ে চালানো। Commit-এর hash যেন আপনার কাছেও একই আসে, তাই লেখকের নাম “Rahim”, email rahim@example.com, আর প্রতিটি commit-এর সময় GIT_AUTHOR_DATE ও GIT_COMMITTER_DATE দিয়ে ২০২৬-০১-১৫ সকাল ১০:০১ (+06:00) থেকে এক মিনিট করে বাড়িয়ে ঠিক করা — rebase চালানোর সময়ও। গল্পটি একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের: main-এ আছে portal.conf (“Add portal config”, 9316721) আর README (“Add README”, 8e9e4e7). একজন developer GPA দেখানোর কাজ শুরু করলেন, আর এর মধ্যে main-এ README-তে যোগাযোগের ঠিকানা যোগ হলো:
$ git switch -c feature/gpa
Switched to a new branch 'feature/gpa'
# (create gpa.txt; git add gpa.txt)
$ git commit -m "Add GPA calculator"
[feature/gpa 2e9e344] Add GPA calculator
1 file changed, 1 insertion(+)
create mode 100644 gpa.txt
# (append a line to gpa.txt; git add gpa.txt)
$ git commit -m "Show GPA on result page"
[feature/gpa 3af94ac] Show GPA on result page
1 file changed, 1 insertion(+)
$ git switch main
Switched to branch 'main'
# (append the contact line to README.md; git add README.md)
$ git commit -m "Add contact to README"
[main 2313ddc] Add contact to README
1 file changed, 1 insertion(+)
$ git log --oneline --graph --all
* 2313ddc (HEAD -> main) Add contact to README
| * 3af94ac (feature/gpa) Show GPA on result page
| * 2e9e344 Add GPA calculator
|/
* 8e9e4e7 Add README
* 9316721 Add portal config
$ git switch feature/gpa
Switched to branch 'feature/gpa'
$ git merge-base main feature/gpa
8e9e4e7f6bce7ab2bb6af6e0b877c8d38156ad2d
$ git rebase main
Successfully rebased and updated refs/heads/feature/gpa.
$ git log --oneline --graph --all
* 2d52732 (HEAD -> feature/gpa) Show GPA on result page
* 0324b2d Add GPA calculator
* 2313ddc (main) Add contact to README
* 8e9e4e7 Add README
* 9316721 Add portal configপ্রথম log-এ দুই পথ আলাদা: feature/gpa-তে 2e9e344 আর 3af94ac, main-এ 2313ddc; দুটির merge base 8e9e4e7. feature/gpa-তে দাঁড়িয়ে git rebase main দিতেই Git জানাল Successfully rebased and updated refs/heads/feature/gpa. (চলার সময় একই লাইনে Rebasing (1/2), Rebasing (2/2) অগ্রগতি দেখায়, পরে মুছে দেয়; এখানে চূড়ান্ত পর্দাটি দেখানো হয়েছে।) এখন log এক সরলরেখা: “Add GPA calculator” আর “Show GPA on result page” বসেছে 2313ddc-এর ওপর, কিন্তু নতুন ID নিয়ে — 0324b2d আর 2d52732. main নিজে একটুও সরেনি; rebase শুধু বর্তমান branch-কে বদলায়।
ভেতরে ঠিক কী ঘটল, নিচের stepper-এ এক ধাপ এক ধাপ করে দেখুন। বৃত্তের অক্ষর commit-এর নাম (A = 9316721, B = 8e9e4e7, E = 2313ddc, C = 2e9e344, D = 3af94ac):
| HEAD | |
| feature | |
| বাকি কাজ | |
| নতুন commit |
- শুরু। HEAD আছে
feature-এ, যার মাথা D.mainআছে E-তে। আপনিgit rebase mainদিলেন — অর্থাৎ “আমার branch-কেmain-এর মাথার ওপর নতুন করে বসানো হোক”. Working tree-তে commit না করা বদল থাকলে Git এখানেই থেমে যায়। - কী replay হবে, তা ঠিক করা। Git
mainআরfeature-এর merge base খোঁজে — B. B-র পরেfeature-এ যে commit-গুলো আছে কিন্তুmain-এ নেই, সেগুলোই কাজের তালিকা: C, তারপর D (পুরোনো থেকে নতুন)। যে commit-এর হুবহু একই পরিবর্তনmain-এ আগেই আছে, Git সেটি তালিকা থেকে বাদ দেয়। - নতুন ভিতে দাঁড়ানো। Git HEAD-কে সরাসরি E-তে বসায় — কোনো branch নয়, তাই এটি detached HEAD (পর্ব ১২)। Reflog-এ এই মুহূর্তটি লেখা থাকে
rebase (start): checkout main. - C replay. C-র পরিবর্তন (gpa.txt তৈরি) E-র ওপর প্রয়োগ করে নতুন commit C′ =
0324b2d. Author, author date আর বার্তা C-র মতোই; parent এখন E, committer date নতুন — তাই ID ভিন্ন। HEAD সরে C′-এ। - D replay. একইভাবে D-র পরিবর্তন C′-এর ওপর → D′ =
2d52732. কাজের তালিকা শেষ। এখানে conflict হলে Git থামত আর আপনার সিদ্ধান্ত চাইত (অংশ ৩)। - Branch সরানো। শেষে
featureনামটি D′-এ সরানো হয় আর HEAD আবারfeature-এ ফিরে আসে (rebase (finish)). পুরোনো D-কে GitORIG_HEADনামে মনে রাখে। C আর D কোনো branch থেকে পৌঁছানো যায় না — ছায়া হয়ে object store-এ থাকে।
ছায়াগুলো সত্যিই আছে কি না, দেখা যাক। সাধারণ git log --all শুধু branch আর tag থেকে পৌঁছানো commit দেখায়, তাই উপরের log-এ পুরোনো দুটি ছিল না। কিন্তু ORIG_HEAD-কেও log-এ দিলে:
$ git log --oneline --graph main feature/gpa ORIG_HEAD
* 2d52732 (HEAD -> feature/gpa) Show GPA on result page
* 0324b2d Add GPA calculator
* 2313ddc (main) Add contact to README
| * 3af94ac Show GPA on result page
| * 2e9e344 Add GPA calculator
|/
* 8e9e4e7 Add README
* 9316721 Add portal config
$ git cat-file -t 3af94ac
commit
$ git reflog -6
2d52732 (HEAD -> feature/gpa) HEAD@{0}: rebase (finish): returning to refs/heads/feature/gpa
2d52732 (HEAD -> feature/gpa) HEAD@{1}: rebase (pick): Show GPA on result page
0324b2d HEAD@{2}: rebase (pick): Add GPA calculator
2313ddc (main) HEAD@{3}: rebase (start): checkout main
3af94ac HEAD@{4}: checkout: moving from main to feature/gpa
2313ddc (main) HEAD@{5}: commit: Add contact to README
$ git range-diff 8e9e4e7..3af94ac main..feature/gpa
1: 2e9e344 = 1: 0324b2d Add GPA calculator
2: 3af94ac = 2: 2d52732 Show GPA on result page
$ git log --format="%h %an %ad | %cd %s" --date=format:%H:%M -3
2d52732 Rahim 10:04 | 10:06 Show GPA on result page
0324b2d Rahim 10:03 | 10:06 Add GPA calculator
2313ddc Rahim 10:05 | 10:05 Add contact to READMEGraph: পুরোনো 3af94ac আর 2e9e344 এখনো 8e9e4e7 থেকে বেরোনো একটি আলাদা রেখা — ঠিক ছবি ১৫.১-এর ছায়া। git cat-file -t নিশ্চিত করল object-টি একটি commit. Reflog (পর্ব ১১ — ভুল শোধরানো) পুরো ঘটনা নিচ থেকে ওপরে বলে: checkout করে feature/gpa-তে আসা, rebase (start)-এ main-এর মাথায় যাওয়া, দুটি rebase (pick), আর rebase (finish). git range-diff পুরোনো আর নতুন commit-সারি জোড়ায় জোড়ায় মেলায়: = চিহ্ন মানে পরিবর্তন হুবহু এক, শুধু ID আলাদা। শেষ লাইনগুলো: author date (10:03, 10:04) মূল commit-এর সময়ই আছে, কিন্তু committer date হয়েছে rebase-এর সময় 10:06 — যিনি rebase করেন, তিনিই নতুন commit-এর committer.
যা জেনে রাখা ভালো
- পূর্ণ রূপ:
git rebase <upstream> [<branch>]. Branch দিলে Git আগে সেটিতে switch করে নেয়। Upstream না দিলে বর্তমান branch-এর upstream branch (যেমনorigin/main; পর্ব ১৪ — Remote repository) ধরা হয়। - Merge commit বাদ পড়ে: সাধারণ rebase branch-এর ভেতরের merge commit-গুলো রাখে না, তাদের দুই দিকের commit এক রেখায় সাজায়। ভেতরের গঠন রাখতে
--rebase-mergesoption আছে (Git ২.১৮-এ যোগ হয়েছে বলে আমাদের জানা; পুরোনো--preserve-mergesপরে সরিয়ে দেওয়া হয়েছে)। - অসমাপ্ত বদল: Working tree পরিষ্কার না থাকলে rebase শুরু হয় না।
git stash(পর্ব ৫৬) করে নিন, বা--autostashদিন — Git নিজে stash করে শেষে ফিরিয়ে দেবে।
৩ · Rebase-এর সময় conflict — --continue, --skip, --abort
Replay মানে প্রতিটি commit-এর জন্য একটি ছোট three-way merge (পর্ব ১৩): base হলো commit-টির পুরোনো parent, “ours” হলো এখন পর্যন্ত তৈরি নতুন ইতিহাসের মাথা, আর “theirs” হলো যে commit replay হচ্ছে। দুই পক্ষ একই লাইন ভিন্নভাবে বদলালে conflict. ফলাফল পোর্টালের নতুন গল্প: একটি branch-এ পাস নম্বর দুই ধাপে বাড়ানো হয়েছে, মাঝে একটি আলাদা option যোগ হয়েছে; অন্যদিকে main-এ একই লাইনে একটি মন্তব্য বসেছে — “এটি বোর্ডের নিয়ম”.
$ git log --oneline --graph --all
* 92a039f (main) Mark pass mark as board rule
| * df28b53 (HEAD -> feature/pass-mark) Set pass mark to 50
| * 9c0a8f6 Add grace marks option
| * d080157 Set pass mark to 45
|/
* 8e9e4e7 Add README
* 9316721 Add portal config
$ git rebase main
Auto-merging portal.conf
CONFLICT (content): Merge conflict in portal.conf
error: could not apply d080157... Set pass mark to 45
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply d080157... # Set pass mark to 45
$ git status
interactive rebase in progress; onto 92a039f
Last command done (1 command done):
pick d080157 # Set pass mark to 45
Next commands to do (2 remaining commands):
pick 9c0a8f6 # Add grace marks option
pick df28b53 # Set pass mark to 50
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'feature/pass-mark' on '92a039f'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: portal.conf
no changes added to commit (use "git add" and/or "git commit -a")
$ cat portal.conf
title = Result Portal
<<<<<<< HEAD
pass_mark = 40 # board rule
=======
pass_mark = 45
>>>>>>> d080157 (Set pass mark to 45)
grade_scale = 5
show_rank = noGit প্রথম commit d080157 replay করতে গিয়েই থামল: CONFLICT (content): Merge conflict in portal.conf আর could not apply d080157... Set pass mark to 45. hint লাইনগুলো তিনটি পথ বলে দিচ্ছে — সমাধান করে git add ও --continue, এই commit বাদ দিতে --skip, অথবা সব বাতিল করতে --abort. git status লেখে interactive rebase in progress — আপনি -i না দিলেও, কারণ Git ২.২৬ থেকে সাধারণ rebase-ও ভেতরে interactive rebase-এর একই যন্ত্র (sequencer) দিয়ে চলে। সেখানে কাজের তালিকাও দেখা যাচ্ছে: ১টি শেষ, ২টি বাকি। শেষ লাইনের #-সহ রূপটি (d080157... # Set…) সাম্প্রতিক সংস্করণের; পুরোনো Git-এ বার্তা একটু ভিন্ন দেখাবে।
সবচেয়ে বড় ফাঁদ — ours আর theirs উল্টে যায়। Conflict marker-এ <<<<<<< HEAD-এর নিচে আছে main-এর লাইন (40 # board rule), আর >>>>>>> d080157-এর ওপরে আপনার নিজের commit-এর লাইন। কারণ rebase চলার সময় HEAD দাঁড়িয়ে আছে নতুন base-এর ওপর, আর আপনার commit-টি “বাইরে থেকে আসা” পরিবর্তন। তাই rebase-এর মাঝে git checkout --ours মানে main-এর সংস্করণ, --theirs মানে আপনার — merge-এর ঠিক উল্টো।
ভয় পেলে বা ভুল branch-এ rebase শুরু করলে এক command-এ সব ফেরানো যায়:
$ git rebase --abort
$ git status --short --branch
## feature/pass-mark
$ git log --oneline -1
df28b53 (HEAD -> feature/pass-mark) Set pass mark to 50git rebase --abort কিছু ছাপায় না, কিন্তু branch-কে ঠিক rebase-এর আগের commit-এ (df28b53) ফিরিয়ে দেয় আর working tree পরিষ্কার করে। আসল ইতিহাসে কোনো দাগ পড়ে না। এবার আবার শুরু করে conflict দুটি সমাধান করা যাক:
$ git rebase main
… (the same conflict in portal.conf as above)
# (edit portal.conf: pass_mark = 45 # board rule; remove the markers)
$ git add portal.conf
# (Git opens the editor with the message "Set pass mark to 45"; save and close)
$ git rebase --continue
[detached HEAD 18cf9e6] Set pass mark to 45
1 file changed, 1 insertion(+), 1 deletion(-)
Auto-merging portal.conf
CONFLICT (content): Merge conflict in portal.conf
error: could not apply df28b53... Set pass mark to 50
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply df28b53... # Set pass mark to 50
$ cat portal.conf
title = Result Portal
<<<<<<< HEAD
pass_mark = 45 # board rule
=======
pass_mark = 50
>>>>>>> df28b53 (Set pass mark to 50)
grade_scale = 5
show_rank = no
grace_marks = 2
# (edit again: pass_mark = 50 # board rule; remove the markers)
$ git add portal.conf
$ git rebase --continue
[detached HEAD 525683c] Set pass mark to 50
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/feature/pass-mark.
$ git log --oneline --graph --all
* 525683c (HEAD -> feature/pass-mark) Set pass mark to 50
* a100128 Add grace marks option
* 18cf9e6 Set pass mark to 45
* 92a039f (main) Mark pass mark as board rule
* 8e9e4e7 Add README
* 9316721 Add portal config
$ cat portal.conf
title = Result Portal
pass_mark = 50 # board rule
grade_scale = 5
show_rank = no
grace_marks = 2প্রথম conflict-এ দুই পক্ষের উদ্দেশ্যই রাখা হলো — নতুন মান ৪৫ আর বোর্ডের মন্তব্য: pass_mark = 45 # board rule. git add দিয়ে “সমাধান হয়েছে” জানানো হলো, তারপর --continue. Git commit বার্তা সম্পাদনার সুযোগ দেয় (editor খোলে), তারপর তৈরি হলো [detached HEAD 18cf9e6] Set pass mark to 45. দ্বিতীয় commit “Add grace marks option” অন্য লাইনে, তাই নিঃশব্দে replay হলো। কিন্তু তৃতীয় commit আবার আটকাল! কারণ সেটি তৈরি হয়েছিল পুরোনো লাইন pass_mark = 45-এর ওপর ভিত্তি করে (base), অথচ এখন লাইনটি pass_mark = 45 # board rule (ours). একই লাইন, দ্বিতীয়বার সমাধান। শেষে Successfully rebased, ইতিহাস সরলরেখা, ফাইলে দুই পক্ষের সব উদ্দেশ্য আছে।
| Command | কখন | কী হয় |
|---|---|---|
git add + git rebase --continue | Conflict সমাধান করেছেন | বর্তমান commit তৈরি হয়, তালিকার পরেরটিতে যায় |
git rebase --skip | এই commit-এর আর দরকার নেই (যেমন একই পরিবর্তন main-এ আগেই এসেছে) | বর্তমান commit বাদ — নতুন ইতিহাসে থাকবে না |
git rebase --abort | যেকোনো সময়, সব বাতিল করতে | Branch আর working tree rebase-এর আগের অবস্থায় |
git rebase --quit | বিরল; শুধু rebase-এর অবস্থা ভুলে যেতে | HEAD যেখানে আছে সেখানেই থাকে, branch সরে না |
Conflict কেন commit-প্রতি — merge-এর সাথে হিসাব
Merge তিনটি snapshot একবার মেলায়: base, দুই tip. Rebase প্রতিটি commit আলাদা করে মেলায়। তাই একই লাইন যদি আপনার branch-এর পাঁচটি commit-এ বদলে থাকে, আর main-ও লাইনটি বদলে থাকে, rebase-এ সেই লাইনে পাঁচবার conflict আসতে পারে; merge-এ একবার। নিচের প্রোগ্রাম এক লাইনের ফাইলে হিসাবটি করে দেখায়:
// Rebase replays commits one by one, so one overlapping line can conflict again
// and again. A merge compares only the two final tips with their merge base.
function merge3(base, ours, theirs) { // one-line three-way merge
if (ours === theirs || theirs === base) return { line: ours };
if (ours === base) return { line: theirs };
return { conflict: true };
}
const resolve = theirs => theirs + " # board rule"; // keep new value AND main's note
const base = "pass_mark = 40";
const mainTip = "pass_mark = 40 # board rule";
const feature = ["pass_mark = 45", "pass_mark = 50", "pass_mark = 55"]; // 3 commits
function rebaseCount(commits) {
let tip = mainTip, parent = base, n = 0;
for (const line of commits) {
let r = merge3(parent, tip, line); // base = the commit's old parent
const hit = !!r.conflict;
if (hit) { n++; r = { line: resolve(line) }; }
console.log(" replay '" + line + "' ->", hit ? "resolution #" + n : "clean", "->", r.line);
tip = r.line; parent = line;
}
return n;
}
console.log("rebase, 3 commits:"); const a = rebaseCount(feature);
const m = merge3(base, mainTip, feature[2]);
const b = m.conflict ? 1 : 0;
console.log("merge once: resolutions", b, "->", m.conflict ? resolve(feature[2]) : m.line);
console.log("squash first, then rebase:"); const c = rebaseCount([feature[2]]);
console.log("total resolutions -> rebase:", a, "| merge:", b, "| squash+rebase:", c);
আউটপুট:
rebase, 3 commits:
replay 'pass_mark = 45' -> resolution #1 -> pass_mark = 45 # board rule
replay 'pass_mark = 50' -> resolution #2 -> pass_mark = 50 # board rule
replay 'pass_mark = 55' -> resolution #3 -> pass_mark = 55 # board rule
merge once: resolutions 1 -> pass_mark = 55 # board rule
squash first, then rebase:
replay 'pass_mark = 55' -> resolution #1 -> pass_mark = 55 # board rule
total resolutions -> rebase: 3 | merge: 1 | squash+rebase: 1
প্রতিটি replay-তে base হলো commit-টির পুরোনো parent-এর লাইন (৪০, তারপর ৪৫, তারপর ৫০), আর ours হলো সমাধান করা নতুন লাইন (মন্তব্যসহ)। দুটি কখনো মেলে না, তাই তিনটি commit-এই conflict — মোট ৩টি সমাধান। Merge শুধু base (৪০), main (৪০ + মন্তব্য) আর feature-এর শেষ অবস্থা (৫৫) দেখে: ১টি সমাধান, ফলও একই। আগে নিজের তিনটি commit squash করে একটি বানালে (অংশ ৬) rebase-এও ১টি। বাস্তবে উপায় আরও আছে: Git-এর rerere (“reuse recorded resolution”) একবারের সমাধান মনে রেখে একই conflict আবার এলে নিজে বসিয়ে দেয় — বিস্তারিত পর্ব ৫৪-এ।
৪ · git pull --rebase — remote-এর কাজের ওপর নিজের commit বসানো
পর্ব ১৪-এ (Remote repository) দেখেছেন, pull আসলে দুই কাজ: fetch (remote থেকে নতুন commit এনে remote-tracking branch যেমন origin/main হালনাগাদ করা) আর তারপর মেলানো। মেলানোর চিরাচরিত পদ্ধতি merge — তাই দুজন একই branch-এ কাজ করলে প্রতিটি pull-এ ছোট ছোট “Merge branch 'main' of …” commit জমে। git pull --rebase দ্বিতীয় ধাপে merge-এর বদলে rebase করে: আপনার এখনো push না করা commit-গুলো remote-এর নতুন কাজের ওপর বসে।
উদাহরণে একটি local bare repository remote হিসেবে কাজ করছে (../origin.git). Rahim আর Karim দুজনেই main-এ কাজ করেন; Karim আগে push করেছেন:
# (meanwhile Karim pushed "Show rank on result page" to origin/main)
# (create help.txt; git add help.txt)
$ git commit -m "Add help page"
[main 5c8086f] Add help page
1 file changed, 1 insertion(+)
create mode 100644 help.txt
$ git push
To ../origin.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '../origin.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git pull --rebase
From ../origin
8e9e4e7..8754dd6 main -> origin/main
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph --all
* 6fb4576 (HEAD -> main) Add help page
* 8754dd6 (origin/main, origin/HEAD) Show rank on result page
* 8e9e4e7 Add README
* 9316721 Add portal config
$ git push
To ../origin.git
8754dd6..6fb4576 main -> mainপ্রথম git push প্রত্যাখ্যাত: ! [rejected] main -> main (fetch first) — remote-এ এমন commit আছে যা Rahim-এর কাছে নেই। git pull --rebase প্রথমে fetch করল (8e9e4e7..8754dd6 main -> origin/main), তারপর Rahim-এর একমাত্র local commit “Add help page”-কে origin/main-এর ওপর replay করল। ID বদলে 5c8086f থেকে 6fb4576, কিন্তু এই commit তখনো কেউ দেখেননি — তাই নিরাপদ। ইতিহাস সোজা, কোনো merge commit নেই, আর এবার git push একটি সাধারণ fast-forward (8754dd6..6fb4576). (log-এ origin/HEAD হলো remote-এর ডিফল্ট branch-এর ছাপ; এখানে প্রাসঙ্গিক নয়।)
প্রতিবার --rebase লিখতে না চাইলে git config pull.rebase true দিয়ে এটিকে ডিফল্ট করা যায়; আর শুধু fast-forward হলে pull হোক, নইলে থামুক — চাইলে pull.ff only. সাম্প্রতিক Git-এ কোনো নীতি ঠিক না থাকলে আর দুই পক্ষ আলাদা হয়ে গেলে pull নিজে থেকে কিছু না করে আপনাকে বেছে নিতে বলে (বার্তা সংস্করণভেদে আলাদা)। Pull-এর সময় conflict হলে সেটি সাধারণ rebase conflict — অংশ ৩-এর মতোই সমাধান।
৫ · --onto আর --update-refs — branch এক ভিত থেকে আরেক ভিতে
ধরুন একজন developer দুটি কাজ পরপর করছেন: feature/api (ফলাফলের API), আর তার ওপর ভিত্তি করে feature/ui (ফলাফলের টেবিল)। এখন সিদ্ধান্ত হলো, UI-এর কাজটি API ছাড়াই আগে যাবে। git rebase main দিলে UI-এর সাথে API-এর দুটি commit-ও main-এ চলে যেত, কারণ সেগুলোও main-এ নেই। দরকার এমন rebase, যা শুধু feature/api-র পরের commit-গুলো নিয়ে main-এর ওপর বসায়। এটিই rebase --onto-এর ক্লাসিক তিন-argument রূপ:
“পুরোনো-base..branch” পরিসরের commit → নতুন-base-এর ওপর
$ git log --oneline --graph --all
* d481af7 (main) Add contact to README
| * 445469e (HEAD -> feature/ui) Add print button
| * 9d34915 Add result table UI
| * aae7a7c (feature/api) Add result by roll API
| * b3e3d6b Add results API
|/
* 8e9e4e7 Add README
* 9316721 Add portal config
$ git rebase --onto main feature/api feature/ui
Successfully rebased and updated refs/heads/feature/ui.
$ git log --oneline --graph --all
* ba9e682 (HEAD -> feature/ui) Add print button
* 648bb82 Add result table UI
* d481af7 (main) Add contact to README
| * aae7a7c (feature/api) Add result by roll API
| * b3e3d6b Add results API
|/
* 8e9e4e7 Add README
* 9316721 Add portal configআগে UI-এর দুটি commit (9d34915, 445469e) বসে ছিল API-এর দুটির ওপর। git rebase --onto main feature/api feature/ui পরিসর feature/api..feature/ui নিল — অর্থাৎ শুধু UI-এর দুটি — আর সেগুলো main-এর মাথা d481af7-এর ওপর বসাল: নতুন ID 648bb82 আর ba9e682. feature/api যেখানে ছিল সেখানেই আছে, এখনো পুরোনো base 8e9e4e7-এর ওপর। একই কৌশলে ভুল branch থেকে খোলা কাজ সঠিক branch-এ সরানো যায়, বা git rebase --onto HEAD~3 HEAD~2 দিয়ে মাঝের একটি commit ফেলে দেওয়া যায় (পরিসর HEAD~2..HEAD বসে HEAD~3-এর ওপর, ফলে HEAD~2-র commit বাদ পড়ে)।
Stacked branch আর --update-refs
উল্টো পরিস্থিতিও সাধারণ: দুটি branch একটির ওপর আরেকটি সাজানো (stacked branches) — দুটিই আলাদা pull request, আর main এগিয়ে গেছে। feature/ui-তে দাঁড়িয়ে git rebase main দিলে চারটি commit-ই নতুন ID পায়, কিন্তু feature/api নামটি পুরোনো commit-এই পড়ে থাকে; তাকে আলাদা করে ঠিক করতে হয়। Git ২.৩৮ (২০২২) থেকে --update-refs option এই কাজটি নিজে করে — আমাদের জানা মতে এর আগের সংস্করণে এটি নেই, তাই git --version দেখে নিন:
# (same starting graph as above, HEAD on feature/ui)
$ git rebase --update-refs main
Successfully rebased and updated refs/heads/feature/ui.
Updated the following refs with --update-refs:
refs/heads/feature/api
$ git log --oneline --graph --all
* 4b38fef (HEAD -> feature/ui) Add print button
* 3cabd0a Add result table UI
* 9a4854f (feature/api) Add result by roll API
* f815977 Add results API
* d481af7 (main) Add contact to README
* 8e9e4e7 Add README
* 9316721 Add portal configএকটি command-এ পুরো stack main-এর মাথা d481af7-এর ওপর উঠল, আর Git জানাল Updated the following refs with --update-refs: refs/heads/feature/api — মাঝের branch-ও নতুন commit 9a4854f-এ সরেছে। Interactive rebase-এ এটি todo list-এ update-ref refs/heads/feature/api লাইন হিসেবে দেখা যায়। সবসময় চালু রাখতে git config rebase.updateRefs true. অন্য worktree-তে checkout করা branch-কে Git এভাবে সরায় না।
৬ · Interactive rebase — todo list দিয়ে history গোছানো
নিজের branch-এ কাজ করতে করতে commit-গুলো প্রায়ই এলোমেলো হয়: “Fix typo”, “Debug print”, বানান-ভুল বার্তা, একই কাজের দুই টুকরো। Review-তে পাঠানোর আগে এগুলো গোছানো ভালো অভ্যাস (পর্ব ৮ আর ১৮-এ ভালো commit-এর নিয়ম)। Interactive rebase — git rebase -i — Git-কে বলে: replay করার আগে কাজের তালিকাটি আমাকে দেখানো হোক, আমি সম্পাদনা করব। পরীক্ষার খাতার খসড়া থেকে ফেয়ার কপি করার মতো: ভুল কাটা অংশ বাদ, ছোট টুকরো জোড়া, ক্রম ঠিক।
git rebase -i HEAD~6 মানে “শেষ ছয়টি commit নিয়ে কাজ” (base = HEAD~6). Git একটি editor-এ todo list খোলে — প্রতিটি লাইনে একটি command, commit-এর ID আর বার্তা, পুরোনোটি ওপরে (log-এর উল্টো ক্রম)। আপনি লাইনের শুরুর শব্দ বদলান, লাইন ওপর-নিচ করেন, সেভ করে বন্ধ করেন; Git ওপর থেকে নিচে চালায়।
| Command | কাজ | কখন কাজে লাগে |
|---|---|---|
pick (p) | commit যেমন আছে রাখা | ডিফল্ট |
reword (r) | রাখা, কিন্তু বার্তা বদলানোর জন্য editor খোলা | বানান-ভুল বা অস্পষ্ট বার্তা |
edit (e) | commit বসিয়ে থামা; আপনি ফাইল বদলে commit --amend করে --continue দেন | পুরোনো commit-এ কিছু যোগ করা, বা একটি commit দুই ভাগ করা |
squash (s) | আগের commit-এর সাথে মেলানো; দুই বার্তা একসাথে editor-এ | একই কাজের টুকরো, দুই বার্তাই দরকারি |
fixup (f) | আগের commit-এর সাথে মেলানো, এই commit-এর বার্তা ফেলে দেওয়া (fixup -C: উল্টো, শুধু এই commit-এর বার্তা রাখা; -c: একই, তবে editor খোলে) | “Fix typo” জাতীয় সংশোধন |
drop (d) | commit বাদ | Debug print, ভুল পরীক্ষা |
exec (x) | shell command চালানো; ব্যর্থ হলে rebase থামে | প্রতিটি commit-এর পরে test চালানো |
break (b) | এখানে থামা, পরে --continue | মাঝপথে অবস্থা পরীক্ষা |
label, reset, merge, update-ref | নাম রাখা, সেই নামে ফেরা, merge commit বানানো, branch সরানোর চিহ্ন | সাধারণত Git নিজে লেখে — --rebase-merges বা --update-refs দিলে |
| লাইনের ক্রম বদল | commit-এর ক্রম বদল | সম্পর্কিত commit পাশাপাশি আনা |
নোটিশ বোর্ডের কাজের একটি branch-এ ছয়টি commit আছে। লক্ষ্য: typo-ঠিক মূল commit-এ মেশানো, তারিখের দুই commit একটি করা, debug commit বাদ, বানান ভুল বার্তা ঠিক, আর categories-এর কাজ আগে আনা; শেষে প্রতিটি ফাইল আছে কি না exec দিয়ে দেখা। (Transcript বানাতে editor-এর কাজ script দিয়ে করানো হয়েছে: GIT_SEQUENCE_EDITOR environment variable todo list-এর editor বদলায়, GIT_EDITOR বার্তার editor. CI বা script-এ interactive rebase স্বয়ংক্রিয় করার এটাই প্রচলিত পদ্ধতি।)
$ git log --oneline
6dadb43 (HEAD -> feature/notice) Add notice categoriess
92fbb0e Debug print
8a3b36f Add date format
769758f Add notice dates
1a1ddb1 Fix typo in notice board
600a574 Add notice board
8e9e4e7 (main) Add README
9316721 Add portal config
$ git rebase -i HEAD~6
# The editor opens the todo list (comment lines trimmed):
pick 600a574 # Add notice board
pick 1a1ddb1 # Fix typo in notice board
pick 769758f # Add notice dates
pick 8a3b36f # Add date format
pick 92fbb0e # Debug print
pick 6dadb43 # Add notice categoriess
# Rebase 8e9e4e7..6dadb43 onto 8e9e4e7 (6 commands)
#
# Commands:
# p, pick <commit> = use commit
# …
# We change it to this, save and close:
pick 600a574 Add notice board
fixup 1a1ddb1 Fix typo in notice board
reword 6dadb43 Add notice categoriess
pick 769758f Add notice dates
squash 8a3b36f Add date format
drop 92fbb0e Debug print
exec ls
# reword: the editor shows "Add notice categoriess"; we fix it to "Add notice categories"
[detached HEAD a665934] Add notice categories
Date: Thu Jan 15 10:08:00 2026 +0600
1 file changed, 2 insertions(+)
create mode 100644 categories.txt
# squash: the editor shows both messages; we write "Add notice dates with dd-mm-yyyy format"
[detached HEAD 78f0f9a] Add notice dates with dd-mm-yyyy format
Date: Thu Jan 15 10:05:00 2026 +0600
1 file changed, 2 insertions(+)
create mode 100644 dates.txt
Executing: ls
README.md
categories.txt
dates.txt
notices.txt
portal.conf
Successfully rebased and updated refs/heads/feature/notice.Squash-এর সময় editor-এ দুই বার্তা এভাবে আসে; # দিয়ে শুরু লাইন বাদ যায়, বাকিটা নতুন বার্তা:
# This is a combination of 2 commits.
# This is the 1st commit message:
Add notice dates
# This is the commit message #2:
Add date format
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
…
$ git log --oneline --graph --all
* 78f0f9a (HEAD -> feature/notice) Add notice dates with dd-mm-yyyy format
* a665934 Add notice categories
* 86c0eed Add notice board
* 8e9e4e7 (main) Add README
* 9316721 Add portal config
$ git log --format="%h %s" main..
78f0f9a Add notice dates with dd-mm-yyyy format
a665934 Add notice categories
86c0eed Add notice board
$ git show --stat --format="%h %s" HEAD~1
a665934 Add notice categories
categories.txt | 2 ++
1 file changed, 2 insertions(+)ছয়টি commit থেকে তিনটি পরিষ্কার commit: 86c0eed “Add notice board” (typo-ঠিক মিশে গেছে, বার্তা প্রথমটিরই), a665934 “Add notice categories” (বানান ঠিক, ক্রমে এখন দ্বিতীয়), আর 78f0f9a “Add notice dates with dd-mm-yyyy format” (দুই commit এক)। Debug commit-এর debug.txt কোথাও নেই — exec ls-এর তালিকায়ও দেখা যাচ্ছে। Reword আর squash-এর ফলাফলে Date: Thu Jan 15 10:08:00 লাইন আসে, কারণ author date (মূল লেখার সময়) এখন committer date থেকে আলাদা। আর খেয়াল করুন, পুরো ছয়টির base main-এর মাথা 8e9e4e7-ই — interactive rebase-এ ভিত না বদলেও ইতিহাস নতুন করে লেখা যায়।
Todo list কীভাবে ধাপে ধাপে চলে, নিচে reflog-এর আসল ID দিয়ে দেখুন:
1pick 600a574 Add notice board2fixup 1a1ddb1 Fix typo in notice board3reword 6dadb43 Add notice categoriess4pick 769758f Add notice dates5squash 8a3b36f Add date format6drop 92fbb0e Debug print7exec ls
| HEAD | |
| নতুন commit | |
| feature/notice |
- লাইন ১ — pick. প্রথম commit-এর parent আগে থেকেই base
8e9e4e7, আর এতে কিছু বদলাচ্ছে না। তাই Git নতুন commit না বানিয়ে পুরোনো600a574-ই রেখে দেয় — reflog-এrebase (start)সরাসরি600a574-এ। অপরিবর্তিত শুরুর commit-এর ID তাই একই থাকে। - লাইন ২ — fixup. “Fix typo”-র পরিবর্তন আগের commit-এর সাথে মিশল, বার্তা থাকল “Add notice board”. বিষয়বস্তু বদলেছে, তাই নতুন ID
86c0eed. নতুন ইতিহাসে এখনো একটিই commit. - লাইন ৩ — reword. ক্রম বদলানো হয়েছিল, তাই categories-এর commit এখন দ্বিতীয়। Git প্রথমে commit বসায় (
c3fb83c), তারপর বার্তার editor খোলে; বানান ঠিক করার পর চূড়ান্ত commita665934. - লাইন ৪ — pick. “Add notice dates” নতুন parent
a665934-এর ওপর →c07af10. এটি টিকবে না, পরের লাইনেই বদলাবে। - লাইন ৫ — squash. “Add date format” আগেরটির সাথে মিশল। Editor-এ দুই বার্তা এলো; নতুন বার্তা লেখা হলো, ফল
78f0f9a. Reflog-এc07af10-এর ঘর থেকে যায়, কিন্তু কোনো branch আর সেটি দেখায় না। - লাইন ৬ — drop. “Debug print” বাদ — কোনো commit তৈরি হলো না, HEAD একই। লাইনটি মুছে দিলেও একই ফল হতো; Git-এর নির্দেশনাতেই লেখা থাকে, লাইন সরালে সেই commit হারায়।
- লাইন ৭ — exec. Git shell-এ
lsচালাল। Command ব্যর্থ হলে (exit code শূন্য না হলে) rebase এখানে থামত। বাস্তবে এখানে বসে test-এর command, যেমনexec npm test. - শেষ। তালিকা শেষ, তাই
feature/noticeসরে78f0f9a-এ, আরORIG_HEADমনে রাখে পুরোনো6dadb43. ছয়টি পুরোনো commit-এর কোনোটিই মোছেনি — অংশ ৭-এ সেগুলো ফিরিয়ে আনব।
Todo list-এর নিয়মগুলো একটি ছোট interpreter দিয়েও বোঝা যায়। নিচের প্রোগ্রামটি একই তালিকা চালিয়ে নতুন ইতিহাস আর বার্তা ছাপে (reword-এর নতুন বার্তা এখানে লাইনেই লেখা, আসল Git editor-এ চায়):
// Interactive rebase todo interpreter: pick, reword, squash, fixup, drop.
const msg = {
a1: "Add notice board", a2: "Fix typo in notice board", a3: "Add notice dates",
a4: "Add date format", a5: "Debug print", a6: "Add notice categoriess"
};
function runTodo(todo) {
const out = []; // new history: {from: [old ids], msg}
for (const line of todo.trim().split("\n")) {
const [cmd, id, ...text] = line.trim().split(/\s+/);
const prev = out[out.length - 1];
if (cmd === "pick") out.push({ from: [id], msg: msg[id] });
else if (cmd === "reword") out.push({ from: [id], msg: text.join(" ") }); // Git asks in an editor
else if (cmd === "squash" || cmd === "fixup") {
if (!prev) throw new Error("cannot '" + cmd + "' without a previous commit");
prev.from.push(id);
if (cmd === "squash") prev.msg += " / " + msg[id]; // Git lets you edit both messages
} else if (cmd !== "drop") throw new Error("unknown command " + cmd);
}
return out;
}
const todo = `
pick a1
fixup a2
reword a6 Add notice categories
pick a3
squash a4
drop a5`;
runTodo(todo).forEach((c, i) =>
console.log((i + 1) + ". " + c.msg.padEnd(36) + "<- " + c.from.join("+")));
const todo2 = todo.replace("drop a5", ""); // delete the line instead
const lost = Object.keys(msg).filter(id => !todo2.includes(" " + id));
console.log("line deleted from the todo -> commit lost:", lost.join(", "));
try { runTodo("fixup a2\npick a1"); } catch (e) { console.log("error:", e.message); }
আউটপুট:
1. Add notice board <- a1+a2
2. Add notice categories <- a6
3. Add notice dates / Add date format <- a3+a4
line deleted from the todo -> commit lost: a5
error: cannot 'fixup' without a previous commit
তিনটি নতুন commit, আর ডান দিকে কোন পুরোনো commit কোথায় মিশেছে: a1+a2 (fixup — বার্তা a1-এর), a6 একা (নতুন বার্তা), a3+a4 (squash — দুই বার্তাই, আসল Git-এ আপনি সম্পাদনা করেন)। চতুর্থ লাইন দেখায় drop লেখা আর লাইন মুছে ফেলা একই ফল দেয় — a5 নতুন ইতিহাসে নেই। শেষ লাইন: তালিকার প্রথমেই fixup থাকলে মেশানোর মতো আগের commit নেই; আসল Git-ও তখন একই ধরনের error দিয়ে তালিকা ঠিক করতে বলে।
commit --fixup আর --autosquash — গোছানো স্বয়ংক্রিয়
Review-এর মাঝে প্রায়ই দেখা যায় দুই commit আগের একটি কাজে ভুল। তখন সংশোধনটি একটি বিশেষ commit হিসেবে করুন: git commit --fixup=<commit>. Git বার্তা বানায় fixup! <মূল commit-এর বার্তা>. পরে git rebase -i --autosquash দিলে Git নিজেই todo list সাজিয়ে fixup commit-কে মূলটির ঠিক নিচে fixup command-সহ বসায়:
# (create login.txt; git add)
$ git commit -m "Add login page"
[feature/login 5631ab5] Add login page
1 file changed, 1 insertion(+)
create mode 100644 login.txt
# (create logout.txt; git add)
$ git commit -m "Add logout button"
[feature/login 0f360a4] Add logout button
1 file changed, 1 insertion(+)
create mode 100644 logout.txt
# (a bug in the login page: add the captcha field to login.txt; git add)
$ git commit --fixup=5631ab5
[feature/login 0e7b03c] fixup! Add login page
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline
0e7b03c (HEAD -> feature/login) fixup! Add login page
0f360a4 Add logout button
5631ab5 Add login page
8e9e4e7 (main) Add README
9316721 Add portal config
$ git rebase -i --autosquash main
# (with --autosquash the todo list already arrives arranged like this; we only save and close)
pick 5631ab5 # Add login page
fixup 0e7b03c # fixup! Add login page
pick 0f360a4 # Add logout button
Successfully rebased and updated refs/heads/feature/login.
$ git log --oneline
d18169d (HEAD -> feature/login) Add logout button
4e4b6bc Add login page
8e9e4e7 (main) Add README
9316721 Add portal config
$ cat login.txt
login form: roll, password, captcha0e7b03c fixup! Add login page সবার ওপরে ছিল, কিন্তু autosquash todo list-এ তাকে “Add login page”-এর নিচে সরিয়ে fixup বসাল — আপনাকে শুধু সেভ করতে হলো। ফল দুটি পরিষ্কার commit; login.txt-এ captcha-সহ চূড়ান্ত রূপ। একই পরিবারে আরও আছে: --squash=<commit> (squash!, বার্তা মেলানোর সুযোগসহ), আর নতুন সংস্করণে --fixup=amend:<commit> (amend! commit; বার্তাও বদলায়) ও --fixup=reword:<commit> (শুধু বার্তা)। Interactive rebase-এ সবসময় চালু রাখতে git config rebase.autoSquash true; Git ২.৪৪ থেকে --autosquash -i ছাড়াও কাজ করে বলে release note-এ আছে — আপনার সংস্করণের git help rebase দেখে নিশ্চিত হোন।
৭ · ভুল rebase থেকে উদ্ধার — ORIG_HEAD আর reflog
ধরুন interactive rebase-এর পরে বুঝলেন debug commit-টি আসলে দরকার ছিল, বা squash ভুল হয়েছে। চিন্তার কিছু নেই — পুরোনো ছয়টি commit object-এ আছে। দুটি পথ (বিস্তারিত পর্ব ১১-এ, আর ref ও উদ্ধারের ভেতরের গল্প পর্ব ৫৩-এ):
$ git reflog -12
78f0f9a (HEAD -> feature/notice) HEAD@{0}: rebase (finish): returning to refs/heads/feature/notice
78f0f9a (HEAD -> feature/notice) HEAD@{1}: rebase (squash): Add notice dates with dd-mm-yyyy format
c07af10 HEAD@{2}: rebase (pick): Add notice dates
a665934 HEAD@{3}: rebase (reword): Add notice categories
c3fb83c HEAD@{4}: rebase (reword): Add notice categoriess
86c0eed HEAD@{5}: rebase (fixup): Add notice board
600a574 HEAD@{6}: rebase (start): checkout HEAD~6
6dadb43 HEAD@{7}: commit: Add notice categoriess
92fbb0e HEAD@{8}: commit: Debug print
8a3b36f HEAD@{9}: commit: Add date format
769758f HEAD@{10}: commit: Add notice dates
1a1ddb1 HEAD@{11}: commit: Fix typo in notice board
$ git reflog show feature/notice -3
78f0f9a (HEAD -> feature/notice) feature/notice@{0}: rebase (finish): refs/heads/feature/notice onto 8e9e4e7f6bce7ab2bb6af6e0b877c8d38156ad2d
6dadb43 feature/notice@{1}: commit: Add notice categoriess
92fbb0e feature/notice@{2}: commit: Debug print
$ git log --oneline ORIG_HEAD -7
6dadb43 Add notice categoriess
92fbb0e Debug print
8a3b36f Add date format
769758f Add notice dates
1a1ddb1 Fix typo in notice board
600a574 Add notice board
8e9e4e7 (main) Add README
$ git reset --hard ORIG_HEAD
HEAD is now at 6dadb43 Add notice categoriess
$ git log --oneline -3
6dadb43 (HEAD -> feature/notice) Add notice categoriess
92fbb0e Debug print
8a3b36f Add date formatHEAD-এর reflog rebase-এর প্রতিটি ধাপ দেখায় — ওপরের stepper-এর ID-গুলো এখান থেকেই নেওয়া। HEAD@{7} হলো rebase শুরুর আগের শেষ commit 6dadb43. Branch-এর reflog (git reflog show feature/notice) আরও পরিষ্কার: পুরো rebase একটি ঘর (feature/notice@{0}: rebase (finish)), আর ঠিক আগের অবস্থা feature/notice@{1} = 6dadb43. ORIG_HEAD-ও সেই commit দেখাচ্ছে, তাই git reset --hard ORIG_HEAD branch-কে rebase-এর আগের অবস্থায় ফিরিয়ে দিল — ছয়টি commit আবার আগের মতো।
দুটি সতর্কতা। (১) ORIG_HEAD একটিই নাম, আর পরের reset, merge বা rebase সেটি নতুন করে লেখে। তাই কিছুক্ষণ পরে উদ্ধার করতে হলে git reflog show <branch> দেখে <branch>@{1}-এর মতো নির্দিষ্ট ঘর ব্যবহার করুন। (২) reset --hard working tree-র commit না করা বদল মুছে দেয়; আগে git status. ঝুঁকির rebase-এর আগে সবচেয়ে সহজ বীমা: git branch backup/notice — একটি নাম রেখে দেওয়া, যা কোনো gc ছুঁতে পারবে না।
পুরোনো commit কতদিন থাকে? Reflog-এর ঘর থাকা পর্যন্ত commit reachable বলে ধরা হয়। Git-এর ডিফল্ট হিসাবে reachable নয় এমন commit-এর reflog-ঘর প্রায় ৩০ দিন, অন্যগুলো ৯০ দিন রাখা হয়, আর কোথাও থেকে পৌঁছানো যায় না এমন object অন্তত দুই সপ্তাহ পরে git gc মুছতে পারে (gc.reflogExpireUnreachable, gc.reflogExpire, gc.pruneExpire — সংখ্যা config দিয়ে বদলানো যায়)। আর reflog শুধু আপনার কম্পিউটারের; অন্য কারও clone-এ আপনার পুরোনো commit নেই।
৮ · Merge বনাম rebase — কখন কোনটি
অংশ ২-এর একই জোড়া branch দিয়ে দুটি পথ পাশাপাশি দেখা যাক। প্রথম পথে rebase না করে main-এ সরাসরি merge; দ্বিতীয় পথে rebase-এর পরে fast-forward. (প্রথমটি একই repository-র দ্বিতীয় একটি কপিতে, rebase-এর আগে; সময় আর লেখক একই বলে ID-ও মিলে যায়।)
# Way 1 (a second copy of the same repo, before any rebase): merge
$ git switch main
Already on 'main'
$ git merge --no-edit feature/gpa
Merge made by the 'ort' strategy.
gpa.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 gpa.txt
$ git log --oneline --graph
* b1bf3a3 (HEAD -> main) Merge branch 'feature/gpa'
|\
| * 3af94ac (feature/gpa) Show GPA on result page
| * 2e9e344 Add GPA calculator
* | 2313ddc Add contact to README
|/
* 8e9e4e7 Add README
* 9316721 Add portal config
# Way 2 (the rebased repo above): fast-forward main
$ git switch main
Switched to branch 'main'
$ git merge --ff-only feature/gpa
Updating 2313ddc..2d52732
Fast-forward
gpa.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 gpa.txt
$ git log --oneline --graph
* 2d52732 (HEAD -> main, feature/gpa) Show GPA on result page
* 0324b2d Add GPA calculator
* 2313ddc Add contact to README
* 8e9e4e7 Add README
* 9316721 Add portal config| Merge পথ | |
| Rebase পথ | |
| C আর D-র ID |
- একই শুরু। দুই পথেই
mainE-তে,featureD-তে, merge base B. দুই branch আলাদা হয়ে গেছে, তাই সরাসরি fast-forward সম্ভব নয়। - প্রথম কাজ। Merge পথ:
main-এ দাঁড়িয়েgit merge feature/gpa→ merge commit M (b1bf3a3), parent E আর D; C আর D যেমন ছিল তেমন। Rebase পথ:feature-এ দাঁড়িয়েgit rebase main→ C′ আর D′, নতুন ID; C আর D ছায়া। - Main-এ আনা। Merge পথে কাজ শেষ —
mainM-এ। Rebase পথেgit merge --ff-only feature/gpa:mainশুধু সামনে সরে D′-এ, কোনো নতুন commit নেই। - তুলনা। Merge-এর log-এ ৬টি commit আর দুই রেখা — কোন কাজ কোন base থেকে, কখন মিলল, সব সত্যি ইতিহাস। Rebase-এর log-এ ৫টি commit, এক সরলরেখা — পড়া সহজ, কিন্তু C′ আর D′ এমন অবস্থায় “তৈরি” দেখায় যেখানে সেগুলো আসলে লেখা হয়নি। দুই পথেই চূড়ান্ত ফাইল হুবহু এক।
| বিষয় | Merge | Rebase |
|---|---|---|
| ইতিহাস | সত্যি ইতিহাস — কে কোথা থেকে শুরু করেছিলেন, কখন মিলল | Linear history — সোজা, পড়া সহজ, কিন্তু নতুন করে লেখা |
| Commit ID | অপরিবর্তিত; কারও কাজ ভাঙে না | সব replay করা commit-এর ID নতুন |
| Conflict | একবার, দুই tip মিলিয়ে | প্রতি commit-এ আলাদা; একই লাইন বারবার আসতে পারে |
| git bisect (পর্ব ৫৫) | Merge commit-সহ graph-এ কাজ করে; --first-parent দিয়ে শুধু মূল পথ | প্রতিটি commit এক রেখায় — binary search সহজ; তবে replay করা কোনো মাঝের commit হয়তো কখনো build-ই হয়নি |
| পুরো feature ফেরত | একটি merge commit revert -m 1 | প্রতিটি commit আলাদা করে revert (বা আগে squash) |
| নিরাপত্তা | Shared branch-এ নিরাপদ | শুধু নিজের, এখনো কেউ ব্যবহার করেননি এমন commit-এ |
সোনালি নিয়ম
সেগুলো কখনো rebase করবেন না।
কেন? ধরুন আপনি main-এর মতো shared branch rebase করে force push করলেন। একজন সহকর্মীর কাছে এখনো পুরোনো C আর D আছে, তার ওপর তিনি নিজের কাজ করেছেন। পরের pull-এ (merge দিয়ে) তাঁর Git দেখবে দুই আলাদা ইতিহাস — পুরোনো C, D আর আপনার নতুন C′, D′ — আর merge করে দুটোই রেখে দেবে। একই পরিবর্তন দুবার, দুই ভিন্ন ID-তে; তার ওপর conflict. কেউ আবার সেটি push করলে আপনার “পরিষ্কার” ইতিহাসে পুরোনো commit ফিরে আসে। Git-এর documentation-এ এই অবস্থাকে বলা হয় “recovering from upstream rebase” — অর্থাৎ যিনি rebase করেননি তাঁকে খেসারত দিতে হয়। এই নিয়মটিই rebase-এর সোনালি নিয়ম.
তাহলে rebase কোথায় নিরাপদ? নিজের local commit-এ (push করার আগে), আর নিজের pull request branch-এ, যেখানে শুধু আপনি push করেন। দ্বিতীয়টিতে branch আগেই push হয়ে আছে, তাই rebase-এর পরে সাধারণ push প্রত্যাখ্যাত হবে; চাই force push. কিন্তু খালি --force remote-এ যা আছে চোখ বন্ধ করে মুছে দেয়। নিরাপদ রূপ --force-with-lease: “remote-এ branch-টি যদি এখনো সেখানে থাকে যেখানে আমি শেষবার দেখেছিলাম, তবেই বদলানো হোক”.
# (feature/sms with one commit "Add SMS alert" is already pushed; main moved on the server)
$ git fetch
From ../origin
8e9e4e7..7c19bd3 main -> origin/main
$ git rebase origin/main
Successfully rebased and updated refs/heads/feature/sms.
$ git push
To ../origin.git
! [rejected] feature/sms -> feature/sms (non-fast-forward)
error: failed to push some refs to '../origin.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
$ git push --force-with-lease
To ../origin.git
+ 96722f0...baca70f feature/sms -> feature/sms (forced update)Rebase-এর পরে সাধারণ push: (non-fast-forward). সাথের hint বলছে git pull করতে — এখানে সেটি ফাঁদ: merge দিয়ে pull করলে পুরোনো 96722f0 আবার merge হয়ে ফিরবে, একই কাজ দুবার। ঠিক পথ git push --force-with-lease: remote-এ branch তখনো 96722f0-এ ছিল, যা Rahim-এর origin/feature/sms-এর সাথে মেলে, তাই + 96722f0...baca70f (forced update). এবার দেখুন lease কীভাবে রক্ষা করে:
# (Karim, by mistake, pushed "Add SMS text" to the same feature/sms;
# Rahim, unaware, commits "Add SMS sender name" and rebases again)
$ git push --force-with-lease
To ../origin.git
! [rejected] feature/sms -> feature/sms (stale info)
error: failed to push some refs to '../origin.git'
$ git fetch
From ../origin
baca70f..ff5f531 feature/sms -> origin/feature/sms
$ git log --oneline --graph --all
* e4a14fb (HEAD -> feature/sms) Add SMS sender name
| * ff5f531 (origin/feature/sms) Add SMS text
|/
* baca70f Add SMS alert
* 7c19bd3 (origin/main, origin/HEAD) Show rank on result page
* 8e9e4e7 (main) Add README
* 9316721 Add portal configKarim-এর ff5f531 remote-এ আছে, কিন্তু Rahim-এর origin/feature/sms তখনো পুরোনো baca70f দেখাচ্ছিল। মেলেনি, তাই (stale info) — push প্রত্যাখ্যাত, Karim-এর কাজ বাঁচল। খালি --force হলে ff5f531 নিঃশব্দে মুছে যেত। Fetch-এর পরে graph-এ দুই পক্ষের কাজ দেখা যাচ্ছে; এখন Rahim git rebase origin/feature/sms করে Karim-এর commit নিজের নিচে নিতে পারেন। একটি সূক্ষ্ম দুর্বলতা: fetch-এর পরে origin/feature/sms হালনাগাদ হয়ে যায়, তাই Karim-এর commit না দেখেই আবার --force-with-lease দিলে এবার তা সফল হবে। কিছু editor নিজে থেকে পেছনে fetch চালায়, তখন ঝুঁকি বাড়ে। Git ২.৩০ থেকে --force-if-includes যোগ করলে Git আরও যাচাই করে যে remote-এর commit আপনার local ইতিহাসে আসলে ঢুকেছে কি না।
দলের নীতি
- Rebase-তারপর-merge: লেখক নিজের branch
main-এর ওপর rebase করেন, তারপর--no-ffmerge — ভেতরে সোজা ইতিহাস, বাইরে feature-এর সীমানা। - Squash merge: পুরো branch একটি commit হয়ে
main-এ ঢোকে। ছোট কাজে পরিষ্কার, কিন্তু ভেতরের ধাপ হারায়। - Rebase merge: branch-এর commit-গুলো একে একে
main-এর ওপর বসানো, merge commit ছাড়া।
অনেক hosting platform-এ pull request merge করার সময় এই তিন বোতাম থাকে (নাম আর আচরণ platform-ভেদে আলাদা — যেমন কোথাও “rebase” বোতাম সবসময় নতুন ID বানায়); বিস্তারিত পর্ব ১৬-এ (Pull request আর fork). কোন branch-এ কে force push করতে পারবেন, protected branch-এর নিয়ম আর trunk-based কাজের ধারায় rebase-এর জায়গা — পর্ব ১৭-এ (Branching strategy). ভালো commit বার্তার নিয়ম, যা interactive rebase-এ কাজে লাগে, পর্ব ১৮-এ (ভালো commit, tag আর release).
আরও গভীরে: Rebase-এর ভিত্তি cherry-pick — একটি commit-এর পরিবর্তন অন্য জায়গায় নতুন commit হিসেবে বসানো (পর্ব ৫৬)। Rebase কার্যত ক্রমানুসারে কয়েকটি cherry-pick, আর patch-id (পরিবর্তনের নিজস্ব hash, line number বাদ দিয়ে) মিলিয়ে বোঝে কোন commit upstream-এ আগেই আছে। Scott Chacon ও Ben Straub-এর Pro Git (২য় সংস্করণ, ২০১৪) বইয়ের “The Perils of Rebasing” অংশে সোনালি নিয়মের সুপরিচিত রূপটি আছে, আর git help rebase-এর “RECOVERING FROM UPSTREAM REBASE” অংশে অন্যের rebase-এর পরে কী করবেন তার নির্দেশনা। Commit object-এর গঠন পর্ব ৫২-এ।
পরীক্ষা ও ইন্টারভিউতে:
- Rebase-এর পরে commit hash বদলায় কেন? — Hash পুরো commit object থেকে হয়, আর replay-তে parent (আর committer date) বদলায়।
- Merge আর rebase-এর পার্থক্য? — Merge দুই parent-ওয়ালা নতুন commit বানিয়ে ইতিহাস রাখে; rebase commit-গুলো নতুন base-এ নতুন ID-তে বসিয়ে সোজা ইতিহাস বানায়।
- Squash আর fixup-এর পার্থক্য? — দুটিই আগের commit-এর সাথে মেলায়; squash দুই বার্তা সম্পাদনা করতে দেয়, fixup নিজের বার্তা ফেলে দেয়।
- ভুল rebase-এর পরে কীভাবে ফিরবেন? —
git reflogদেখেgit reset --hard ORIG_HEADবা<branch>@{1}. - Rebase-এর পরে push কীভাবে? — শুধু নিজের branch-এ,
git push --force-with-lease; shared branch-এ নয়।
৯ · AI-এর যুগে — rebase-এর পরামর্শ যাচাই
AI assistant-কে history পরিষ্কার করতে বললেই সে এখন command-এর সারি লিখে দেয়। Rebase-এর ক্ষেত্রে এর কিছু চেনা ভুল আছে, আর ভুলের দাম বেশি, কারণ rebase ইতিহাস নতুন করে লেখে:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Rebase-এর পরে git push --force, কোন branch তা না জিজ্ঞেস করেই — এমনকি main-এ | জিজ্ঞেস করুন “এই commit কি আর কেউ নিয়েছেন?” shared হলে rebase নয়; নিজের হলে --force-with-lease |
“ঠিক করতে” git reset --hard বা git rebase --abort, কাজ হারানোর কথা না বলে | আগে git status আর একটি backup branch; reflog-এর ঘর লিখে রাখুন |
অস্তিত্বহীন flag (যেমন --squash-all) বা ভুল সংস্করণের দাবি | git help rebase, git --version, অফিসিয়াল documentation |
Rebase conflict-এ --ours/--theirs-এর অর্থ merge-এর মতো ধরে নেওয়া | Marker-এর পাশের নাম আর git status পড়ুন; rebase-এ ours = নতুন base |
| Todo list-এ লাইন মুছে “গুছিয়ে” দেওয়া — commit নীরবে হারায় | শেষে git range-diff বা git diff ORIG_HEAD দিয়ে দেখুন কী বদলাল |
Prompt-এ পুরো git log -p — সাথে config-এর password বা token | AI-কে দেওয়ার আগে গোপন মান মুছে দিন (পর্ব ৫৭) |
নিচের prompt-এ AI-কে branch-এর অবস্থা, কে কে ব্যবহার করেন আর কী চান — সব জানিয়ে দেওয়া হয়েছে, আর প্রতিটি command-এর প্রভাব ব্যাখ্যা করতে বলা হয়েছে, যাতে আপনি যাচাই করতে পারেন:
AI-এর দেওয়া rebase-পরিকল্পনা হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Rebase নিয়ে Git সম্প্রদায়ে এত বিতর্ক কেন? কেউ বলেন “সবসময় rebase”, কোনো version control ব্যবস্থা আবার rebase রাখেইনি।
বিতর্কের মূলে একটি দার্শনিক প্রশ্ন: version control-এর ইতিহাস কী — যা সত্যিই ঘটেছিল তার রেকর্ড, নাকি পাঠকের জন্য গুছিয়ে লেখা একটি গল্প? প্রথম মত বলে, ইতিহাস হলো আদালতের নথির মতো; কেউ কোন base থেকে কাজ শুরু করেছিলেন, কখন কী মিলিয়েছিলেন — সব রাখা উচিত, কারণ পরে bug খুঁজতে সেটাই প্রমাণ। দ্বিতীয় মত বলে, ইতিহাস হলো বইয়ের মতো; লেখকের প্রতিটি কাটাকুটি পাঠকের দরকার নেই, দরকার পরিষ্কার অধ্যায়।
Git দুই পক্ষকেই হাতিয়ার দিয়েছে, আর Linux kernel-এর সংস্কৃতি একটি মাঝামাঝি নিয়ম দাঁড় করিয়েছে। Linus Torvalds একাধিকবার mailing list-এ লিখেছেন (২০০৯ সালের দিকের একটি ইমেইল বিশেষভাবে উদ্ধৃত হয়) যে নিজের ব্যক্তিগত, এখনো প্রকাশ না করা কাজ যত খুশি গুছিয়ে নিন, কিন্তু অন্যরা যে tree থেকে pull করেন, সেটি rebase করবেন না। অর্থাৎ rebase খারাপ নয়, প্রকাশিত ইতিহাস বদলানো খারাপ।
অন্য প্রান্তে আছে Fossil — SQLite-এর লেখক D. Richard Hipp-এর তৈরি version control ব্যবস্থা। Fossil-এর documentation-এ “Rebase Considered Harmful” নামে একটি লেখা আছে, আর Fossil ইচ্ছে করেই rebase রাখেনি। তাদের যুক্তি: rebase পরীক্ষা না করা মাঝের অবস্থা তৈরি করে, প্রকৃত প্রেক্ষাপট মুছে দেয়, আর সোজা ইতিহাসের সুবিধা ভালো log-দেখানোর টুল দিয়েও পাওয়া যায়।
বাস্তবে বেশিরভাগ দল একটি ব্যবহারিক মাঝামাঝি পথে চলে: নিজের branch-এ interactive rebase দিয়ে commit গোছানো (review সহজ হয়), তারপর main-এ merge commit বা squash. গুরুত্বপূর্ণ হলো নীতিটি লিখিত আর সবাই মানেন। এক দলে অর্ধেক মানুষ force push করলে আর অর্ধেক merge করলে কোনো ইতিহাসই আর নির্ভরযোগ্য থাকে না। আর এই সিদ্ধান্ত দলের প্রশ্নের ওপর নির্ভর করে — history দিয়ে আপনারা কী খোঁজেন: কোন feature কবে ঢুকল, কোন commit-এ bug এলো, নাকি কে কোথা থেকে কাজ শুরু করেছিলেন।
মূল উপলব্ধি: Rebase আর merge দুটি আলাদা মূল্যবোধ রক্ষা করে — পড়ার সুবিধা আর সত্যি ইতিহাস। সোনালি নিয়ম দুটিকে মেলায়: খসড়া গোছান, প্রকাশিত ইতিহাস ছোঁবেন না।
প্র ০২বলা হয়, linear history-তে git bisect সহজ। কথাটি কতটা সত্যি, আর rebase কি উল্টো বিপদও আনতে পারে?
git bisect (পর্ব ৫৫) হলো ইতিহাসের ওপর binary search: একটি “ভালো” আর একটি “খারাপ” commit দিলে Git মাঝখানের commit checkout করে, আপনি পরীক্ষা করে জানান ভালো না খারাপ, আর প্রতিবার সন্দেহের পরিসর অর্ধেক হয়। ১০২৪টি commit-এর মধ্যে bug-ওয়ালা প্রথম commit খুঁজতে মোটামুটি log₂ ১০২৪ = ১০টি পরীক্ষাই যথেষ্ট। এই হিসাবের শর্ত — প্রতিটি পরীক্ষা নির্ভরযোগ্যভাবে “ভালো” বা “খারাপ” বলতে পারতে হবে।
Linear history এখানে দুইভাবে সাহায্য করে। প্রথমত, পথ একটাই, তাই “মাঝখান” স্পষ্ট। দ্বিতীয়ত, ফল হিসেবে bisect একটি ছোট, নির্দিষ্ট commit দেখায় — “Set pass mark to 50” — একটি বিশাল merge commit নয়, যার ভেতরে বিশটি পরিবর্তন। Merge-ভরা graph-এও bisect কাজ করে, কিন্তু কখনো কখনো ফল আসে একটি merge commit, যেখানে দুই পক্ষ আলাদাভাবে ঠিক ছিল, শুধু একসাথে ভাঙে (পর্ব ১৩-এর semantic conflict). Git ২.২৯ থেকে git bisect start --first-parent দিয়ে শুধু মূল branch-এর পথ ধরে খোঁজা যায় — merge-ভিত্তিক দলের জন্য এটি বড় সুবিধা।
কিন্তু rebase একটি উল্টো বিপদও আনে। Replay করা প্রতিটি মাঝের commit এমন একটি অবস্থা, যা আগে কখনো ছিল না: আপনার পুরোনো commit + নতুন base. সেটি হয়তো compile-ই হয় না, কারণ main-এ কেউ একটি function-এর নাম বদলেছেন, আর আপনার পরের commit-এ সেটি ঠিক করেছেন। bisect তখন সেই ভাঙা commit-এ দাঁড়িয়ে “bug আছে” বা “পরীক্ষা করা যায় না” দেখাবে — ভুল পথে নিয়ে যাবে। git bisect skip দিয়ে এড়ানো যায়, কিন্তু খোঁজ ধীর হয়।
সমাধান অভ্যাসে: rebase-এর সময় git rebase -x "make test" main (বা todo list-এ exec) দিয়ে প্রতিটি নতুন commit-এ build আর test চালানো, যাতে প্রতিটি commit নিজে দাঁড়াতে পারে। অনেক open-source project ঠিক এই কারণে patch series-এর প্রতিটি patch আলাদাভাবে build হওয়া বাধ্যতামূলক করে।
মূল উপলব্ধি: Linear history bisect-কে সাহায্য করে কেবল তখনই, যখন প্রতিটি commit সত্যিই কাজ করে। Rebase ইতিহাস সোজা করে, কিন্তু প্রতিটি commit পরীক্ষা করার দায় আপনার।
প্র ০৩দীর্ঘদিনের একটি branch-এ ৪০টি commit, আর main-এর সাথে একই ফাইলে অনেক বিরোধ। Rebase করবেন, merge করবেন, না অন্য কিছু?
অংশ ৩-এর হিসাব মনে করুন: একই লাইন তিনটি commit-এ বদলালে rebase-এ তিনবার সমাধান, merge-এ একবার। ৪০টি commit-এর branch, যার অনেকগুলো একই ফাইলের একই অংশ ছোঁয়, rebase করলে কয়েক ডজন conflict আসতে পারে — প্রতিটির জন্য মনে করতে হবে সেই commit-এর সময় কোডটি কেমন ছিল। ক্লান্ত মানুষ তখন ভুল সমাধান করেন, আর সেই ভুল পরের commit-গুলোর base হয়ে ছড়ায়।
কয়েকটি পথ আছে, প্রতিটির দাম আলাদা। (১) Merge একবার: main-কে branch-এ merge করুন (বা branch-কে main-এ)। সব বিরোধ এক জায়গায়, এক প্রেক্ষাপটে; ইতিহাস সত্যি থাকে। দাম: graph-এ একটি merge commit, আর ভেতরের commit-গুলো পুরোনো base-এ থাকে। (২) আগে squash, তারপর rebase: git rebase -i-এ নিজের ৪০টিকে কয়েকটি যুক্তিসঙ্গত commit-এ নামিয়ে আনুন — নিজের base-এর ওপর, তাই প্রায় conflict ছাড়া — তারপর main-এর ওপর rebase. সমাধান কমে যায়; দাম হলো ছোট ধাপগুলোর ইতিহাস হারানো। (৩) rerere: git config rerere.enabled true দিলে Git প্রতিটি সমাধান মনে রাখে; একই conflict আবার এলে নিজেই বসায় (পর্ব ৫৪)। বারবার rebase করতে হয় এমন branch-এ খুব কাজের।
কিন্তু সবচেয়ে বড় শিক্ষা হলো সমস্যাটি এড়ানো। দীর্ঘজীবী branch-এ বিরোধ সময়ের সাথে বাড়ে, কারণ দুই পক্ষ যত দূরে যায়, মেলানো তত কঠিন। ছোট branch, ঘন ঘন main-এর সাথে হালনাগাদ (দিনে একবার git pull --rebase বা git rebase origin/main), আর বড় কাজকে feature flag-এর আড়ালে ছোট ছোট অংশে main-এ ঢোকানো — trunk-based development-এর মূল কথা এটাই (পর্ব ১৭)। ঈদের আগে বাসের টিকিটের ভিড়ের মতো: সবাই শেষ দিনে গেলে লাইন অসহনীয়, আগে আগে অল্প অল্প গেলে ভিড় হয় না।
মূল উপলব্ধি: Rebase-এর খরচ commit-এর সংখ্যার সাথে বাড়ে, merge-এর খরচ বাড়ে না। বড়, পুরোনো branch-এ merge বা squash-তারপর-rebase বেছে নিন — আর ভবিষ্যতে branch ছোট রাখুন।
প্র ০৪Force push নিয়ে এত সতর্কতা কেন, আর --force-with-lease কি সব সমস্যার সমাধান?
সাধারণ push-এর একটি অন্তর্নির্মিত নিরাপত্তা আছে: remote-এর branch শুধু fast-forward হতে পারে, অর্থাৎ নতুন commit যোগ হয়, পুরোনো কিছু হারায় না। Rebase এই শর্ত ভাঙে, কারণ নতুন ইতিহাসে পুরোনো commit নেই। git push --force সেই নিরাপত্তা বন্ধ করে বলে, “remote-এ যা-ই থাকুক, আমার ইতিহাস বসানো হোক।” আপনি যদি শেষবার fetch করার পরে কেউ সেখানে commit push করে থাকেন, সেগুলো remote থেকে নিঃশব্দে মুছে যায়। তাঁর কম্পিউটারে হয়তো কপি আছে, কিন্তু তিনি জানবেনই না যে কিছু হারিয়েছে, যতক্ষণ না কেউ খোঁজেন।
--force-with-lease (আমাদের জানা মতে Git ১.৮.৫, ২০১৩ থেকে) একটি “compare-and-swap”: remote-এর branch ঠিক সেই commit-এ আছে কি না, যা আপনার origin/<branch> দেখায় — থাকলে বদলানো হয়, না থাকলে প্রত্যাখ্যান (stale info). অংশ ৮-এ এটাই Karim-এর commit বাঁচিয়েছিল। কিন্তু lease-এর ভিত্তি আপনার remote-tracking branch, আর সেটি প্রতিটি fetch-এ হালনাগাদ হয়। কেউ git fetch করে কিন্তু নতুন commit নিজের branch-এ না নিয়ে আবার --force-with-lease দিলে lease “মিলে যায়”, আর মুছে ফেলা ঘটে। কিছু IDE পেছনে নিজে থেকে fetch চালায় বলে এটি অজান্তেই হতে পারে। Git ২.৩০-এর --force-if-includes এই ফাঁক বন্ধ করে — remote-এর commit আপনার local ইতিহাসে (reflog দেখে) আসলে ঢুকেছে কি না, তা যাচাই করে। Lease-এর জন্য নির্দিষ্ট commit-ও দেওয়া যায়: --force-with-lease=.
তাই টুলের বাইরে নিয়মও লাগে। বেশিরভাগ hosting platform-এ main-এর মতো branch-এ force push বন্ধ রাখা যায় (“protected branch”, নাম আর বিকল্প platform-ভেদে আলাদা)। PR branch-এ নিয়ম: এক branch-এ এক লেখক; অন্য কেউ সাহায্য করতে চাইলে নিজের branch থেকে PR-এ PR. Review চলাকালে force push করলে কিছু platform-এ আগের মন্তব্যের প্রেক্ষাপট হারাতে পারে, তাই অনেক দল review শেষে একবার গুছিয়ে force push করে।
মূল উপলব্ধি: Force push মানে অন্যের কাজ মোছার ক্ষমতা। --force-with-lease (দরকারে --force-if-includes-সহ) সেই ক্ষমতায় একটি তালা, আর protected branch ও এক-লেখক-এক-branch নিয়ম দ্বিতীয় তালা।
প্র ০৫Rebase-এ commit ID বদলায় — তাহলে code review ব্যবস্থাগুলো কীভাবে বোঝে যে নতুন commit আসলে আগের সেই পরিবর্তনই?
এই প্রশ্নের উত্তর খুঁজতে গিয়ে কয়েকটি ভিন্ন কাজের ধারা তৈরি হয়েছে। প্রথমটি Linux kernel-এর email-ভিত্তিক ধারা: developer git format-patch দিয়ে commit-গুলো patch হিসেবে পাঠান, reviewer মন্তব্য করেন, তারপর developer rebase করে সংশোধিত সিরিজ পাঠান “[PATCH v2]”, “v3” নামে। এখানে পরিচয় commit ID দিয়ে নয়, বিষয় আর সিরিজের সংস্করণ দিয়ে। পুরোনো আর নতুন সিরিজ তুলনা করতে Git ২.১৯ (২০১৮)-এ এসেছে git range-diff — অংশ ২-এ দেখেছেন, এটি জোড়ায় জোড়ায় বলে কোন commit অপরিবর্তিত (=) আর কোনটি বদলেছে (!)।
দ্বিতীয়টি Gerrit-এর মতো review ব্যবস্থা। প্রতিটি commit বার্তার শেষে একটি Change-Id: লাইন যোগ হয় (একটি hook দিয়ে)। Amend বা rebase-এ commit ID বদলালেও Change-Id একই থাকে, তাই Gerrit নতুন commit-কে একই change-এর নতুন “patch set” হিসেবে দেখায়, আগের মন্তব্যসহ। প্রতিটি commit সেখানে নিজেই একটি review-এর একক।
তৃতীয়টি “stacked diffs” বা stacked PR: বড় কাজ কয়েকটি ছোট, পরস্পর-নির্ভর পরিবর্তনে ভাগ করা, প্রতিটি আলাদা review. Facebook-এ (এখন Meta) তৈরি Phabricator (পরে open source, যদিও মূল project-এর রক্ষণাবেক্ষণ বছর কয়েক আগে বন্ধ হয়েছে বলে জানা যায়) এই ধারা জনপ্রিয় করে। Git-এ এর ঝামেলা ছিল — নিচের branch বদলালে ওপরের সব branch হাতে rebase করা। অংশ ৫-এর --update-refs (Git ২.৩৮) ঠিক এই ব্যথা কমাতে এসেছে; এর আশপাশে নানা বাইরের টুলও আছে।
জনপ্রিয় hosting platform-এর pull request সাধারণত branch-ভিত্তিক: PR মানে “এই branch-এর মাথা”. Force push-এর পরে platform আগের আর নতুন মাথার তুলনা দেখাতে পারে (ব্যবস্থা platform-ভেদে আলাদা), কিন্তু commit-ধরে মন্তব্য অনেক সময় “পুরোনো” হয়ে যায়। একারণে কেউ review চলাকালে rebase না করে নতুন commit যোগ করেন, আর merge-এর সময় squash করেন (পর্ব ১৬)।
মূল উপলব্ধি: Commit ID বিষয়বস্তুর পরিচয়, কাজের পরিচয় নয়। Rebase-ভিত্তিক review-এ পরিচয় রাখতে আলাদা কিছু লাগে — সিরিজের সংস্করণ, Change-Id, বা branch — আর পরিবর্তন দেখতে range-diff.
অনুশীলন
Command-এর ক্রম, আউটপুট অনুমান, সিদ্ধান্তের সমালোচনা আর উদ্ধার — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।
-
--ontoআর replay-এর হিসাব: একটি repository-র commit graph (তীর parent-এর দিকে):A ← B ← C, যেখানেmainআছে C-তে;B ← D ← E, যেখানেtopicআছে E-তে;E ← F ← G, যেখানেfixআছে G-তে। (ক)fix-এ দাঁড়িয়েgit rebase mainদিলে কোন কোন commit replay হবে, কয়টি নতুন ID, আরtopicকোথায় থাকবে? (খ) শুধু F আর G-কেmain-এর ওপর নিতে কোন command? (গ) দুই branch একসাথেmain-এর ওপর নিতে,topic-সহ? (ঘ) শুরুর অবস্থা থেকে শুধু (খ) চালালে কোনো branch থেকে পৌঁছানো যায় এমন commit কয়টি, আর কোনগুলো শুধু reflog-এ?(ক)
main(C) আরfix(G)-এর merge base B. B-র পরেfix-এ আছে কিন্তুmain-এ নেই: D, E, F, G — ৪টি replay, ৪টি নতুন ID (D′, E′, F′, G′, C-র ওপর)।topicনামটি পুরোনো E-তেই থাকে — এখন D, E দুই জায়গায় দুই ID-তে।(খ)
git rebase --onto main topic fix— পরিসরtopic..fix= F, G; ২টি replay, C-র ওপর F′ ← G′. D আর Emain-এ যায় না।(গ)
git switch fix, তারপরgit rebase --update-refs main(Git ২.৩৮+). D′, E′, F′, G′ তৈরি হয়, আরtopicনিজে থেকে E′-এ সরে। পুরোনো Git-এ: আগেtopic-কেgit rebase main, তারপরgit rebase --onto topic E fix(E-এর আসল hash দিয়ে)।(ঘ) Branch:
main= C,topic= E,fix= G′. পৌঁছানো যায়: A, B, C, D, E, F′, G′ = ৭টি. পুরোনো F আর G (২টি) শুধু reflog আরORIG_HEADথেকে পাওয়া যায়, gc-র আগ পর্যন্ত। -
আউটপুট অনুমান — todo list-এর পরে history:
feature/marks-এmain-এর পরে ছয়টি commit, পুরোনো থেকে নতুন: p1 “Add marks table”, p2 “WIP”, p3 “Add grade column”, p4 “fixup! Add marks table”, p5 “Add CSV export”, p6 “Fix grade colum typo”. প্রতিটি commit আলাদা ফাইলে কাজ করে, তবে p4 p1-এরmarks.txtবদলায় আর p6 p3-এরgrade.txtবদলায়।git rebase -i main-এ todo list করা হলো:pick p1/fixup p4/pick p5/pick p3/squash p6/drop p2, আর squash-এর editor-এ কিছু না বদলে সেভ করা হলো।git log --oneline main..কী দেখাবে (নতুন থেকে পুরোনো)? কোনো পুরোনো ID কি টিকে থাকবে? শেষ commit-এর পূর্ণ বার্তা কী?তিনটি commit, নতুন থেকে পুরোনো:
<id3> Add grade column <id2> Add CSV export <id1> Add marks tableকারণ: p1 + p4 (fixup) → “Add marks table”, বার্তা p1-এর,
marks.txt-এ p4-এর সংশোধিত রূপ। তারপর p5 — ক্রম বদলানো হয়েছে বলে CSV export দ্বিতীয়। তারপর p3 + p6 (squash) → প্রথম লাইন “Add grade column”. p2 বাদ, তাইwip.txtকোথাও নেই।পুরোনো ID: একটিও টেকে না। p1 প্রথমে pick হলেও পরের লাইনের fixup তার বিষয়বস্তু বদলায়, তাই নতুন ID; বাকিগুলোর parent বদলেছে। (তালিকার প্রথম কয়েকটি commit যদি একেবারে অপরিবর্তিত pick হতো, Git সেগুলোর ID রেখে দিত।)
শেষ commit-এর বার্তা: editor-এ কিছু না বদলালে
#-লাইন বাদ দিয়ে দুই বার্তা থাকে:Add grade column Fix grade colum typo(এই লেখক একটি পরীক্ষার repository-তে Git ২.৫৩-এ ঠিক এই তালিকা চালিয়ে ফল মিলিয়েছেন।) ভালো অভ্যাস হলো squash-এর editor-এ দ্বিতীয় বার্তাটি মুছে দেওয়া বা একটি অর্থপূর্ণ বার্তা লেখা — “Fix … typo” তথ্যটি এখন আর কোনো কাজে লাগে না।
-
সমালোচনা — এখানে rebase না merge? প্রতিটি পরিস্থিতিতে আপনার সিদ্ধান্ত আর কারণ লিখুন: (ক) নিজের local branch, এখনো push হয়নি,
mainএগিয়ে গেছে। (খ) চারজন যে shareddevelopbranch-এ push করেন, একজন সেটি rebase করে force push করতে চান “history সুন্দর করতে”. (গ) শুধু আপনার একটি খোলা pull request; reviewer তিনটি ছোট ঠিক করতে বলেছেন। (ঘ) একটি দীর্ঘজীবী release branch-এর hotfixmain-এ আনা। (ঙ) দলের নতুন নিয়ম প্রস্তাব: “সবসময় rebase, কখনো merge নয় —main-এও।”(ক) Rebase —
git rebase main(বাgit pull --rebase). Commit-গুলো আর কেউ দেখেননি, তাই নতুন ID-তে ক্ষতি নেই; ইতিহাস সোজা থাকে।(খ) না। এটি সোনালি নিয়মের সরাসরি লঙ্ঘন: বাকি তিনজনের কাছে পুরোনো commit, পরের pull-এ duplicate commit আর conflict. ইতিহাস খারাপ লাগলে ভবিষ্যতের জন্য নিয়ম বদলান (যেমন PR-এ squash merge); পুরোনোটি যেমন আছে থাকুক। ভুল কিছু ফেরাতে হলে
git revert.(গ) Rebase চলে, সাবধানে।
git commit --fixup=<commit>দিয়ে তিনটি সংশোধন, তারপরgit rebase -i --autosquash main, শেষেgit push --force-with-lease. তবে দলের নিয়ম দেখুন — কেউ review চলাকালে force push না করে fixup commit রেখে দিতে বলেন, যাতে reviewer শুধু নতুন অংশ দেখতে পান; merge-এর আগে গোছানো হয়।(ঘ) Merge. Release branch অনেকে ব্যবহার করেন, আর কোন hotfix কোন release থেকে এলো, তা সত্যি ইতিহাসে থাকা দরকার। Rebase করলে release branch-এর commit-এর ID বদলাত — tag-এর সাথে মিল থাকত না। (একটিমাত্র commit আনতে cherry-pick-ও চলে, পর্ব ৫৬।)
(ঙ) প্রস্তাবটি ভুল। “কখনো merge নয়” মানে কোনো এক সময়
mainনিজেই rebase হবে, আর তাতে সবার clone ভাঙবে। ভালো রূপ: “নিজের branch-এ rebase চলবে;mainকখনো rebase বা force push হবে না (protected branch); PR merge হবে rebase-merge বা squash-merge বোতামে।” — অর্থাৎ rebase লেখকের হাতিয়ার,main-এর নীতি নয়। -
উদ্ধারের ধাপ:
feature/report-এ interactive rebase করে ভুল করে “Add PDF export” commit-টি drop করে ফেলেছেন। এরপর নতুন একটি commit “Add footer” করেছেন। এখন বুঝলেন PDF export দরকার।git reset --hard ORIG_HEADদিলে কী হবে? নতুন commit না হারিয়ে PDF export ফেরানোর command-এর ক্রম লিখুন।reset --hard ORIG_HEADদিলে:commitORIG_HEAD বদলায় না, তাই এটি এখনো rebase-এর আগের tip দেখায়। Reset করলে branch সেখানে ফিরবে — PDF export আবার আসবে, কিন্তু “Add footer” আর rebase-এর সব গোছানো branch থেকে হারাবে (reflog-এ থাকবে)। তাই এটি এখানে ভুল হাতিয়ার।ভালো ক্রম:
git status # working tree clean? git reflog show feature/report # find the entry before "rebase (finish)" git log --oneline feature/report@{2} # the old tip; adjust the number # ... find the hash of "Add PDF export", say 4c1e2aa git branch rescue/pdf 4c1e2aa # name it so gc cannot remove it git cherry-pick 4c1e2aa # replay it on top of "Add footer"Reflog-এর সংখ্যা আপনার repository-তে ভিন্ন হতে পারে;
git reflog show-এর তালিকা দেখে ঠিক ঘরটি বাছুন। Cherry-pick (পর্ব ৫৬) ওই একটি commit-এর পরিবর্তন নতুন commit হিসেবে বর্তমান মাথার ওপর বসায়; conflict হলে rebase-এর মতোই সমাধান করেgit cherry-pick --continue. ক্রম গুরুত্বপূর্ণ হলে আবারgit rebase -iদিয়ে PDF export-কে সঠিক জায়গায় সরানো যায়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমConflict-সহ একটি rebase-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
feature-এ গিয়েgit rebase main- Git merge base খুঁজে replay করার commit-এর তালিকা বানায়
- HEAD
main-এর মাথায় detached, commit একে একে replay - একটি commit-এ conflict — Git থামে
- ফাইল ঠিক করে
git add, তারপরgit rebase --continue featureনতুন tip-এ সরে, পুরোনো tipORIG_HEAD-এ
একটি repository-তে main: A ← B ← E, আর feature: B ← C ← D (B থেকে শাখা)। feature-এ দাঁড়িয়ে git rebase main দেওয়া হলো, কোনো conflict হয়নি। কোন বক্তব্যটি ঠিক?
- E-র ID বদলায়, C আর D একই থাকে
- C আর D-র পরিবর্তন নতুন ID-তে E-র ওপর বসে; পুরোনো C, D মোছে না, শুধু branch থেকে আর পৌঁছানো যায় না
- E আর D-কে parent করে একটি merge commit তৈরি হয়
- C আর D একই ID নিয়ে শুধু E-র পরে সরে যায়
git rebase main চলার সময় config.txt-এ conflict হলো। এ অবস্থায় git checkout --theirs config.txt ফাইলে কোন সংস্করণ বসাবে?
- main-এর সংস্করণ
- Merge base-এর সংস্করণ
- যে commit replay হচ্ছে, অর্থাৎ আপনার branch-এর commit-এর সংস্করণ
- Conflict marker-সহ দুই সংস্করণ একসাথে
Interactive rebase-এর todo list: pick X Add login / fixup Y Fix login typo / pick Z Add logout. সফলভাবে শেষ হলে নতুন ইতিহাসে কী থাকবে?
- দুটি commit: “Add login” (X আর Y-এর পরিবর্তন মিলিয়ে) আর “Add logout”
- তিনটি commit, বার্তা অপরিবর্তিত
- একটি commit, তিনটির পরিবর্তন মিলিয়ে
- দুটি commit: “Fix login typo” আর “Add logout”
Commit graph: main C-তে (A ← B ← C); topic E-তে (B ← D ← E); fix G-তে (E ← F ← G). git rebase --onto main topic fix কোন commit-গুলো replay করবে?
- D, E, F, G
- C, F, G
- শুধু G
- F আর G, C-র ওপর
আপনার নিজের pull request branch, যাতে শুধু আপনি push করেন, main-এর ওপর rebase করলেন। সাধারণ git push “non-fast-forward” বলে প্রত্যাখ্যাত। সবচেয়ে ভালো পরের কাজ কোনটি?
- Git-এর hint মেনে
git pull, তারপর push git push --force-with-leasegit push --force- একই commit নতুন একটি branch নামে push করা
একটি rebase শেষ হওয়ার ঠিক পরে বুঝলেন ফল ভুল হয়েছে, আর এর মধ্যে আর কোনো command চালাননি। Branch-কে rebase-এর আগের অবস্থায় ফেরানোর সরাসরি উপায় কোনটি?
git rebase --abortgit revert HEADgit reset --hard ORIG_HEADgit gc
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Rebase = replay: নিজের commit-গুলো নতুন base-এর ওপর আবার বসানো; পরিবর্তন একই, parent নতুন, তাই ID নতুন.
- পুরোনো commit মোছে না:
ORIG_HEAD,<branch>@{1}আর reflog দিয়ে ফেরা যায়, gc-র আগ পর্যন্ত। - Conflict প্রতি commit-এ:
add+--continue,--skip,--abort; rebase-এ ours = নতুন base, theirs = আপনার commit. rebase -i-এর todo list: pick, reword, edit, squash, fixup, drop, exec, লাইনের ক্রম = commit-এর ক্রম;--fixup+--autosquash;--ontoআর--update-refs.- সোনালি নিয়ম: অন্যরা যে commit ব্যবহার করেন তা rebase নয়; নিজের branch-এ rebase করলে push
--force-with-leaseদিয়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৬ · Pull request আর fork — দলের কাজের ধারা, review আর merge-এর নিয়মপরবর্তী পর্বSquash-merge, rebase-merge আর review-এর পুরো ধারা।
- পর্ব ১৩ · Merge আর conflictআগের ভিতThree-way merge, conflict marker আর merge base — rebase-এর প্রতিটি replay এর ওপর দাঁড়িয়ে।
- পর্ব ১১ · ভুল শোধরানো — restore, reset, revert, amend আর reflogউদ্ধারReset-এর তিন রূপ, ORIG_HEAD আর reflog বিস্তারিত।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- DSA · Binary searchপাশের ট্র্যাক
git bisect-এর পেছনের অ্যালগরিদম — কেন log₂ n ধাপ। - System Design · Deployment আর releaseগভীরে যানপরিষ্কার ইতিহাস থেকে নিরাপদ release পর্যন্ত।