এই পর্বে যা শিখবেন
- Remote,
origin, bare repository আরgit cloneঠিক কী কী তৈরি করে - Remote-tracking branch, upstream config, আর
git status-এর ahead/behind সংখ্যা কীভাবে হিসাব হয় fetch,pull(merge, rebase, ff-only) আরpush-এর পার্থক্য — কোনটি কী বদলায়- Rejected push কেন হয় ও কীভাবে সামলাবেন;
--force,--force-with-lease,--force-if-includes; remote branch মোছা আর prune - HTTPS (credential helper, token) বনাম SSH (key pair) — আর remote add/rename/set-url/remove, fork-এর
upstream
১ · মডিউল ৩ শুরু — নিজের খাতা থেকে দলের খাতায়
মডিউল ২-এ Git ছিল শুধু আপনার কম্পিউটারে: commit (পর্ব ৮ — প্রথম Git repository), history (পর্ব ৯), ভুল শোধরানো (পর্ব ১১ — ভুল শোধরানো), branch (পর্ব ১২ — Branch) আর merge (পর্ব ১৩ — Merge আর conflict). কিন্তু software প্রায় কখনো একা বানানো হয় না। দুজন, দশজন বা হাজারজন মানুষ একই code-এ কাজ করেন। তাঁদের commit এক জায়গায় আনার ব্যবস্থার নামই remote.
পাড়ার একটি মুদি দোকানের কথা ভাবুন। দোকানে একটি মূল হিসাবের খাতা থাকে। দুজন কর্মচারী — রহিম আর নাসরিন — প্রত্যেকে নিজের একটি কপি খাতা রাখেন, কারণ সারাদিন মূল খাতা নিয়ে টানাটানি করা যায় না। প্রত্যেকে নিজের খাতায় দিনের হিসাব লেখেন। প্রত্যেকের খাতার শেষ পাতায় একটি ছোট নোটও থাকে: “আজ সকালে মূল খাতায় শেষ লাইন ছিল ৪২ নম্বর।” এই নোট পুরোনো হতে পারে — দুপুরে নাসরিন মূল খাতায় নতুন লাইন তুললে রহিমের নোট সেটা জানে না, যতক্ষণ না রহিম নিজে গিয়ে আবার মূল খাতা দেখে আসেন। Git-এ মূল খাতা হলো remote repository, নিজের কপি হলো local repository, আর শেষ পাতার নোটটিই origin/main.
এই মডিউলে ধাপে ধাপে দলের কাজের পুরো ছবি আসবে। আজ ভিত্তি: commit কীভাবে এক repository থেকে আরেকটিতে যায়। তারপর পর্ব ১৫-এ (Rebase) history নতুন করে সাজানো, পর্ব ১৬-এ (Pull request আর fork) review-সহ দলের কাজের ধারা, পর্ব ১৭-এ (Branching strategy) Git Flow বনাম trunk-based, পর্ব ১৮-এ (ভালো commit, tag আর release) version আর release, আর পর্ব ১৯-এ (বড় repository) monorepo ও submodule.
Remote আসলে কী — একটি নাম, একটি ঠিকানা
পর্ব ৭-এ (Version control কী) দেখেছেন, Git একটি distributed version control ব্যবস্থা: প্রতিটি কপিতে পুরো history থাকে। তাই Git-এর চোখে “server” বলে বিশেষ কিছু নেই — সেটিও আরেকটি repository মাত্র। দল শুধু সিদ্ধান্ত নেয় যে একটি নির্দিষ্ট কপিকে সবাই “মূল খাতা” হিসেবে মানবে। সাধারণত সেটি থাকে কোনো hosting platform-এ, অথবা প্রতিষ্ঠানের নিজের server-এ।
আপনার repository-তে একটি remote হলো শুধু দুটি জিনিসের জোড়া: একটি ছোট নাম (যেমন origin) আর একটি ঠিকানা (URL বা path), যেমন https://github.com/example/bazar-app.git. নামটি থাকে যাতে প্রতিবার লম্বা ঠিকানা লিখতে না হয়। একটি repository-র একাধিক remote থাকতে পারে। Clone করলে Git নিজে থেকেই প্রথম remote-এর নাম দেয় origin — এটা শুধুই প্রথা, কোনো জাদু নেই। চাইলে git clone -o server … দিয়ে অন্য নাম দেওয়া যায়।
Remote-এর সাথে কাজ মাত্র চারটি command-এ: git clone (পুরো কপি আনা), git fetch (নতুন commit আনা, নিজের কাজ না ছুঁয়ে), git pull (আনা আর নিজের branch-এ মেশানো) আর git push (নিজের commit পাঠানো)। বাকি সব এদের খুঁটিনাটি।
এই পর্বের সব Git আউটপুট আসল — Git ২.৫৩ (Windows) দিয়ে একটি খালি পরীক্ষার folder-এ চালানো। “Server” হিসেবে কোনো আসল website নয়, একই কম্পিউটারের একটি bare repository (working tree ছাড়া শুধু .git-এর ভেতরের জিনিস) server/bazar.git; তার পাশে দুটি working কপি — rahim/ আর nasrin/, দুজন কাল্পনিক developer. Commit hash যেন আপনার কাছেও একই আসে, তাই নাম “Rahim” / “Nasrin”, email rahim@example.com / nasrin@example.com, init.defaultBranch=main, core.autocrlf=false, আর প্রতিটি commit-এর সময় GIT_AUTHOR_DATE ও GIT_COMMITTER_DATE দিয়ে ২০২৬-০১-২০ সকাল ১০:০০ (+06:00) থেকে এক মিনিট করে বাড়িয়ে ঠিক করা। Global config ফাঁকা রাখা হয়েছে, যাতে Git-এর default আচরণ দেখা যায়। পরীক্ষার folder-এর লম্বা path আউটপুটে C:/…/ দিয়ে ছোট করা হয়েছে। গল্পটি একটি কাল্পনিক অনলাইন বাজার অ্যাপের config ফাইল নিয়ে।
২ · প্রথম remote আর clone — bare repository, remote add, push আর git clone
রহিম আগে নিজের কম্পিউটারে project শুরু করেছেন — দুটি commit আছে। এখন দলের জন্য একটি “মূল খাতা” বানিয়ে সেখানে কাজ পাঠাবেন:
$ git init --bare server/bazar.git
Initialized empty Git repository in C:/…/t14/server/bazar.git/
$ cd rahim
$ git log --oneline
c4f7d9c Add README
be5e7dd Add shop config
$ git remote add origin ../server/bazar.git
$ git remote -v
origin ../server/bazar.git (fetch)
origin ../server/bazar.git (push)
$ git push -u origin main
To ../server/bazar.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
$ git branch -vv
* main c4f7d9c [origin/main] Add README
$ git config --get-regexp "^(remote|branch)\."
remote.origin.url ../server/bazar.git
remote.origin.fetch +refs/heads/*:refs/remotes/origin/*
branch.main.remote origin
branch.main.merge refs/heads/main
$ git ls-remote origin
c4f7d9cd9552be8b41fd33b713fd24641db0340d HEAD
c4f7d9cd9552be8b41fd33b713fd24641db0340d refs/heads/maingit init --bare এমন repository বানায় যাতে কেউ সরাসরি ফাইল সম্পাদনা করবে না — শুধু push আর fetch হবে। Server-এর repository সাধারণত bare হয়, নামের শেষে .git রাখার প্রথা আছে। git remote add origin ../server/bazar.git কিছুই পাঠায় না, শুধু config-এ নাম আর ঠিকানা লেখে। git remote -v দেখাচ্ছে একই ঠিকানা দুবার — fetch-এর জন্য আর push-এর জন্য (চাইলে দুটি আলাদা রাখা যায়)।
git push -u origin main: “origin-এ আমার main পাঠানো হোক।” Server-এ main ছিল না, তাই * [new branch] main -> main. শেষ লাইন branch 'main' set up to track 'origin/main'. এসেছে -u-এর কারণে: এখন থেকে local main-এর upstream branch হলো origin/main. তাই git branch -vv বন্ধনীর ভেতরে [origin/main] দেখাচ্ছে।
Config-এর চার লাইনই remote-এর পুরো রহস্য। remote.origin.url ঠিকানা; remote.origin.fetch একটি refspec: +refs/heads/*:refs/remotes/origin/* মানে “remote-এর প্রতিটি branch (refs/heads/…) আমার এখানে refs/remotes/origin/… নামে রাখা হোক”; শুরুর + মানে remote-এ history পুনর্লিখিত হলেও এই স্মৃতি জোর করে হালনাগাদ করা চলবে। branch.main.remote আর branch.main.merge মিলে upstream: “main-এর জোড়া হলো origin-এর refs/heads/main.” শেষে git ls-remote সরাসরি remote-কে জিজ্ঞেস করে তার ref-গুলো কোথায় — কিছু download না করে। Server-এর main এখন রহিমের c4f7d9c-এ।
এখানে ঠিকানাটি একটি আপেক্ষিক path (../server/bazar.git), যা repository-র folder থেকে ধরা হয়। পরীক্ষার জন্য সুবিধাজনক, কিন্তু folder সরালে ভেঙে যায়। বাস্তবে ঠিকানা হয় একটি URL — https://… বা git@… — যা অংশ ৯-এ দেখবেন।
নতুন সদস্য: git clone — এক command-এ কী কী তৈরি হয়
এবার নাসরিন দলে যোগ দিলেন। তাঁর কম্পিউটারে কিছুই নেই; তিনি মূল খাতার একটি পূর্ণ কপি নেবেন:
$ git clone server/bazar.git nasrin
Cloning into 'nasrin'...
done.
$ cd nasrin
$ git remote -v
origin C:/…/t14/server/bazar.git (fetch)
origin C:/…/t14/server/bazar.git (push)
$ git remote set-url origin ../server/bazar.git
$ git remote -v
origin ../server/bazar.git (fetch)
origin ../server/bazar.git (push)
$ git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
$ git branch -vv
* main c4f7d9c [origin/main] Add README
$ git log --oneline --decorate --all
c4f7d9c (HEAD -> main, origin/main, origin/HEAD) Add README
be5e7dd Add shop config
$ git config --get-regexp "^(remote|branch)\."
remote.origin.url ../server/bazar.git
remote.origin.fetch +refs/heads/*:refs/remotes/origin/*
branch.main.remote origin
branch.main.merge refs/heads/mainএকটি git clone আসলে কয়েকটি কাজ একসাথে করে:
- নতুন folder
nasrin/বানিয়ে ভেতরেgit init. originনামে remote যোগ, সাথে default refspec — উপরের config-এ একই চার লাইন।- Remote-এর সব commit আর object নামানো (একটি fetch), আর প্রতিটি remote branch-এর জন্য একটি remote-tracking branch: এখানে
origin/main. সাথেorigin/HEAD -> origin/main, যা মনে রাখে remote-এর default branch কোনটি। - Remote-এর default branch-এর নামে একটি local branch
mainবানানো, যাorigin/main-কে track করে (branch.main.*), আর সেটি working tree-তে checkout করা।
Local path থেকে clone করলে Git ঠিকানাটি পূর্ণ (absolute) path করে লেখে — তাই প্রথম remote -v-তে লম্বা path. নাসরিন git remote set-url দিয়ে সেটি রহিমের মতো আপেক্ষিক করে নিলেন; set-url শুধু ঠিকানা বদলায়, আর কিছু নয়। git branch -a local আর remote-tracking দুই ধরনের branch-ই দেখায়; remote-tracking-গুলোর পূর্ণ নাম refs/remotes/origin/main, ছোট করে origin/main. লক্ষ্য করুন, clone-এর পরে নাসরিনের কাছে local branch মাত্র একটি — remote-এ আরও branch থাকলে সেগুলো শুধু origin/… হিসেবে আসত; দরকার হলে git switch feature-x দিলে Git নিজেই origin/feature-x থেকে একটি tracking branch বানিয়ে দেয় (অংশ ৮-এ দেখবেন)।
এবার নিচের stepper-এ দেখুন clone থেকে প্রথম push পর্যন্ত কোন ref কখন সরে। মানগুলো উপরের আর পরের অংশের আসল transcript থেকে নেওয়া।
| server-এ main | |
| নাসরিনের main | |
| নাসরিনের origin/main | |
| নাসরিনের status | |
| রহিমের origin/main |
- Clone-এর ঠিক পরে। নাসরিনের
main, তাঁরorigin/mainআর server-এরmain— তিনটিইc4f7d9c.git statusবলবে “up to date with 'origin/main'”. - নাসরিন একটি commit করলেন (
ec6020e, ডেলিভারি চার্জ ৬০ থেকে ৫০)। শুধু তাঁর localmainসরল।origin/mainআগের জায়গাতেই, তাই status: “ahead 1” — তাঁর কাছে এমন একটি commit আছে যা remote-এর জানা অবস্থায় নেই। git push. Commit-টি server-এ গেল, server-এরmainসরেec6020e. সফল push-এর পরে Git নাসরিনেরorigin/main-ও হালনাগাদ করে, কারণ এখন সে জানে remote কোথায়। আবার “up to date”.- রহিমের কম্পিউটারে কী জানা? কিছুই বদলায়নি। রহিমের
origin/mainএখনোc4f7d9c— server যে সরেছে, সে খবর তাঁর কাছে আসেনি। Git নিজে থেকে কখনো network-এ খোঁজ নেয় না; রহিমgit fetchনা দেওয়া পর্যন্ত তাঁর “শেষ পাতার নোট” পুরোনোই থাকবে।
৩ · Remote-tracking branch আর git fetch — নিরাপদে খবর আনা
origin/main দেখতে branch-এর মতো, কিন্তু আচরণ আলাদা। এটি একটি ref, যা থাকে refs/remotes/origin/-এর নিচে, আর এর কাজ একটাই: শেষবার যোগাযোগের সময় remote-এর main কোন commit-এ ছিল, তা মনে রাখা। একে read-only বুকমার্ক ভাবুন:
- এটি সরে শুধু তখন, যখন Git remote-এর সাথে কথা বলে —
git fetch,git pull(যার ভেতরে fetch আছে) বা সফলgit push. - আপনি এতে commit করতে পারেন না।
git switch origin/mainদিলে Git সরাসরি রাজি হয় না, আরgit switch --detach origin/mainবাgit checkout origin/mainদিলে আপনি detached HEAD-এ চলে যান (পর্ব ১২) — সেখানে commit করলেওorigin/mainনড়ে না। - এটি পুরোনো হতে পারে। “Your branch is up to date with 'origin/main'” মানে শুধু “শেষ fetch-এর সময়ের তুলনায়” — remote এর মধ্যে বদলে থাকতে পারে।
git fetch remote-কে জিজ্ঞেস করে তার ref-গুলো কোথায়, যেসব commit আর object আপনার কাছে নেই সেগুলো নামায়, তারপর remote-tracking ref সরায়। ব্যস। আপনার local branch, staging area আর working tree একটুও বদলায় না। তাই fetch যেকোনো সময় নির্ভয়ে চালানো যায় — এমনকি কাজের মাঝখানে, commit না করা বদল থাকলেও।
তিনটি জায়গা, চারটি তীর
Remote repository, your remote-tracking refs and your local branch
নিচের stepper-এ একটি আসল মুহূর্ত দেখুন: রহিম 41edf81 push করেছেন (অংশ ৫-৬-এ সেই গল্প), নাসরিনের কাছে তখনো আগের ec6020e. নাসরিন প্রথমে fetch দেন, তারপর মেশান।
| object database-এ 41edf81 | |
| origin/main | |
| main (HEAD) | |
| working tree-র README.md | |
| git status |
- আগে। Server-এ
main=41edf81, কিন্তু নাসরিন জানেন না। তাঁরgit statusবলবে “up to date with 'origin/main'” — কথাটি সত্য, তবে পুরোনো খবরের ভিত্তিতে। git fetch. নতুন commit-এর object নেমে এল,origin/mainসরে41edf81. নাসরিনেরmain, index আর working tree অপরিবর্তিত — README.md এখনো পুরোনো। এবার status বলে “behind 1, and can be fast-forwarded”.git merge(বাgit pull --ff-only-এর দ্বিতীয় অর্ধেক).mainfast-forward হয়ে41edf81-এ গেল; Git working tree আর index নতুন commit-এর মতো করে দিল — README.md এখন ৩ লাইনের।- সারকথা।
git pull= ধাপ ২ + ধাপ ৩, এক command-এ। আলাদা করে fetch দিলে মাঝখানে থেমেgit log main..origin/mainদিয়ে দেখে নেওয়া যায় কী আসছে — তারপর সিদ্ধান্ত নেওয়া যায় merge, না rebase.
fetch-এর পরে দেখার দুটি কাজের command: git log --oneline main..origin/main — remote-এ আছে কিন্তু আপনার main-এ নেই এমন commit (যা আসবে); আর git log --oneline origin/main..main — আপনার কাছে আছে কিন্তু remote-এ নেই (যা যাবে)। দুই বিন্দুর অর্থ পর্ব ৯-এ (History আর পার্থক্য দেখা) দেখেছেন।
৪ · Ahead আর behind — upstream থেকে কত commit এগিয়ে, কত পিছিয়ে
প্রতিটি tracking branch-এর জন্য Git দুটি সংখ্যা দেখায়। এগুলো কীভাবে আসে, তা জানলে git status-এর প্রতিটি বার্তা নিজেই পড়তে পারবেন।
ধরুন R(x) = commit x আর parent ধরে ধরে পেছনে গিয়ে যত commit-এ পৌঁছানো যায় (reachable), সবগুলোর set. Local branch-এর tip L, upstream-এর tip U হলে:
ahead = |R(L) − R(U)| — L থেকে পৌঁছানো যায়, U থেকে যায় না;
behind = |R(U) − R(L)| — U থেকে পৌঁছানো যায়, L থেকে যায় না।
Git-এ এটিই git rev-list --left-right --count main...origin/main.
এর থেকেই চারটি অবস্থা পড়া যায়, আর প্রতিটির জন্য Git-এর বার্তা আলাদা:
| ahead, behind | git status (Git ২.৫৩) | অর্থ ও পরের কাজ |
|---|---|---|
| 0, 0 | Your branch is up to date with 'origin/main'. | শেষ fetch-এর হিসাবে সমান |
| a > 0, 0 | Your branch is ahead of 'origin/main' by 1 commit. | push করা যায় (যদি এর মধ্যে remote না সরে থাকে) |
| 0, b > 0 | …behind 'origin/main' by 1 commit, and can be fast-forwarded. | git pull --ff-only বা merge — fast-forward |
| a > 0, b > 0 | …have diverged, and have 1 and 1 different commits each | merge বা rebase দরকার, তারপর push |
সংক্ষিপ্ত রূপেও একই তথ্য: git status -sb প্রথম লাইনে ## main...origin/main [ahead 1, behind 1] দেখায়, আর git branch -vv প্রতিটি branch-এর পাশে [origin/main: behind 4]. নিচের প্রোগ্রামটি এই গল্পের আসল commit graph (আসল ছোট hash) নিয়ে সংজ্ঞা অনুযায়ী দুটি set-এর পার্থক্য গুনছে। প্রতিটি সারির সংখ্যা এই পর্বের কোনো একটি আসল git status-এর সাথে মেলে।
// Ahead/behind by reachability: the commit graph of the Bazar Mart story (real hashes).
const parents = {
be5e7dd: [], c4f7d9c: ["be5e7dd"], ec6020e: ["c4f7d9c"],
e743a3a: ["c4f7d9c"], // Rahim's commit before the pull
"23d6d2c": ["e743a3a", "ec6020e"], // the merge made by git pull --no-rebase
"41edf81": ["ec6020e"], // the same work after git pull --rebase
e90c3b7: ["41edf81"], "4218834": ["e90c3b7"], "11fa2d8": ["41edf81"],
a1f5d64: ["4218834"], "93710ae": ["41edf81", "a1f5d64"]
};
function reach(c) { // c and everything reachable through parents
const seen = new Set([c]), stack = [c];
while (stack.length)
for (const p of parents[stack.pop()])
if (!seen.has(p)) { seen.add(p); stack.push(p); }
return seen;
}
function aheadBehind(local, remote) { // like git rev-list --left-right --count local...remote
const L = reach(local), R = reach(remote);
const ahead = [...L].filter(c => !R.has(c)), behind = [...R].filter(c => !L.has(c));
return { ahead, behind };
}
const cases = [
["e743a3a", "ec6020e", "Rahim after fetch"],
["23d6d2c", "ec6020e", "Rahim after pull --no-rebase"],
["41edf81", "93710ae", "Nasrin after fetch --prune"],
["11fa2d8", "e90c3b7", "Rahim after amend, stale"],
["11fa2d8", "4218834", "Rahim after amend + fetch"]
];
for (const [l, r, label] of cases) {
const { ahead, behind } = aheadBehind(l, r);
console.log(label.padEnd(29) + ahead.length + "\t" + behind.length +
" ahead: " + (ahead.join(" ") || "-") + " | behind: " + (behind.join(" ") || "-"));
}আউটপুট:
Rahim after fetch 1 1 ahead: e743a3a | behind: ec6020e
Rahim after pull --no-rebase 2 0 ahead: 23d6d2c e743a3a | behind: -
Nasrin after fetch --prune 0 4 ahead: - | behind: 93710ae a1f5d64 4218834 e90c3b7
Rahim after amend, stale 1 1 ahead: 11fa2d8 | behind: e90c3b7
Rahim after amend + fetch 1 2 ahead: 11fa2d8 | behind: 4218834 e90c3b7
প্রথম সারি হুবহু অংশ ৫-এর git rev-list --left-right --count main...origin/main-এর 1 1. দ্বিতীয় সারি: merge commit নিজেও একটি commit, তাই pull-এর পরে “ahead 2” — merge commit 23d6d2c আর রহিমের নিজের e743a3a; behind শূন্য, কারণ merge commit-এর দ্বিতীয় parent হিসেবে ec6020e এখন রহিমের history-তে। তৃতীয় সারি: নাসরিন চারটি commit পিছিয়ে, যার একটি merge commit (93710ae). শেষ দুই সারি force push-এর গল্প (অংশ ৮): amend করার পরে রহিমের commit 11fa2d8-এর parent 41edf81, তাই পুরোনো e90c3b7 আর তাঁর branch-এ নেই — সেটি “behind”-এ গোনা হচ্ছে, যদিও কাজটি তাঁরই।
এই সংখ্যাগুলো সবসময় local origin/main-এর সাথে তুলনা — remote-এর সাথে সরাসরি নয়। পরের অংশে রহিমের status বলবে “ahead of 'origin/main' by 1 commit”, অথচ ঠিক তখনই server-এ নাসরিনের একটি নতুন commit বসে আছে। আসল অবস্থা জানতে আগে git fetch, তারপর status.
৫ · সহকর্মী আগে push করলেন — rejected push আর তার কারণ
দলের কাজে সবচেয়ে পরিচিত দৃশ্য এটি। নাসরিন ডেলিভারি চার্জ কমিয়ে push করলেন। একই সময়ে রহিম, কোনো fetch না দিয়ে, README-তে একটি লাইন যোগ করে push করতে গেলেন:
# --- Nasrin ---
# (edit shop.conf: delivery_fee = 60 -> 50; git add shop.conf)
$ git commit -m "Lower delivery fee to 50"
[main ec6020e] Lower delivery fee to 50
1 file changed, 1 insertion(+), 1 deletion(-)
$ git status -sb
## main...origin/main [ahead 1]
$ git push
To ../server/bazar.git
c4f7d9c..ec6020e main -> main
# --- Rahim (has not fetched) ---
# (add two lines to README.md; git add README.md)
$ git commit -m "Describe the shop in README"
[main e743a3a] Describe the shop in README
1 file changed, 2 insertions(+)
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
$ git push
To ../server/bazar.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '../server/bazar.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 fetch
From ../server/bazar
c4f7d9c..ec6020e main -> origin/main
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree clean
$ git rev-list --left-right --count main...origin/main
1 1
$ git log --oneline --graph --all
* e743a3a Describe the shop in README
| * ec6020e Lower delivery fee to 50
|/
* c4f7d9c Add README
* be5e7dd Add shop configনাসরিনের push-এর লাইন c4f7d9c..ec6020e main -> main মানে server-এর main পুরোনো থেকে নতুন commit-এ fast-forward হলো। রহিমের git status তখনও আত্মবিশ্বাসী — “ahead by 1 commit” — কারণ তাঁর origin/main পুরোনো। Push করতেই server জানাল ! [rejected] … (fetch first): “আমার কাছে এমন কাজ আছে যা আপনার কাছে নেই।” কিছুই বদলায়নি — server, রহিমের branch, কিছুই না।
git fetch-এর লাইন c4f7d9c..ec6020e main -> origin/main — লক্ষ্য করুন ডানে এবার origin/main, main নয়। এখন আসল ছবি: দুই পথ আলাদা হয়ে গেছে (diverged), ১টি করে commit দুই দিকে, graph-এ দুটি শাখা। (fetch-এর From ../server/bazar-এ শেষের .git Git নিজেই ছোট করে দেখায়।)
কেন Git push প্রত্যাখ্যান করল — non-fast-forward
Push-এর অনুরোধ আসলে server-কে বলা: “আপনার main-কে e743a3a-এ সরানো হোক।” Server-এর main তখন ec6020e-এ, আর ec6020e রহিমের commit-এর ancestor নয়। অনুরোধ মানলে ec6020e server-এর কোনো branch থেকে আর পৌঁছানো যেত না — নাসরিনের কাজ নীরবে “হারিয়ে” যেত। তাই নিয়ম:
না হলে non-fast-forward → rejected
এটি ঠিক পর্ব ১৩-এর fast-forward-এর শর্ত, শুধু দিক উল্টো: এবার server-এর ref সামনে সরানো যায় কি না, সেটি প্রশ্ন। Rejected বার্তার বন্ধনীতে দুই রকম কারণ দেখতে পারেন: (fetch first) — server-এ এমন commit আছে যা আপনার কাছে নেই-ই; আর (non-fast-forward) — commit-টি আপনার কাছে আছে (যেমন fetch করেছেন, কিন্তু merge করেননি), তবু আপনার tip সেটির উত্তরসূরি নয়।
নিচের প্রোগ্রামটি দুটি clone আর একটি shared remote-এর ছোট মডেল: commit, fetch, push (fast-forward না হলে reject), আর pull (fetch + merge). প্রতিটি ধাপের পরে প্রতিটি repository-র main/origin-main [ahead behind] ছাপে।
// Two clones and one shared remote. All commits live in one shared store here;
// real Git copies the objects during fetch and push.
const parents = { A: [] }, server = { main: "A" }; // commit -> its parents
const mk = () => ({ main: "A", "origin/main": "A" }), repos = { rahim: mk(), nasrin: mk() };
const anc = c => { const s = new Set(), q = [c]; // c and all its ancestors
while (q.length) { const x = q.pop(); if (!s.has(x)) { s.add(x); q.push(...parents[x]); } }
return s; };
const count = (a, b) => [...anc(a)].filter(c => !anc(b).has(c)).length;
function commit(r, id) { parents[id] = [repos[r].main]; repos[r].main = id; }
function fetchOrigin(r) { repos[r]["origin/main"] = server.main; }
function push(r) {
const me = repos[r];
if (!anc(me.main).has(server.main)) return "! [rejected] main -> main (fetch first)";
server.main = me.main; me["origin/main"] = me.main; return "ok, server main = " + me.main;
}
function pull(r, id) { // fetch + merge
fetchOrigin(r); const me = repos[r], t = me["origin/main"];
if (anc(me.main).has(t)) return "Already up to date";
if (anc(t).has(me.main)) { me.main = t; return "Fast-forward to " + t; }
parents[id] = [me.main, t]; me.main = id; return "Merge commit " + id;
}
function show(step, msg) { // main/origin-main [ahead behind]
let line = step.padEnd(20);
for (const [n, r] of Object.entries(repos)) {
const o = r["origin/main"];
line += n + " " + r.main + "/" + o + " [" + count(r.main, o) + " " + count(o, r.main) + "] ";
}
console.log(line + "server " + server.main + (msg ? " " + msg : ""));
}
commit("nasrin", "B"); show("1 nasrin commits B"); show("2 nasrin pushes", push("nasrin"));
commit("rahim", "C"); show("3 rahim commits C"); show("4 rahim pushes", push("rahim"));
fetchOrigin("rahim"); show("5 rahim fetches"); show("6 rahim pulls", pull("rahim", "M"));
show("7 rahim pushes", push("rahim"));
fetchOrigin("nasrin"); show("8 nasrin fetches"); show("9 nasrin pulls", pull("nasrin", "X"));আউটপুট:
1 nasrin commits B rahim A/A [0 0] nasrin B/A [1 0] server A
2 nasrin pushes rahim A/A [0 0] nasrin B/B [0 0] server B ok, server main = B
3 rahim commits C rahim C/A [1 0] nasrin B/B [0 0] server B
4 rahim pushes rahim C/A [1 0] nasrin B/B [0 0] server B ! [rejected] main -> main (fetch first)
5 rahim fetches rahim C/B [1 1] nasrin B/B [0 0] server B
6 rahim pulls rahim M/B [2 0] nasrin B/B [0 0] server B Merge commit M
7 rahim pushes rahim M/M [0 0] nasrin B/B [0 0] server M ok, server main = M
8 nasrin fetches rahim M/M [0 0] nasrin B/M [0 2] server M
9 nasrin pulls rahim M/M [0 0] nasrin M/M [0 0] server M Fast-forward to M
ধাপ ৩-৪ পুরো গল্পের মূল: রহিমের হিসাবে [1 0] (ahead 1), অথচ server ততক্ষণে B-তে — তাই reject. ধাপ ৫-এ fetch করতেই আসল ছবি [1 1]. ধাপ ৬-এ pull একটি merge commit M বানায় (দুই parent: C আর B), তাই [2 0] — আসল Git-এর “ahead 2”-এর সাথে মেলে। এবার M-এর ancestor-এর মধ্যে B আছে, তাই ধাপ ৭-এর push fast-forward. নাসরিন ধাপ ৮-এ দুই commit পিছিয়ে (C আর M), আর ধাপ ৯-এ তাঁর pull শুধু fast-forward — নতুন merge commit লাগেনি। মডেলে fetch শুধু একটি নাম সরায়; আসল Git সাথে অনুপস্থিত object-ও নামায়।
৬ · git pull — merge, rebase, নাকি শুধু fast-forward
git pull দুটি কাজ: প্রথমে upstream থেকে fetch, তারপর upstream-কে বর্তমান branch-এ মেশানো। দ্বিতীয় কাজটি তিনভাবে হতে পারে, আর দুই পথ আলাদা হয়ে গেলে Git ২.৫৩ আপনাকে বেছে নিতে বাধ্য করে। রহিমের অবস্থার একটি কপিতে প্রথমে কোনো option ছাড়া, তারপর merge-এর পথে:
$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint: git config pull.rebase false # merge
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.
$ git pull --no-rebase
Merge made by the 'ort' strategy.
shop.conf | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph -5
* 23d6d2c Merge branch 'main' of ../server/bazar
|\
| * ec6020e Lower delivery fee to 50
* | e743a3a Describe the shop in README
|/
* c4f7d9c Add README
* be5e7dd Add shop config
$ git status -sb
## main...origin/main [ahead 2]প্রথম git pull fetch করেছে ঠিকই (fetch-এর অংশে নতুন কিছু ছিল না, কারণ আগেই fetch হয়েছিল), কিন্তু মেশানোর আগে থেমেছে: fatal: Need to specify how to reconcile divergent branches. Hint-এ তিনটি পথ। --no-rebase মানে merge: পর্ব ১৩-এর three-way merge, ফল একটি merge commit 23d6d2c, বার্তা Merge branch 'main' of ../server/bazar. এখন “ahead 2”, push করলে fast-forward হবে।
আসল গল্পে রহিম merge-এর বদলে rebase বেছে নিলেন: নিজের commit-টি নাসরিনের commit-এর ওপরে নতুন করে বসানো, যাতে history একটি সরলরেখা থাকে।
$ git pull --rebase
Successfully rebased and updated refs/heads/main.
$ git log --oneline --graph --all
* 41edf81 Describe the shop in README
* ec6020e Lower delivery fee to 50
* c4f7d9c Add README
* be5e7dd Add shop config
$ git status -sb
## main...origin/main [ahead 1]
$ git push
To ../server/bazar.git
ec6020e..41edf81 main -> main
# --- Nasrin ---
$ git fetch
From ../server/bazar
ec6020e..41edf81 main -> origin/main
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
$ git pull --ff-only
Updating ec6020e..41edf81
Fast-forward
README.md | 2 ++
1 file changed, 2 insertions(+)রহিমের commit-এর hash বদলে e743a3a থেকে 41edf81 হলো — একই লেখা, কিন্তু নতুন parent (ec6020e), তাই নতুন commit (rebase নতুন commit বানায়, এটা পর্ব ১৫-এর বিষয়)। Graph এখন সোজা, “ahead 1”, আর push fast-forward. নাসরিনের দিকে fetch-এর পরে status “behind 1 … can be fast-forwarded”, আর git pull --ff-only শুধু pointer সরিয়েছে (Updating ec6020e..41edf81, Fast-forward). ff-only কখনো merge commit বানায় না; দুই পথ আলাদা হলে কিছু না করে থেমে যায়।
পুরো উদ্ধার-পথটি এক জায়গায় ধাপে ধাপে দেখুন — নিচের graph আর সারণির মান উপরের আসল transcript থেকে:
| server main | |
| রহিমের main | |
| রহিমের origin/main | |
| status | |
| command |
- শুরু। নাসরিনের
ec6020eইতিমধ্যে server-এ। রহিমের কাছে নিজেরe743a3a; তাঁরorigin/mainপুরোনোc4f7d9c, তাই status বলছে “ahead 1”. - Push প্রত্যাখ্যাত। Server-এর
ec6020eরহিমের commit-এর ancestor নয় — মানলে নাসরিনের কাজ হারাত।! [rejected] (fetch first); কিছুই বদলায়নি। git fetch. শুধু রহিমেরorigin/mainসরেec6020e; তাঁরmainআর ফাইল অপরিবর্তিত। এখন সত্যিকারের ছবি: ahead 1, behind 1 — diverged.- পথ ক — merge.
git pull --no-rebase(বাgit merge origin/main) merge commit23d6d2cবানায়, দুই parent. ahead 2, behind 0 — push এখন fast-forward হবে। History-তে শাখাটি থেকে যায়। - পথ খ — rebase. বদলে
git pull --rebaseরহিমের commit-কেec6020e-এর ওপর নতুন করে বসায়: নতুন hash41edf81, পুরোনোe743a3aআর branch-এ নেই। ahead 1, সরলরেখা। রহিম এই পথ বেছে নিলেন। - Push গৃহীত।
ec6020e..41edf81 main -> main— fast-forward. Server আর রহিমেরorigin/mainদুটোই41edf81. নাসরিন পরে fetch করলে “behind 1” দেখবেন।
কোনটি বেছে নেবেন, আর default কীভাবে ঠিক করবেন
| Option / config | দুই পথ আলাদা হলে | কখন ভালো |
|---|---|---|
--no-rebase / pull.rebase false | merge commit | Shared branch-এ, যখন history বদলানো যাবে না; সত্যিকারের ঘটনাক্রম রাখতে |
--rebase / pull.rebase true | নিজের অপ্রকাশিত commit upstream-এর ওপর নতুন করে বসে | নিজের ছোট, এখনো push না করা commit; সরলরেখার history |
--ff-only / pull.ff only | কিছু না করে থামে | যে branch-এ আপনি নিজে commit করেন না (যেমন local main শুধু হালনাগাদ রাখা) |
একটি repository-তে git config pull.rebase true, বা সব repository-র জন্য --global দিয়ে default ঠিক করা যায়। ইতিহাসটিও জানা দরকার, কারণ পুরোনো টিউটোরিয়াল আর AI প্রায়ই পুরোনো আচরণ বর্ণনা করে: অনেক বছর git pull কিছু না জিজ্ঞেস করেই merge করত। Git ২.২৭ (২০২০) থেকে pull.rebase সেট না থাকলে একটি সতর্কবার্তা দেখাত; পরের কয়েকটি সংস্করণে আচরণ ধাপে ধাপে বদলায় (থেমে যাওয়ার আচরণ মোটামুটি ২.৩৩-এর আশপাশের সংস্করণ থেকে), আর এখন (যেমন ২.৫৩-এ) দুই পথ আলাদা হলে pull উপরের মতো থেমে যায় — তবে fast-forward সম্ভব হলে কোনো config ছাড়াই fast-forward করে। আপনার Git কোন সংস্করণ, git --version দেখে মিলিয়ে নিন।
যে commit আগেই push হয়েছে আর অন্যরা ব্যবহার করছেন, সেগুলো rebase করবেন না — hash বদলে যায়, ফলে push করতে force লাগে আর সহকর্মীদের history-র সাথে গোলমাল হয়। pull --rebase নিরাপদ কারণ এটি শুধু আপনার এখনো push না করা commit সরায়। বিস্তারিত নিয়ম আর interactive rebase পর্ব ১৫-এ (Rebase).
৭ · Push-এর খুঁটিনাটি — upstream সেট করা, remote branch মোছা আর prune
নতুন branch-এর প্রথম push: -u আর push.autoSetupRemote
রহিম মোবাইল ওয়ালেটে পেমেন্টের জন্য একটি feature branch খুললেন। নাসরিন খুললেন কুপনের branch. দুজনের প্রথম push দুই রকম:
# --- Rahim ---
$ git switch -c feature/wallet
Switched to a new branch 'feature/wallet'
# (create payment.conf; git add payment.conf)
$ git commit -m "Add mobile wallet payment"
[feature/wallet e90c3b7] Add mobile wallet payment
1 file changed, 1 insertion(+)
create mode 100644 payment.conf
$ git push
fatal: The current branch feature/wallet has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature/wallet
To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.
$ git push -u origin feature/wallet
To ../server/bazar.git
* [new branch] feature/wallet -> feature/wallet
branch 'feature/wallet' set up to track 'origin/feature/wallet'.
$ git branch -vv
* feature/wallet e90c3b7 [origin/feature/wallet] Add mobile wallet payment
main 41edf81 [origin/main] Describe the shop in README
# --- Nasrin ---
$ git switch -c feature/coupon
Switched to a new branch 'feature/coupon'
# (create coupon.conf, git add, git commit -m "Add eid coupon")
$ git config push.autoSetupRemote true
$ git push
To ../server/bazar.git
* [new branch] feature/coupon -> feature/coupon
branch 'feature/coupon' set up to track 'origin/feature/coupon'.নতুন branch-এর কোনো upstream নেই, তাই শুধু git push জানে না কোথায় পাঠাবে — বার্তাটি নিজেই সমাধান বলে দেয়: git push --set-upstream origin feature/wallet (-u তার ছোট রূপ)। একবার দিলে config-এ branch.feature/wallet.remote আর .merge লেখা হয়; এরপর শুধু git push, git pull আর status-এর ahead/behind এই জোড়া ধরেই চলে। নাসরিন push.autoSetupRemote true সেট করেছেন (Git ২.৩৭, ২০২২ থেকে আছে বলে জানা যায়): upstream না থাকলে প্রথম git push নিজেই একই নামে remote branch বানিয়ে upstream সেট করে দিল।
শুধু git push কোন branch পাঠাবে, তা ঠিক করে push.default. Git ২.০ (২০১৪) থেকে default হলো simple: শুধু বর্তমান branch, আর শুধু তখনই যখন upstream-এর নাম একই। পুরোনো default matching একসাথে সব মিল-থাকা branch push করত — ভুল করে অর্ধেক কাজ পাঠিয়ে দেওয়ার পরিচিত উৎস ছিল। Push-এর পূর্ণ রূপ git push origin local:remote (যেমন git push origin HEAD:review/wallet) — বাঁয়ে আপনার ref, ডানে remote-এ কোন নামে বসবে। এটিও একটি refspec.
Remote branch মোছা আর fetch --prune
কাজ শেষে (অংশ ৮-এর ঝামেলা মেটার পরে) রহিম feature/wallet main-এ merge করে push দিলেন আর remote-এর branch মুছে দিলেন। নাসরিনের কাছে তখনো সেই branch-এর পুরোনো স্মৃতি:
# --- Rahim ---
$ git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git merge --no-ff -m "Merge branch 'feature/wallet'" feature/wallet
Merge made by the 'ort' strategy.
payment.conf | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 payment.conf
$ git push
To ../server/bazar.git
41edf81..93710ae main -> main
$ git push origin --delete feature/wallet
To ../server/bazar.git
- [deleted] feature/wallet
$ git branch -d feature/wallet
Deleted branch feature/wallet (was a1f5d64).
# --- Nasrin ---
$ git branch -vv
feature/coupon 4408a83 [origin/feature/coupon] Add eid coupon
feature/wallet 4218834 [origin/feature/wallet] Set minimum wallet payment
* main 41edf81 [origin/main] Describe the shop in README
$ git fetch --prune
From ../server/bazar
- [deleted] (none) -> origin/feature/wallet
41edf81..93710ae main -> origin/main
$ git branch -vv
feature/coupon 4408a83 [origin/feature/coupon] Add eid coupon
feature/wallet 4218834 [origin/feature/wallet: gone] Set minimum wallet payment
* main 41edf81 [origin/main: behind 4] Describe the shop in README
$ git pull --ff-only
Updating 41edf81..93710ae
Fast-forward
payment.conf | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 payment.conf
$ git branch -d feature/wallet
Deleted branch feature/wallet (was 4218834).
$ git ls-remote origin
93710aeceb3c78fe37e8a63228c92d1b9d216365 HEAD
4408a83d090513788c31aca06f1421849a67a77c refs/heads/feature/coupon
93710aeceb3c78fe37e8a63228c92d1b9d216365 refs/heads/maingit push origin --delete feature/wallet server-এর branch মোছে (- [deleted]); পুরোনো লেখায় একই কাজ git push origin :feature/wallet — বাঁ পাশ ফাঁকা refspec, অর্থাৎ “কিছু না দিয়ে বদলানো”। রহিমের local branch আলাদা জিনিস, তাই সেটি git branch -d দিয়ে আলাদা করে মোছা।
নাসরিনের দিকে সাধারণ fetch ইচ্ছে করেই মুছে যাওয়া remote branch-এর স্মৃতি সরায় না। --prune দিলে সরায়: - [deleted] (none) -> origin/feature/wallet. তারপর git branch -vv-তে [origin/feature/wallet: gone] — local branch আছে, কিন্তু তার upstream আর নেই। একই fetch-এ main-এর খবরও এসেছে: “behind 4”. Fast-forward-এর পরে নাসরিনের local feature/wallet-এর commit main-এ ঢুকে গেছে, তাই -d নির্ভয়ে মুছল (merge না হলে Git -d-তে আপত্তি করত)। শেষে git ls-remote দেখাচ্ছে server-এ এখন শুধু main আর feature/coupon. প্রতিবার --prune লিখতে না চাইলে git config fetch.prune true.
৮ · Force push — --force, --force-with-lease আর --force-if-includes
কখনো কখনো সত্যিই remote-এর history বদলাতে হয়: নিজের feature branch rebase বা amend করার পরে (পর্ব ১১-এ commit --amend দেখেছেন), পুরোনো commit-এর জায়গায় নতুন commit. তখন push non-fast-forward, আর সাধারণ push প্রত্যাখ্যাত হবে। Force push server-কে বলে “যা আছে মুছে আমারটা বসানো হোক।” প্রশ্ন হলো — server-এ এখন যা আছে, সেটা কি আপনি জানেন?
গল্প: feature/wallet-এ রহিমের commit e90c3b7-এর ওপর নাসরিন একটি commit (4218834, ন্যূনতম পেমেন্ট ২০ টাকা) push করেছেন। রহিম fetch না করেই নিজের commit amend করলেন:
# --- Nasrin ---
$ git fetch
From ../server/bazar
* [new branch] feature/wallet -> origin/feature/wallet
$ git switch feature/wallet
Switched to a new branch 'feature/wallet'
branch 'feature/wallet' set up to track 'origin/feature/wallet'.
# (add "min_amount = 20" to payment.conf; git add)
$ git commit -m "Set minimum wallet payment"
[feature/wallet 4218834] Set minimum wallet payment
1 file changed, 1 insertion(+)
$ git push
To ../server/bazar.git
e90c3b7..4218834 feature/wallet -> feature/wallet
# --- Rahim (has NOT fetched) ---
# (edit payment.conf: wallet -> mobile-wallet; git add)
$ git commit --amend -m "Add mobile-wallet payment method"
[feature/wallet 11fa2d8] Add mobile-wallet payment method
Date: Tue Jan 20 10:06:00 2026 +0600
1 file changed, 1 insertion(+)
create mode 100644 payment.conf
$ git status -sb
## feature/wallet...origin/feature/wallet [ahead 1, behind 1]
$ git push
To ../server/bazar.git
! [rejected] feature/wallet -> feature/wallet (fetch first)
error: failed to push some refs to '../server/bazar.git'
hint: Updates were rejected because the remote contains work that you do not
…
$ git push --force-with-lease
To ../server/bazar.git
! [rejected] feature/wallet -> feature/wallet (stale info)
error: failed to push some refs to '../server/bazar.git'নাসরিনের git switch feature/wallet লক্ষ্য করুন: local branch না থাকলেও Git origin/feature/wallet দেখে একই নামে tracking branch বানিয়ে দিল। রহিমের amend-এর পরে status “ahead 1, behind 1” — তাঁর পুরোনো স্মৃতির e90c3b7 আর তাঁর branch-এ নেই (amend সেটিকে নতুন 11fa2d8 দিয়ে বদলেছে)। সাধারণ push প্রত্যাখ্যাত। এরপর --force-with-lease: Git server-কে বলে “branch-টি বদলানো হোক, তবে শুধু যদি সেটি এখনো e90c3b7-এ থাকে” — অর্থাৎ রহিমের origin/feature/wallet যেখানে আছে। Server-এ আছে 4218834, শর্ত মেলেনি: (stale info). নাসরিনের commit বেঁচে গেল।
একই অবস্থার একটি কপিতে সাধারণ --force দিলে কী হতো:
$ git ls-remote origin refs/heads/feature/wallet
4218834c42fb0b29b8ed0c4ffe7a0d244cde5a30 refs/heads/feature/wallet
$ git push --force
To ../server/bazar.git
+ 4218834...11fa2d8 feature/wallet -> feature/wallet (forced update)
$ git ls-remote origin refs/heads/feature/wallet
11fa2d80680f31be471e339fa1aaa20a6a136821 refs/heads/feature/walletকোনো প্রশ্ন ছাড়াই + 4218834...11fa2d8 … (forced update). তিন বিন্দু (...) আর শুরুর + বলে দেয় এটি fast-forward নয়। Server-এর branch এখন 11fa2d8, যার history-তে 4218834 নেই — নাসরিনের commit server থেকে আর পৌঁছানো যায় না। নাসরিনের কম্পিউটারে কপিটি আছে বলে উদ্ধার সম্ভব (পরীক্ষার পরে ঠিক এভাবে server ফিরিয়ে আনা হয়েছে), কিন্তু তিনি না জানলে কাজটি হারিয়ে যেত।
Lease-এর ফাঁক — fetch-এর পরে lease অন্ধ
Lease মেলানো হয় আপনার origin/…-এর সাথে। কিন্তু fetch হলে origin/… নিজেই সরে যায় — অনেক editor আর IDE পেছনে নিজে থেকে fetch চালায়। তখন lease “মিলে যায়”, যদিও আপনি নতুন commit-টি কখনো দেখেননি:
$ git fetch
From ../server/bazar
e90c3b7..4218834 feature/wallet -> origin/feature/wallet
* [new branch] feature/coupon -> origin/feature/coupon
$ git status -sb
## feature/wallet...origin/feature/wallet [ahead 1, behind 2]
$ git push --force-with-lease
To ../server/bazar.git
+ 4218834...11fa2d8 feature/wallet -> feature/wallet (forced update)
# (server restored to 4218834 for the next try; Rahim fetches again)
$ git push --force-with-lease --force-if-includes
To ../server/bazar.git
! [rejected] feature/wallet -> feature/wallet (remote ref updated since checkout)
error: failed to push some refs to '../server/bazar.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
…Fetch-এর পরে status “behind 2” (নাসরিনের commit আর রহিমের amend-এ বাদ পড়া পুরোনো commit). শুধু --force-with-lease এবার সফল — এবং নাসরিনের commit মুছে দিল! --force-if-includes (Git ২.৩০ থেকে) আরেকটি শর্ত যোগ করে: remote-tracking ref-এর বর্তমান commit আপনার local branch-এর reflog-এর কোনো entry থেকে পৌঁছানো যায় কি না — অর্থাৎ সেটি আপনি আসলে কখনো নিজের branch-এ এনেছিলেন কি না। এখানে ছিল না, তাই (remote ref updated since checkout). সবচেয়ে নিরাপদ অভ্যাস: git push --force-with-lease --force-if-includes, বা git config push.useForceIfIncludes true.
আসল সমাধান অবশ্য force নয়: রহিম নাসরিনের commit-এর ওপর নিজের বদলটি rebase করলেন (ফাইলে add/add conflict হয়েছিল — দুজনেই payment.conf “তৈরি” করেছেন — দুই লাইনই রেখে সমাধান), তারপর সাধারণ push:
$ git rebase origin/feature/wallet
Auto-merging payment.conf
CONFLICT (add/add): Merge conflict in payment.conf
error: could not apply 11fa2d8... Add mobile-wallet payment method
…
# (edit payment.conf: keep "mobile-wallet" AND "min_amount = 20")
$ git add payment.conf
$ git rebase --continue
[detached HEAD a1f5d64] Add mobile-wallet payment method
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/feature/wallet.
$ git log --oneline --graph -4
* a1f5d64 Add mobile-wallet payment method
* 4218834 Set minimum wallet payment
* e90c3b7 Add mobile wallet payment
* 41edf81 Describe the shop in README
$ git push
To ../server/bazar.git
4218834..a1f5d64 feature/wallet -> feature/walletনিচের প্রোগ্রামটি চারটি পরিস্থিতি পাশাপাশি রাখে — শর্তগুলো Git-এর নিয়মের সরল রূপ:
// --force vs --force-with-lease vs --force-if-includes on one shared branch.
// W = Rahim's first commit, N = Nasrin's commit on top of W, W2 = Rahim's amended W.
const parents = { base: [], W: ["base"], N: ["W"], W2: ["base"] };
const reach = c => [c, ...parents[c].flatMap(reach)]; // c and all its ancestors
function scenario(title, fetched, mode) {
const server = { branch: "N" }; // Nasrin already pushed N
const rahim = { branch: "W2", tracking: "W", reflog: ["W", "W2"] };
if (fetched) rahim.tracking = server.branch; // e.g. an editor's background fetch
let result = "pushed";
if (mode !== "force" && server.branch !== rahim.tracking)
result = "rejected (stale info)"; // lease: server must match my tracking ref
else if (mode === "if-includes" && !rahim.reflog.includes(rahim.tracking))
result = "rejected (remote ref updated since checkout)";
if (result === "pushed") server.branch = rahim.branch;
const kept = reach(server.branch).includes("N") ? "N kept" : "N LOST";
console.log(title.padEnd(42) + result.padEnd(46) + "server=" + server.branch.padEnd(3) + kept);
}
scenario("stale, --force", false, "force");
scenario("stale, --force-with-lease", false, "lease");
scenario("after fetch, --force-with-lease", true, "lease");
scenario("after fetch, lease + --force-if-includes", true, "if-includes");আউটপুট:
stale, --force pushed server=W2 N LOST
stale, --force-with-lease rejected (stale info) server=N N kept
after fetch, --force-with-lease pushed server=W2 N LOST
after fetch, lease + --force-if-includes rejected (remote ref updated since checkout) server=N N kept
চারটি সারি উপরের চারটি আসল Git ফলের সাথে মেলে। --force কিছুই যাচাই করে না। Lease যাচাই করে “server কি আমার স্মৃতির জায়গায়?” — fetch না হলে এটি কাজের পাহারাদার, fetch-এর পরে অন্ধ। --force-if-includes যাচাই করে “আমার স্মৃতির commit কি কখনো আমার branch-এ ছিল?” — তৃতীয় সারির ফাঁকটি বন্ধ করে। মডেলে reflog শুধু একটি তালিকা; আসল Git দেখে, tracking ref-এর commit reflog-এর কোনো entry থেকে পৌঁছানো যায় কি না।
দলের নিয়ম: shared branch-এ (main, release branch) force push নয় — অনেক দল hosting platform-এ এসব branch “protected” করে force push বন্ধ রাখে (সুবিধার নাম আর খুঁটিনাটি platform-ভেদে আলাদা; পর্ব ১৬)। নিজের feature branch-এ দরকার হলে --force-with-lease --force-if-includes, আর আগে সহকর্মীদের জানিয়ে। Server-এর branch ভুল করে মুছে গেলে যার কাছে কপি আছে তিনি push করে ফেরাতে পারেন; নিজের হারানো commit খুঁজতে reflog (পর্ব ১১, গভীরে পর্ব ৫৩)।
৯ · ঠিকানা আর remote সামলানো — HTTPS, SSH, local path, fork
এতক্ষণ remote ছিল একই কম্পিউটারের একটি folder. বাস্তবে remote থাকে network-এর ওপারে, আর ঠিকানার শুরু দেখেই বোঝা যায় Git কোন পথে (transport protocol) কথা বলবে:
| ঠিকানার রূপ | পথ | পরিচয় প্রমাণ | কখন |
|---|---|---|---|
https://github.com/ | HTTPS | username + token, credential helper-এ রাখা | সবচেয়ে সহজ; firewall-এ সাধারণত খোলা |
git@github.com: বা ssh://… | SSH | key pair; গোপন key আপনার কম্পিউটারে | প্রতিদিনের developer কাজ; password ছাড়া |
../server/bazar.git, /srv/git/x.git | local path | ফাইলের অনুমতি | পরীক্ষা, shared drive |
file:///srv/git/x.git | local, তবে network-এর মতো | ফাইলের অনুমতি | সাধারণ path-এর মতোই, কিছু পার্থক্যসহ |
শেষ দুটির পার্থক্য সূক্ষ্ম: সাধারণ path থেকে clone করলে Git সম্ভব হলে object ফাইলগুলো hardlink বা সরাসরি কপি করে (দ্রুত), আর file:// দিলে network-এর মতো pack বানিয়ে পাঠায়। আরেকটি পুরোনো পথ git:// (port 9418) এনক্রিপ্ট করা নয় আর পরিচয়ও যাচাই করে না; বড় platform-গুলো এটি বন্ধ করে দিয়েছে বলে জানা যায়, তাই নতুন কাজে ব্যবহার করবেন না।
HTTPS — token আর credential helper
HTTPS-এ প্রতিটি push-এ (আর private repository-র fetch-এও) Git পরিচয় চায়। একসময় সেখানে account-এর password দেওয়া হতো। এখন বড় hosting platform-গুলো Git-এর HTTPS কাজে account password নেয় না — GitHub ২০২১ সালের আগস্টে এটি বন্ধ করার কথা ঘোষণা করেছিল, অন্যরাও একই পথে (লেখার সময়ে; খুঁটিনাটি platform-ভেদে আলাদা)। বদলে লাগে একটি personal access token — platform-এর settings থেকে বানানো একটি লম্বা গোপন string, যার মেয়াদ আর ক্ষমতা (কোন repository, শুধু পড়া না লেখাও) সীমিত করা যায়। আর প্রতিবার token টাইপ না করতে Git একটি credential helper ব্যবহার করে, যা OS-এর নিরাপদ ভান্ডারে (Windows-এ Credential Manager, macOS-এ Keychain) token রেখে দেয়। Git for Windows-এ সাধারণত Git Credential Manager আগে থেকেই সেট থাকে; দেখতে git config --get credential.helper.
SSH — key pair
SSH-এ password বা token-এর বদলে একজোড়া key: গোপন (private) key থাকে শুধু আপনার কম্পিউটারে, আর প্রকাশ্য (public) key-টি hosting platform-এর account settings-এ যোগ করেন। Server একটি চ্যালেঞ্জ পাঠায়, যার সঠিক উত্তর শুধু private key দিয়েই দেওয়া যায় — key নিজে কখনো network-এ যায় না। (Public-key cryptography-র ভিত্তি: Computer Networks · Cryptography-র মৌলিক; TLS আর HTTPS: TLS আর HTTPS.) Key বানানোর command (OpenSSH; আউটপুট প্রতিবার আলাদা, কারণ key এলোমেলোভাবে তৈরি হয়):
$ ssh-keygen -t ed25519 -C "rahim@example.com" -f keys/id_ed25519 -N ""
Generating public/private ed25519 key pair.
Your identification has been saved in keys/id_ed25519
Your public key has been saved in keys/id_ed25519.pub
The key fingerprint is:
SHA256:… rahim@example.com
The key's randomart image is:
…
$ ls keys
id_ed25519
id_ed25519.pubপরীক্ষায় -f keys/… দিয়ে key-টি একটি খেলনা folder-এ আর -N "" দিয়ে ফাঁকা passphrase-এ বানিয়ে তারপর মুছে ফেলা হয়েছে (fingerprint আর ছবিটি প্রতিবার আলাদা, তাই বাদ)। নিজের আসল key-র জন্য এই দুটি option বাদ দিন — ssh-keygen তখন default জায়গা (~/.ssh/) আর passphrase জিজ্ঞেস করবে। আসল key-তে একটি passphrase দিন (চাবির ওপর আরেকটি তালা), আর বারবার না লিখতে ssh-agent ব্যবহার করুন। -t ed25519 আধুনিক, ছোট আর দ্রুত key-র ধরন; খুব পুরোনো server-এ না চললে তবেই RSA. শুধু .pub ফাইলটিই কাউকে দেওয়া যায় — id_ed25519 কখনো নয়, কোনো chat বা AI prompt-এও নয়। একাধিক account বা key থাকলে ~/.ssh/config:
# ~/.ssh/config
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
# then use the alias in the remote URL:
# git remote set-url origin git@github-work:example/bazar-app.gitএখানে github-work একটি ডাকনাম: SSH সেটিকে আসল host-এ বদলে নির্দিষ্ট key ব্যবহার করে, আর IdentitiesOnly yes অন্য key চেষ্টা করা বন্ধ রাখে। Signed commit, ফাঁস হওয়া token বাতিল করা আর key-এর নিরাপত্তার গভীর আলোচনা পর্ব ৫৭-এ।
Remote সামলানো — add, rename, set-url, remove
একটি repository-তে remote যোগ, নাম বদল, ঠিকানা বদল আর মোছার কাজগুলো শুধু config সম্পাদনা — network-এ কিছু যায় না। নাসরিন পরীক্ষার জন্য একটি কাল্পনিক ঠিকানা যোগ করলেন (কোনো fetch দেওয়া হয়নি, তাই কোনো server-এর সাথে যোগাযোগও হয়নি):
$ git remote add upstream https://github.com/example/bazar-app.git
$ git remote -v
origin ../server/bazar.git (fetch)
origin ../server/bazar.git (push)
upstream https://github.com/example/bazar-app.git (fetch)
upstream https://github.com/example/bazar-app.git (push)
$ git remote rename upstream example
$ git remote set-url example git@github.com:example/bazar-app.git
$ git remote -v
example git@github.com:example/bazar-app.git (fetch)
example git@github.com:example/bazar-app.git (push)
origin ../server/bazar.git (fetch)
origin ../server/bazar.git (push)
$ git remote remove example
$ git remote
origin
$ git remote show origin
* remote origin
Fetch URL: ../server/bazar.git
Push URL: ../server/bazar.git
HEAD branch: main
Remote branches:
feature/coupon tracked
main tracked
Local branches configured for 'git pull':
feature/coupon merges with remote feature/coupon
main merges with remote main
Local refs configured for 'git push':
feature/coupon pushes to feature/coupon (up to date)
main pushes to main (up to date)rename শুধু নাম নয়, সেই remote-এর সব remote-tracking ref আর upstream config-ও নতুন নামে সরায়। set-url HTTPS থেকে SSH-এ যাওয়ার সাধারণ উপায় — history-তে কিছু বদলায় না। remove remote-টির সাথে তার সব refs/remotes/<নাম>/… মুছে দেয় (commit নিজে নয়)। git remote show origin একসাথে সব দেখায়, এবং এটি remote-এর সাথে যোগাযোগ করে — তাই “up to date” কথাটি তাজা খবর। (git remote show -n origin যোগাযোগ ছাড়া শুধু config দেখায়।)
Fork আর দুটি remote: origin আর upstream
Open source-এ সাধারণত মূল repository-তে আপনার push করার অনুমতি থাকে না। তখন hosting platform-এ একটি fork বানান — platform-এর server-এ মূল repository-র আপনার নিজের কপি। প্রথা হলো: নিজের fork = origin (সেখানে push করেন), মূল repository = upstream (সেখান থেকে শুধু fetch করেন)। এখানে upstream remote কথাটি git push -u-এর upstream branch থেকে আলাদা জিনিস — একই শব্দের দুই অর্থ, গুলিয়ে ফেলবেন না।
# commands only; example URLs, not run against a real server
git clone https://github.com/you/bazar-app.git # your fork = origin
cd bazar-app
git remote add upstream https://github.com/example/bazar-app.git
git fetch upstream # read the original
git switch -c fix/typo upstream/main # start from its latest main
# ... commit ...
git push -u origin fix/typo # push to YOUR fork, then open a PRএরপর platform-এ pull request খুলে মূল repository-র রক্ষণাবেক্ষণকারীদের কাছে কাজটি পাঠানো হয়। সেই কাজের ধারা, review আর merge-এর নিয়ম পর্ব ১৬-এ (Pull request আর fork). Release-এর tag remote-এ পাঠানো (git push নিজে থেকে tag পাঠায় না) পর্ব ১৮-এ (ভালো commit, tag আর release).
আরও গভীরে: fetch আর push-এর পেছনে Git-এর “pack protocol”: দুই পক্ষ প্রথমে ref-এর তালিকা আদান-প্রদান করে (যা git ls-remote দেখায়), তারপর কোন commit কার কাছে আছে মিলিয়ে শুধু অনুপস্থিত object একটি সংকুচিত packfile-এ পাঠায়। নতুন সংস্করণে protocol v2 (Git ২.১৮-এ যোগ হয়, পরে default) ref-এর তালিকাও শুধু দরকারমতো চায়, বড় repository-তে যা অনেক সময় বাঁচায়। Object আর packfile-এর ভেতরের গঠন পর্ব ৫২-এ, ref আর reflog-এর গভীরে পর্ব ৫৩-এ; Scott Chacon ও Ben Straub-এর Pro Git (২য় সংস্করণ, ২০১৪)-এর “Git on the Server” আর “Transfer Protocols” অধ্যায় ভালো সূত্র।
পরীক্ষা ও ইন্টারভিউতে:
git fetchআরgit pull-এর পার্থক্য? — fetch শুধু commit আর remote-tracking ref আনে; pull = fetch + বর্তমান branch-এ merge (বা rebase).origin/mainকী? — remote-এরmainশেষবার কোথায় দেখা গেছে, তার local read-only ref; fetch বা সফল push-এ সরে।- Push “rejected (fetch first)” কেন? — remote-এর tip আপনার tip-এর ancestor নয় (non-fast-forward); মানলে অন্যের commit হারাত। সমাধান fetch → merge/rebase → push.
--forceবনাম--force-with-lease? — lease শুধু তখনই বদলায় যখন remote আপনার জানা commit-এ আছে; ফলে অন্যের অজানা commit মুছে যায় না।git push -uকী করে? — push-এর সাথে upstream (branch.<নাম>.remote/merge) সেট করে, যাতে পরে শুধু push/pull আর ahead/behind কাজ করে।
১০ · AI-এর যুগে — remote-এর command-এ AI-কে যাচাই
“Push rejected” দেখে AI assistant-কে জিজ্ঞেস করলে উত্তর আসে কয়েক সেকেন্ডে। কিন্তু এই বিষয়ে ভুলের দাম বেশি, কারণ remote মানে অন্যদের কাজও। AI-এর চেনা ভুলগুলো:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Rejected push-এর সমাধান হিসেবে সরাসরি git push --force — কোনো সতর্কবার্তা ছাড়া | আগে git fetch আর git log main..origin/main — কার কোন commit মুছবে দেখুন; সমাধান সাধারণত merge বা rebase |
git reset --hard origin/main “sync করতে” — নিজের অপ্রকাশিত commit আর অসমাপ্ত কাজ হারায় | git status আর git log origin/main..main দেখে নিন কী হারাবে; আগে একটি branch বানিয়ে রাখুন |
পুরোনো আচরণের বর্ণনা (“pull সবসময় merge করে”, push.default matching) | git --version আর git help pull / git help config মিলিয়ে দেখুন |
বানানো flag বা option (যেমন অস্তিত্বহীন --safe-force) | git help push; অজানা option-এ Git নিজেই error দেয় — throwaway clone-এ চালিয়ে দেখুন |
Token বা URL-এর ভেতরে password বসানোর পরামর্শ (https://user:token@…) | Credential helper বা SSH ব্যবহার করুন; URL-এ বসানো token config ফাইল আর shell history-তে খোলা পড়ে থাকে |
Error-এর সাথে পুরো git remote -v বা config prompt-এ পাঠানো, ভেতরে token | পাঠানোর আগে token, private key, ভেতরের server-এর নাম মুছে দিন (পর্ব ৫৭) |
নিচের prompt-এ AI-কে আসল অবস্থা (ahead/behind, log) দেওয়া হয়েছে আর ধ্বংসাত্মক পথ নিষেধ করা হয়েছে, যাতে উত্তর যাচাই করা যায়:
AI-এর দেওয়া remote-সমাধান হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Git তো distributed — প্রতিটি কপিতে পুরো history. তাহলে প্রায় সব দল কেন একটি “central” remote রাখে? এতে কি distributed হওয়ার লাভ চলে যায়?
Git-এর নকশায় কোনো repository অন্যটির চেয়ে বিশেষ নয়। Linus Torvalds ২০০৫ সালে Git বানিয়েছিলেন Linux kernel-এর কাজের ধারার জন্য, যেখানে কোনো একটি server-এ সবাই push করে না। সেখানে বিভিন্ন subsystem-এর রক্ষণাবেক্ষণকারীরা নিজেদের repository রাখেন, patch আসে email-এ, আর ওপরের স্তরের রক্ষণাবেক্ষণকারী নিচের স্তরের repository থেকে pull করেন। Git-এ git request-pull আর git format-patch-এর মতো command এই ধারার চিহ্ন। অর্থাৎ “কে কার কাছ থেকে নেবে” — এটি সামাজিক চুক্তি, Git-এর কাঠামো নয়।
তবু বেশিরভাগ দল একটি central remote রাখে, কারণ তাতে প্রশ্নের উত্তর সহজ হয়: “এখনকার সঠিক main কোনটি?” CI, deploy, code review, issue — সবকিছু একটি জায়গাকে ধরে চলে। Hosting platform-গুলো এই কেন্দ্রের চারপাশে review, অনুমতি আর automation জুড়ে দিয়েছে।
কিন্তু distributed হওয়ার লাভ হারায় না। প্রথমত, প্রায় সব কাজ — commit, branch, merge, log, diff — network ছাড়াই হয়; load-shedding বা দুর্বল ইন্টারনেটেও কাজ চলে, পরে push. দ্বিতীয়ত, প্রতিটি clone একটি পূর্ণ backup. Server-এর disk নষ্ট হলে যেকোনো developer-এর কপি থেকে history ফেরানো যায় — centralized ব্যবস্থায় (পর্ব ৭) server হারানো মানে history হারানো। তৃতীয়ত, একাধিক remote একসাথে রাখা যায়: fork-এর origin আর upstream, বা একটি backup mirror.
একটি সূক্ষ্ম বিষয়: central remote-ও শুধু ref আর object রাখে। কোনটি “আসল” history, তা ঠিক হয় কেবল সেই remote-এর branch কোন commit-এ আছে তা দিয়ে। তাই force push-এর মতো কাজ কেন্দ্রের চুক্তি ভাঙে, আর platform-গুলো protected branch দিয়ে সেই চুক্তি রক্ষা করে।
মূল উপলব্ধি: Git প্রযুক্তিগতভাবে distributed, কিন্তু দলের কাজ সামাজিকভাবে প্রায়ই centralized. দুটি একসাথে থাকায় offline কাজ ও পূর্ণ backup-এর সুবিধার সাথে একটি স্পষ্ট “সত্যের উৎস”-ও পাওয়া যায়।
প্র ০২Git push-এ non-fast-forward প্রত্যাখ্যান করে, অথচ remote-tracking ref (+refs/heads/*:refs/remotes/origin/*) জোর করে বদলাতে দেয়। এই অসামঞ্জস্যের পেছনে যুক্তি কী?
দুটি ref-এর মালিকানা আলাদা, তাই নিয়মও আলাদা। Remote-এর refs/heads/main দলের সবার যৌথ সম্পত্তি। সেখানে একজন non-fast-forward বদল করলে অন্যদের commit সেই branch থেকে আর পৌঁছানো যায় না। এই পর্বের পরীক্ষায় সাধারণ --force ঠিক এটাই করেছে: নাসরিনের 4218834 server থেকে “হারিয়ে” গেল, কোনো error ছাড়া। তাই default নিয়ম রক্ষণশীল — শুধু সামনে সরানো চলবে, পেছনে বা পাশে নয়। Server-এর দিক থেকেও receive.denyNonFastForwards config আর hosting platform-এর protected branch এই নিয়ম আরও কঠোর করে, যাতে কেউ --force দিলেও কাজ না হয়।
উল্টো দিকে refs/remotes/origin/main কারও কাজ নয় — এটি শুধু একটি পর্যবেক্ষণ: “remote-এ এখন এটা আছে।” পর্যবেক্ষণ সবসময় সত্যের সাথে মিলিয়ে নেওয়াই উচিত। কেউ remote-এ history পুনর্লিখন করলে (যেমন নিজের feature branch rebase করে force push) আপনার স্মৃতিকেও সেই নতুন অবস্থায় যেতে হবে, নইলে ahead/behind ভুল হবে। তাই refspec-এর শুরুতে +: fetch-এর আউটপুটে তখন + 11fa2d8...4218834 … (forced update) দেখা যায়, যেমন এই পর্বের পরীক্ষায় এসেছে। এতে আপনার নিজের কোনো commit হারায় না — local branch অপরিবর্তিত থাকে, আর পুরোনো ref-এর মান reflog-এ থেকে যায়।
এখানে একটি সাধারণ নকশা-নীতি আছে: যে state অন্যের কাজ ধারণ করে, তাতে বদল শুধু যোগ-করার দিকে (append-only, এখানে fast-forward); আর যে state শুধু অন্য কিছুর ছায়া (cache), তা যেকোনো সময় উৎস থেকে নতুন করে লেখা যায়। ডেটাবেসের replication বা browser cache-এও একই চিন্তা।
মূল উপলব্ধি: push-এর কঠোরতা অন্যের কাজ রক্ষা করে; tracking ref-এর নমনীয়তা আপনার জানাকে সত্যের সাথে মেলায়। একটি “মালিকানা”, অন্যটি “ছায়া”।
প্র ০৩অনেক অভিজ্ঞ developer git pull প্রায় ব্যবহারই করেন না — আলাদা করে fetch, তারপর নিজে merge বা rebase. কেন? আর Git নিজেই pull-এর আচরণ কেন বদলাল?
git pull একটি সুবিধাজনক জোড়া-command, কিন্তু এটি দুটি সিদ্ধান্ত একসাথে নেয়: কী আসছে সেটি না দেখেই মেশানো, আর কীভাবে মেশানো হবে। পুরোনো Git-এ default ছিল merge, ফলে নতুনরা প্রতিদিন নিজের অজান্তে “Merge branch 'main' of …” ধরনের ছোট ছোট merge commit বানাতেন। এই পর্বে 23d6d2c Merge branch 'main' of ../server/bazar ঠিক তেমন একটি। দলের history-তে এগুলো এত বেশি জমে যে আসল feature merge খুঁজে পাওয়া কঠিন হয়।
আলাদা fetch দিলে মাঝখানে একটি মুহূর্ত পাওয়া যায়: git log main..origin/main দিয়ে দেখা যায় কী আসছে, git diff main...origin/main দিয়ে বদলটা পড়া যায়, তারপর সচেতনভাবে fast-forward, merge বা rebase বেছে নেওয়া যায়। Fetch যেহেতু কিছু ভাঙে না, এটি যত খুশি চালানো যায় — অনেকে git fetch --all --prune দিয়ে দিন শুরু করেন।
Git-এর রক্ষণাবেক্ষণকারীরাও এই সমস্যা স্বীকার করেছেন। Git ২.২৭ (২০২০)-এ pull.rebase সেট না থাকলে সতর্কবার্তা যোগ হয়। এরপর কয়েকটি সংস্করণে আলোচনা আর ধাপে ধাপে বদল হয় (থেমে যাওয়ার আচরণ মোটামুটি ২.৩৩-এর আশপাশ থেকে); এখন (যেমন ২.৫৩-এ) দুই পথ আলাদা হলে pull থেমে যায় আর আপনাকে --rebase, --no-rebase বা --ff-only বেছে নিতে বলে — এই পর্বের transcript-এ যা দেখেছেন। Fast-forward সম্ভব হলে অবশ্য আগের মতোই নীরবে fast-forward করে। এটি একটি ভালো উদাহরণ যে জনপ্রিয় টুলও সময়ের সাথে “নিরাপদ default”-এর দিকে যায়, আর পুরোনো টিউটোরিয়াল বা AI-এর উত্তর তখন ভুল হয়ে পড়ে।
তবু pull খারাপ নয়। নিজের local main হালনাগাদ রাখতে git pull --ff-only নিখুঁত — কখনো অপ্রত্যাশিত merge বানাবে না। আর নিজের অপ্রকাশিত commit থাকলে git pull --rebase ছোট, সরল history দেয়।
মূল উপলব্ধি: fetch “জানা”, merge/rebase “সিদ্ধান্ত”। দুটি আলাদা রাখলে সিদ্ধান্তটি সচেতন হয়; pull ব্যবহার করলে অন্তত কোন ধরনের মেশানো হবে, তা config-এ স্পষ্ট করে রাখুন।
প্র ০৪HTTPS-এ token, নাকি SSH key — নিরাপত্তার দিক থেকে কোনটি ভালো? Platform-গুলো password বন্ধ করল কেন?
Account password দিয়ে Git চালানোর সমস্যা ছিল কয়েকটি। একই password দিয়ে পুরো account-এ ঢোকা যায় — repository মোছা, settings বদলানো, সবকিছু। Password প্রায়ই অন্য website-এও একই থাকে, তাই অন্য কোথাও ফাঁস হলে Git account-ও ঝুঁকিতে। আর script বা CI-তে password রাখলে সেটি বদলানো মানে সব জায়গা ভাঙা। দুই-ধাপের যাচাই (2FA) চালু থাকলে Git-এর মতো command-line টুলে password দিয়ে কাজ করাও কঠিন। এসব কারণে বড় platform-গুলো Git-এর HTTPS কাজে password নেওয়া বন্ধ করেছে — GitHub-এর ঘোষণা অনুযায়ী ২০২১ সালের আগস্ট থেকে (অন্যদের তারিখ ও নিয়ম আলাদা হতে পারে)।
Token এই সমস্যার উত্তর least privilege নীতিতে: একটি token শুধু নির্দিষ্ট repository, শুধু নির্দিষ্ট কাজ (পড়া বা লেখা) আর নির্দিষ্ট মেয়াদের জন্য বানানো যায়, আর আলাদাভাবে বাতিল করা যায়। ফাঁস হলে ক্ষতি সীমিত। দুর্বলতা হলো token একটি “bearer” গোপন কথা — যার হাতে string-টি আছে, সে-ই ব্যবহার করতে পারে। তাই সেটি URL, script বা prompt-এ লিখে রাখা বিপজ্জনক; ভালো credential helper (যেমন Git Credential Manager বা macOS-এর Keychain) OS-এর এনক্রিপ্ট করা ভান্ডারে রাখে, তবে store helper সাধারণ লেখায় ফাইলে রাখে।
SSH key-এ গোপন অংশটি কখনো network-এ যায় না; server শুধু প্রমাণ চায় যে আপনার কাছে key আছে। Passphrase দিলে চুরি যাওয়া key ফাইলও সরাসরি কাজে লাগে না। দুর্বলতা হলো key-এর মেয়াদ সাধারণত থাকে না, আর একটি key অনেক repository-তে কাজ করে — বছরের পর বছর পুরোনো ল্যাপটপে পড়ে থাকা key ঝুঁকি। কিছু প্রতিষ্ঠান তাই hardware security key-তে রাখা SSH key বা স্বল্পমেয়াদি certificate ব্যবহার করে।
তাই উত্তর “কোনটি সব সময় ভালো” নয়: ব্যক্তিগত ল্যাপটপে passphrase-সহ ed25519 SSH key বা credential helper-এ রাখা সীমিত token — দুটোই যুক্তিসংগত। CI-তে সাধারণত platform-এর দেওয়া স্বল্পমেয়াদি token সবচেয়ে ভালো। ফাঁস হলে কী করবেন — বাতিল, নতুন বানানো, history পরিষ্কার — পর্ব ৫৭-এ।
মূল উপলব্ধি: নিরাপত্তার প্রশ্ন হলো “ফাঁস হলে ক্ষতি কতটা, আর কত দ্রুত বাতিল করা যায়?” সীমিত ক্ষমতা, মেয়াদ আর নিরাপদ ভান্ডার — যে পদ্ধতিই হোক, এই তিনটি নিশ্চিত করুন।
প্র ০৫--force-with-lease-কে “নিরাপদ force” বলা হয়। তাহলে Git-কে আবার --force-if-includes যোগ করতে হলো কেন? এখান থেকে নিরাপত্তা-ব্যবস্থার নকশা নিয়ে কী শেখার আছে?
Force push-এর বিপদ বাস্তব। প্রকাশ্যে জানা একটি ঘটনায়, ২০১৩ সালে Jenkins open-source project-এ একজন অবদানকারীর একটি ভুল force push-এ শতাধিক repository-র branch পুরোনো অবস্থায় ফিরে গিয়েছিল বলে project-এর mailing list-এ জানানো হয় (সংখ্যা আর খুঁটিনাটি সূত্রভেদে আলাদা)। Developer-দের local কপি থেকে সেগুলো পরে ফেরানো হয় — distributed হওয়ার সুফল। এ ধরনের ঘটনা থেকেই “শর্তসাপেক্ষ” force-এর চাহিদা।
--force-with-lease (Git ১.৮.৫, ২০১৩) একটি “compare-and-swap”: “server-এর ref যদি X হয়, তবেই Y বসানো হোক।” X কী, তা আপনি নিজে দিতে পারেন (--force-with-lease=feature/wallet:e90c3b7); না দিলে Git ধরে নেয় আপনার origin/feature/wallet. এখানেই ফাঁক: X আপনার জানা অবস্থা হওয়ার কথা, কিন্তু remote-tracking ref “জানা” নয়, শুধু “নামানো”। Editor পেছনে fetch করলে বা আপনি অভ্যাসবশত fetch দিলে X নিজে থেকে সর্বশেষ অবস্থায় চলে যায়, আর lease অন্ধভাবে মিলে যায় — এই পর্বের পরীক্ষায় fetch-এর পরে lease ঠিক এভাবে নাসরিনের commit মুছেছে।
--force-if-includes (Git ২.৩০) প্রশ্নটি বদলায়: “remote-এর বর্তমান commit কি আমি কখনো নিজের branch-এ এনেছিলাম?” এটি reflog দেখে যাচাই করে। শুধু fetch করে দেখা নয়, সত্যিই merge, rebase বা reset করে নিজের কাজে মেশানো হলেই শর্ত মেলে। ফলে “নামানো” আর “জানা”-র পার্থক্যটা Git ধরতে পারে।
নিরাপত্তা-নকশার শিক্ষা দুটি। এক, একটি সুরক্ষা প্রায়ই একটি অনুমানের ওপর দাঁড়ায় (এখানে: “tracking ref = আমার জানা অবস্থা”); অনুমানটি কোন পরিস্থিতিতে ভাঙে, তা খুঁজতে হয়। দুই, স্তরে স্তরে পাহারা: client-এ lease আর if-includes, server-এ protected branch, আর শেষ ভরসা প্রত্যেকের local কপি আর reflog. কোনো একটি স্তর নিখুঁত নয়, মিলে তারা ঝুঁকি কমায়।
মূল উপলব্ধি: “নিরাপদ” option-ও একটি অনুমানের ওপর নিরাপদ। অনুমানটি জানুন, আর গুরুত্বপূর্ণ জায়গায় একাধিক স্তরের সুরক্ষা রাখুন।
অনুশীলন
প্রথমে নিজে উত্তর লিখুন, তারপর মিলিয়ে নিন। চাইলে একটি খালি folder-এ bare repository আর দুটি clone বানিয়ে নিজে চালিয়ে দেখুন।
-
আউটপুট অনুমান করুন: আপনি clone করেছিলেন যখন
origin/main= P. তারপর নিজেরmain-এ দুটি commit X আর Y করলেন। এর মধ্যে এক সহকর্মী P-এর ওপর একটি commit Z push করেছেন। কোনো fetch না দিয়ে (ক)git statusকী বলবে? (খ)git push-এর rejected লাইনের বন্ধনীতে কী থাকবে? (গ)git fetch-এর পরেgit status? (ঘ) তারপরgit pull --rebaseদিলেgit status -sb-এর প্রথম লাইন?(ক)
Your branch is ahead of 'origin/main' by 2 commits.— তুলনা পুরোনোorigin/main(P)-এর সাথে, Z-এর খবর নেই।(খ)
! [rejected] main -> main (fetch first)— Z আপনার কাছে নেই-ই, তাই “fetch first”; সাথেerror: failed to push some refs …আর hint.(গ)
Your branch and 'origin/main' have diverged, and have 2 and 1 different commits each, respectively.— ahead = {X, Y}, behind = {Z}.(ঘ)
## main...origin/main [ahead 2]— X আর Y নতুন hash নিয়ে Z-এর ওপরে বসেছে (X′, Y′); behind শূন্য। এখন সাধারণgit pushfast-forward হবে। (--no-rebaseদিলে হতো[ahead 3]: X, Y আর merge commit.) -
Command-এর ক্রম লিখুন: আপনি
feature/coupon-এ কাজ করছেন (upstreamorigin/feature/coupon), দুটি commit এখনো push করেননি। জানতে পারলেন সহকর্মীও এই branch-এ একটি commit push করেছেন। (১) আগে নিরাপদে দেখুন কী এসেছে, (২) নিজের commit সহকর্মীর commit-এর ওপরে সাজান, (৩) push করুন, (৪) কাজmain-এ merge হওয়ার পরে remote আর local branch মুছুন, আর মুছে যাওয়া remote branch-এর স্মৃতি পরিষ্কার করুন। প্রতিটি ধাপের command লিখুন।git fetch # 1: only origin/* moves git log --oneline feature/coupon..origin/feature/coupon # what is coming git log --oneline origin/feature/coupon..feature/coupon # what I will send git pull --rebase # 2: (or: git rebase origin/feature/coupon) git status -sb # expect [ahead 2] git push # 3: fast-forward, no force needed # 4: after the merge into main git switch main git pull --ff-only git push origin --delete feature/coupon git branch -d feature/coupon git fetch --pruneমূল কথা: rebase শুধু আপনার অপ্রকাশিত দুটি commit সরিয়েছে, তাই push সাধারণ fast-forward — force লাগেনি। Conflict হলে ফাইল ঠিক করে
git add, তারপরgit rebase --continue.git branch -d(ছোট d) আপনার branchmain-এ merge না হয়ে থাকলে আপত্তি করবে — সেটি একটি দরকারি নিরাপত্তা। দলের কেউ যদি ইতিমধ্যে remote branch মুছে দিয়ে থাকেন, তবেpush --deleteএকটি error দেবে; তখন শুধুgit fetch --pruneযথেষ্ট। (সফলpush --deleteনিজেই আপনারorigin/feature/couponসরায়;--pruneপরিষ্কার করে অন্যদের মোছা branch-এর স্মৃতি।) -
Ahead/behind হিসাব করুন: Commit graph (প্রতিটির parent বন্ধনীতে): A( ), B(A), C(B), D(C), E(B), F(E), G(F, C). আপনার
main= G,origin/main= D. (ক) ahead আর behind কত, কোন কোন commit? (খ)mainআরorigin/main-এর merge base কোনটি? (গ) এখন push করলে কী হবে?R(G) = {G, F, E, C, B, A} — G-এর দুই parent F আর C, সেখান থেকে E, B, A. R(D) = {D, C, B, A}.
(ক) ahead = R(G) − R(D) = {G, F, E} → ৩; behind = R(D) − R(G) = {D} → ১.
git status: “have diverged, and have 3 and 1 different commits each”.(খ) Common ancestor = {C, B, A}; এদের মধ্যে C অন্য দুটির উত্তরসূরি, তাই merge base = C. (G আগে একবার C-কে merge করেছিল — তাই base B নয়।)
(গ) D, G-এর ancestor নয় → non-fast-forward →
! [rejected]. D আপনার কাছে আছে (fetch হয়েছে বলেইorigin/main= D), তাই বন্ধনীতে(non-fast-forward), “fetch first” নয়। আগে D-কে merge বা rebase করতে হবে। -
সমালোচনা করুন: একটি কাল্পনিক ছোট দলের README-তে Git-এর নিয়ম এমন: (১) “সবাই সরাসরি
main-এ push করবেন।” (২) “Push reject হলেgit push --forceদিয়ে দেবেন, সময় বাঁচবে।” (৩) “Password বারবার চায়, তাই remote URL-এhttps://name:token@…লিখে রাখবেন।” (৪) “প্রতিদিন সকালেgit pull; কী ঘটছে দেখার দরকার নেই।” (৫) “fetch.prune trueসেট করবেন।” কোনগুলো সমস্যাজনক, কেন, আর কী লিখলে ভালো হয়?- (১) খুব ছোট দলে চলতে পারে, কিন্তু review ছাড়া সব কাজ সরাসরি মূল branch-এ যায়। ভালো: ছোট feature branch আর pull request-এর মাধ্যমে
main-এ ঢোকা (পর্ব ১৬, ১৭)। - (২) সবচেয়ে বিপজ্জনক। Reject মানেই কারও নতুন কাজ remote-এ আছে; force দিলে সেটি মুছে যায়। ভালো: “Reject হলে
git fetch, তারপরgit pull --rebaseবা merge, তারপর push.main-এ force push নিষেধ; নিজের feature branch-এ দরকার হলে শুধু--force-with-lease --force-if-includes.” Platform-এmainprotected রাখুন। - (৩) Token config ফাইলে খোলা লেখায় থাকে,
git remote -v-তে দেখা যায়, screenshot বা AI prompt-এ ফাঁস হয়। ভালো: credential helper বা SSH key (passphrase-সহ); token-এর ক্ষমতা আর মেয়াদ সীমিত। - (৪) Config ছাড়া Git ২.৫৩-এ দুই পথ আলাদা হলে pull থেমে যাবে; পুরোনো সংস্করণে নীরবে merge commit বানাত। ভালো: “
git fetch,git statusদেখুন; localmain-এgit pull --ff-only; নিজের branch-এpull.rebase trueবা দলের ঠিক করা নিয়ম।” - (৫) ভালো নিয়ম — মুছে যাওয়া remote branch-এর পুরোনো স্মৃতি জমে না। শুধু জানিয়ে রাখুন যে এটি local branch মোছে না।
- (১) খুব ছোট দলে চলতে পারে, কিন্তু review ছাড়া সব কাজ সরাসরি মূল branch-এ যায়। ভালো: ছোট feature branch আর pull request-এর মাধ্যমে
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমসহকর্মী আগে push করায় আপনার push প্রত্যাখ্যাত হয়েছে। নিরাপদ সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
git fetch— remote-এর নতুন commit আরorigin/mainহালনাগাদgit log main..origin/main— কী এসেছে পড়াgit rebase origin/mainবাgit merge origin/main— দুই কাজ মেলানো- Conflict থাকলে সমাধান করে test চালানো
git push— এবার fast-forward, force ছাড়াই
git fetch origin চালালে নিচের কোনটি বদলাতে পারে?
- Working tree-র ফাইল
- শুধু object database আর
origin/…remote-tracking ref - Local
mainbranch - Remote-এর
mainbranch
একজন developer-এর git status বলছে “Your branch is ahead of 'origin/main' by 1 commit.” অথচ git push প্রত্যাখ্যাত হলো (fetch first) বলে। এটি কীভাবে সম্ভব?
- status সবসময় remote-এর সাথে সরাসরি তুলনা করে, তাই এটি Git-এর bug
- Branch-টির upstream সেট করা নেই
- তাঁর
origin/mainপুরোনো — শেষ fetch-এর পরে অন্য কেউ push করেছেন - Remote repository-টি bare
Local main = G, origin/main = D. Parent সম্পর্ক: B(A), C(B), D(C), E(B), G(E). git rev-list --left-right --count main...origin/main-এর ফল কী?
- 2 আর 2
- 4 আর 4
- 2 আর 1
- 0 আর 2
দুই পথ আলাদা হয়ে গেছে, আর pull.rebase বা pull.ff কোনোটিই সেট নেই। Git ২.৫৩-এ শুধু git pull দিলে কী হয়?
- নীরবে একটি merge commit বানায়
- নিজে থেকে rebase করে
- Remote-এর branch-কে local-এর মতো করে দেয়
- একটি hint দেখিয়ে fatal error দিয়ে থামে
রহিম নিজের feature branch amend করেছেন, কিন্তু fetch করেননি। এর মধ্যে সহকর্মী সেই branch-এ একটি commit push করেছেন। রহিম git push --force-with-lease দিলে কী হবে?
- সহকর্মীর commit মুছে রহিমের branch বসবে
- প্রত্যাখ্যাত হবে
(stale info)বলে - Git নিজে থেকে দুই commit merge করে push করবে
- Option-টি অচেনা বলে error
Remote-এ কেউ feature/x মুছে দিয়েছেন। আপনার git branch -vv-তে local feature/x-এর পাশে [origin/feature/x: gone] দেখাতে হলে কী করতে হবে?
- শুধু
git fetch git push origin --delete feature/xgit fetch --prunegit branch -D feature/x
একটি নতুন branch feature/y-এ প্রথমবার git push -u origin feature/y দেওয়ার স্থায়ী ফল কোনটি?
- Branch-টির upstream config সেট হয়, ফলে পরে শুধু
git push/git pullচলে - ভবিষ্যতের সব push force push হবে
- Repository-র সব branch একসাথে push হবে
- Remote-এর default branch
feature/yহয়ে যাবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Remote = নাম + ঠিকানা; clone-এ প্রথাগত নাম
origin. Clone বানায় remote, refspec, প্রতিটি remote branch-এরorigin/…আর default branch-এর একটি tracking local branch. origin/mainremote-এর শেষ দেখা অবস্থার read-only স্মৃতি; সরে শুধু fetch, pull আর সফল push-এ। Status-এর ahead/behind এর সাথেই তুলনা — আগে fetch.- fetch নিরাপদ (শুধু object আর remote-tracking ref); pull = fetch + merge /
--rebase/--ff-only; দুই পথ আলাদা হলে Git ২.৫৩ বেছে নিতে বলে। - Push শুধু fast-forward হলে গৃহীত; rejected হলে fetch → merge বা rebase → push.
-uবাpush.autoSetupRemoteupstream সেট করে;push origin --deleteআরfetch --pruneমোছা branch সামলায়। - Force লাগলে
--force-with-lease --force-if-includes, শুধু নিজের branch-এ। HTTPS-এ token + credential helper, SSH-এ passphrase-সহ key; কোনো গোপন কথা URL বা prompt-এ নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৫ · Rebase — history নতুন করে সাজানো, interactive rebase আর merge বনাম rebaseপরবর্তী পর্ব
pull --rebase-এর ভেতরে কী ঘটে, আর published history কেন rebase করবেন না। - পর্ব ১৩ · Merge আর conflict — fast-forward, three-way merge আর conflict সমাধানআগের পর্বpull-এর merge অংশ আর fast-forward-এর শর্ত।
- পর্ব ১৬ · Pull request আর fork — দলের কাজের ধারা, review আর merge-এর নিয়মএই মডিউলেFork,
upstreamremote আর review-সহ merge. - সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Computer Networks · TLS আর HTTPSপাশের ট্র্যাকGit-এর HTTPS transport-এর নিচে কী এনক্রিপশন চলে।
- DSA · Graphগভীরে যানReachability আর DAG — ahead/behind হিসাবের গণিত।