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

Merge-এর ভেতরে — merge base, ort strategy, diff3 আর rerere

Git Merge Internals — Merge Bases, Strategies and Options, Index Stages, merge-tree and rerere

Taught in Bangla, this lesson goes inside Git merges: merge-base with --all and --is-ancestor, criss-cross histories and virtual merge bases, the ort, resolve, octopus, ours and subtree strategies, the difference between -s ours and -X ours, rename detection, index stages 1 to 3 during conflicts, git merge-file and merge-tree, rerere, and custom merge drivers.

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

  • git merge-base-এর --all, --octopus আর --is-ancestor; criss-cross-এ কেন দুটি merge base, আর virtual base কীভাবে তৈরি হয়
  • Merge strategy: ort, resolve, octopus, ours, subtree — কোনটি কখন, আর কোনটি কী করতে রাজি নয়
  • -X ours, -X theirs, -s ours-এর সঠিক পার্থক্য; whitespace, renormalize, diff algorithm আর rename-এর option
  • Conflict চলাকালে index-এর stage পড়া, checkout --ours বনাম -X ours, আর merge-file ও merge-tree দিয়ে নিচের স্তরে merge
  • rerere-এর পুরো জীবনচক্র, .gitattributes-এ merge driver (union আর নিজস্ব driver), আর binary ফাইলের conflict

১ · Merge base নিখুঁতভাবে — merge-base, --all, --is-ancestor

পর্ব ১৩-এ (Merge আর conflict — fast-forward, three-way merge আর conflict সমাধান) আপনি দেখেছেন, three-way merge তিনটি সংস্করণ মেলায়: ours, theirs আর তাদের merge base. সেখানে fast-forward, conflict marker, diff3/zdiff3 আর --abort শেখা হয়ে গেছে। এই পর্বে আমরা ঢাকনা খুলব: Git ঠিক কোন commit-কে base ধরে, base একাধিক হলে কী করে, কোন “strategy” কোন কাজ করে, আর conflict চলাকালে ভেতরে কী জমা থাকে। পর্ব ৫২-এর (Git-এর ভেতরে — blob, tree, commit আর content-addressed storage) blob আর tree-র ধারণা এখানে কাজে লাগবে; না পড়ে থাকলেও চলবে — শুধু মনে রাখুন, tree মানে একটি folder-এর পুরো ছবি।

একটি পারিবারিক বংশতালিকার কথা ভাবুন। দুই চাচাতো ভাইয়ের “সবচেয়ে কাছের common পূর্বপুরুষ” সাধারণত একজন — তাঁদের দাদা। কিন্তু দুই পরিবারের মধ্যে যদি আড়াআড়ি বিয়ে হয় — এক পরিবারের মেয়ে অন্য পরিবারে, আর অন্য পরিবারের মেয়ে প্রথম পরিবারে — তখন পরের প্রজন্মের দুজনের “সবচেয়ে কাছের common পূর্বপুরুষ” দুজন হতে পারেন, কেউ কারও চেয়ে বেশি কাছের নন। Git-এর commit graph-এ এই আড়াআড়ি বিয়ের নাম criss-cross merge.

সংজ্ঞাটি আবার নিখুঁতভাবে বলা যাক। Commit history একটি DAG. দুটি commit X আর Y-এর common ancestor হলো এমন commit, যেখানে X আর Y দুটি থেকেই parent ধরে পেছনে হেঁটে পৌঁছানো যায় (নিজেও নিজের ancestor). এদের মধ্যে যে commit অন্য কোনো common ancestor-এর ancestor নয়, সেটি best common ancestor. Tree-তে এমন commit একটিই, কিন্তু DAG-এ একাধিক থাকতে পারে — কারণ merge commit-এর দুটি parent. LCA-র এই সাধারণ রূপটিই Git-এর merge base.

নিচের পরীক্ষার repository-টি এই পর্বের জন্য বানানো (Git ২.৫৩, লেখক “Rahim”, প্রতিটি commit-এর সময় ২০২৬-০১-১৫ সকাল ১০:০১ +06:00 থেকে এক মিনিট করে বাড়ানো — তাই hash আপনার কাছেও একই আসবে)। ফাইল f.txt-এ পাঁচটি লাইন: a = 0, তিনটি keep লাইন, আর b = 0. Commit A থেকে দুই branch: main-এ B (a = 1), side-এ C (b = 1). তারপর আড়াআড়ি merge — main-এ side merge হলো (M1), আর প্রায় একই সময়ে side-এ পুরোনো main অর্থাৎ B merge হলো (M2). শেষে main-এ D (a = 2), side-এ E (b = 2).

$ git log --oneline --graph --all
* e104383 E: b = 2
*   773fa0c M2: merge B into side
|\
| | * 26043ea D: a = 2
| | * 998a27d M1: merge side into main
| |/|
| |/
|/|
* | c567f5e C: b = 1
| * 612c0ad B: a = 1
|/
* dacb7cf A: start
$ git merge-base main side
612c0ad4ae5ce87bcf7d225f1a44871df2ef829d
$ git merge-base --all main side
612c0ad4ae5ce87bcf7d225f1a44871df2ef829d
c567f5ecf952ffb4358a7fed9ace34c3b4c8bb8a
$ git merge-base --is-ancestor 612c0ad side && echo yes || echo no
yes
$ git merge-base --is-ancestor main side && echo yes || echo no
no

Text graph-টি জট পাকানো — criss-cross সবসময় এমন দেখায়। মূল কথা তিনটি। (১) সাধারণ git merge-base একটি উত্তর দিল: 612c0ad (B). (২) --all দিলে দুটি: B আর c567f5e (C). Documentation স্পষ্ট বলে, --all ছাড়া একাধিক best base থাকলে কোনটি ছাপা হবে তা নির্দিষ্ট নয় — তাই script-এ এক উত্তরের উপর ভরসা করবেন না। (৩) --is-ancestor কিছু ছাপে না, শুধু exit code দেয়: 0 মানে “হ্যাঁ, প্রথমটি দ্বিতীয়টির ancestor”, 1 মানে “না”। B হলো side-এর ancestor (M2 দিয়ে), কিন্তু main (D) নয় — অর্থাৎ fast-forward অসম্ভব। পুরোনো script-এ test "$A" = "$(git merge-base A B)" জাতীয় তুলনা দেখবেন; আধুনিক Git-এ --is-ancestor সরাসরি আর নির্ভরযোগ্য।

Criss-cross history — দুটি merge base

M1 and M2 merge B and C in opposite directions, so D and E have two best common ancestors

Criss-cross merge-এ দুটি merge base: B আর C ABC M1M2DE dacb7cf 612c0adc567f5e 998a27d773fa0c 26043eae104383 HEAD → main side কমলা বৃত্ত = merge base (দুটি, সমান “কাছের”)
ছবি ৫৪.১ — তীর child থেকে parent-এর দিকে। M1-এর parent B আর C; M2-এর parent C আর B. ফলে D আর E-র common ancestor A, B, C — কিন্তু A বাদ, কারণ A নিজে B আর C-র ancestor. B আর C কেউ কারও ancestor নয়, তাই দুজনেই “best”.

--octopus আর তিন commit-এর merge base

git merge-base-কে দুইয়ের বেশি commit দিলে অর্থ একটু অদ্ভুত: git merge-base A B C হিসাব করে A আর একটি কাল্পনিক commit-এর merge base, যেটি B আর C-কে merge করে বানানো। সব commit-এর একসাথে common ancestor চাইলে --octopus দিতে হয় — octopus merge-এর আগে Git নিজে এটাই খোঁজে (অংশ ৩)। আর --independent দেওয়া commit-গুলোর মধ্যে সেগুলোই ছাপে, যেগুলো অন্য কোনোটি থেকে পৌঁছানো যায় না। এই পার্থক্য documentation-এর (git help merge-base) উদাহরণ দিয়ে একবার নিজে মিলিয়ে নিন; interview-তে প্রশ্নটি ফাঁদ হিসেবে আসে।

Git কীভাবে এত দ্রুত খোঁজে? পর্ব ১৩-এর প্রোগ্রাম দুই পক্ষের সব ancestor-এর set বানিয়েছিল — লাখো commit-এর repository-তে সেটি ধীর। Git দুই commit থেকে একসাথে পেছনে হাঁটে, প্রতিটি commit-এ “X থেকে পৌঁছেছি”, “Y থেকে পৌঁছেছি” চিহ্ন দেয়, আর নতুন commit আগে দেখে (commit-graph ফাইল থাকলে generation number দিয়ে) — দুই চিহ্নই পাওয়া প্রথম commit-গুলো প্রার্থী। তারপর প্রার্থীদের মধ্যে কেউ অন্যের ancestor কি না, তা বাছাই হয়। খুঁটিনাটি সংস্করণ-ভেদে বদলেছে; ফল একই।

২ · Criss-cross আর virtual merge base — base-গুলোকে আগে merge করা

দুটি merge base থাকলে কোনটি নিয়ে three-way merge করবেন? একটি বেছে নিলে অন্যটির পরের বদল ভুলভাবে “নতুন” মনে হতে পারে। ort (আর তার আগের recursive) strategy-র উত্তর চতুর: base-গুলোকেই আগে merge করুন। B আর C-কে তাদের নিজেদের merge base (A) দিয়ে three-way merge করলে একটি নতুন tree পাওয়া যায় — এটি কোনো branch-এ নেই, কোনো commit হিসেবে থাকে না। একে বলে virtual merge base. তারপর সেটিকে base ধরে D আর E-র আসল merge হয়। Base-গুলো merge করতে গিয়ে আবার একাধিক base পেলে একই কাজ আবার — এ কারণেই পুরোনো নাম “recursive”.

f.txt-এর দুটি গুরুত্বপূর্ণ লাইন (a আর b)
কী মেলানো হচ্ছে
base
ours
theirs
ফল
git merge sideGit ২.৫৩, ort
Auto-merging f.txt
Merge made by the 'ort' strategy.
 f.txt | 2 +-
git merge -s resolve sideGit ২.৫৩
Trying simple merge.
Simple merge failed, trying Automatic merge.
ERROR: content conflict in f.txt
  1. লক্ষ্য। main (D: a = 2, b = 1)-এ side (E: a = 1, b = 2) merge করতে চাই। মানুষের চোখে উত্তর স্পষ্ট: main a বদলেছে, side b বদলেছে — ফল a = 2, b = 2. কিন্তু Git-কে আগে base ঠিক করতে হবে।
  2. Base খোঁজা। D আর E-র common ancestor A, B আর C. A বাদ পড়ে, কারণ সে B আর C দুজনেরই ancestor. B আর C-র কেউ কারও ancestor নয় — তাই merge-base --all দুটিই দেখায়।
  3. Base-গুলোর merge. ort B আর C-কে তাদের base A দিয়ে মেলায়: a লাইন শুধু B বদলেছে (0 → 1), b লাইন শুধু C বদলেছে (0 → 1). ফল V: a = 1, b = 1. এটি একটি tree মাত্র, কোনো branch-এ commit নয় — virtual merge base.
  4. আসল merge. এবার base V. a: V-তে 1, D-তে 2, E-তে 1 → শুধু ours বদলেছে → 2. b: V-তে 1, D-তে 1, E-তে 2 → শুধু theirs বদলেছে → 2. পরিষ্কার merge, ঠিক মানুষের প্রত্যাশামতো।
  5. এক base বাছলে কী হতো। পুরোনো resolve strategy একটিমাত্র base নেয়; এখানে B (a = 1, b = 0). তখন b লাইনে base 0, ours 1, theirs 2 — দুই পক্ষই “বদলেছে” বলে মনে হয় → conflict. অথচ ours-এর b = 1 আসলে C থেকে আগেই মেলানো পুরোনো বদল। এই অপ্রয়োজনীয় conflict-ই virtual base এড়ায়।

এবার আসল Git দিয়ে একই কাজ। Virtual base নিজের চোখে দেখতে অংশ ৭-এর git merge-tree দিয়ে B আর C-কে মেলানো হয়েছে; তারপর একটি পরীক্ষার branch-এ resolve, আর main-এ সাধারণ (ort) merge:

$ git merge-tree --write-tree 612c0ad c567f5e
491859d986dafd0d8c1de8dd7245134729c092ad
$ git show $(git merge-tree --write-tree 612c0ad c567f5e):f.txt
a = 1
keep 1
keep 2
keep 3
b = 1
# (git switch -c try-resolve)
$ git merge -s resolve side
Trying simple merge.
Simple merge failed, trying Automatic merge.
Auto-merging f.txt
ERROR: content conflict in f.txt
fatal: merge program failed
Automatic merge failed; fix conflicts and then commit the result.
$ cat f.txt
a = 2
keep 1
keep 2
keep 3
<<<<<<< .merge_file_q5jRne
b = 1
=======
b = 2
>>>>>>> .merge_file_01p3fB
# (git merge --abort; git switch main)
$ git merge side
Auto-merging f.txt
Merge made by the 'ort' strategy.
 f.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ cat f.txt
a = 2
keep 1
keep 2
keep 3
b = 2

প্রথম দুটি command দেখাচ্ছে B আর C-র merge-এর tree 491859d, যার f.txt-এ a = 1, b = 1 — stepper-এর V হুবহু। resolve একটি base (B) নিয়ে b লাইনে অপ্রয়োজনীয় conflict দিল; marker-এর পাশে branch-এর নামের বদলে .merge_file_… নামের অস্থায়ী ফাইলের নাম — এই নাম প্রতিবার ভিন্ন হয়, আপনার কম্পিউটারেও অন্য অক্ষর দেখবেন। ort virtual base ব্যবহার করে পরিষ্কার merge করল: a = 2, b = 2. Git-এর documentation বলে, Linux kernel-এর আসল merge-এর উপর পরীক্ষায় এই পদ্ধতি কম conflict দিয়েছে, ভুল merge না বাড়িয়ে।

এবার একই ধারণা একটি ছোট প্রোগ্রামে, ঠিক এই repository-র মান দিয়ে। পর্ব ১৩-এর merge_base.js-এ শুধু base খোঁজা হয়েছিল; এখানে base খোঁজার পরে virtual base বানানো আর দুই পদ্ধতির ফল তুলনা করা হয়েছে। লাইন মেলানোর কাজ সরল রাখতে প্রতিটি লাইনকে একটি key (a, b) ধরা হয়েছে:

// Criss-cross: all merge bases, then the virtual merge base that ort/recursive builds.
const parents = { A: [], B: ["A"], C: ["A"], M1: ["B", "C"], M2: ["C", "B"], D: ["M1"], E: ["M2"] };
const file = { A: { a: 0, b: 0 }, B: { a: 1, b: 0 }, C: { a: 0, b: 1 },   // content of f.txt
  M1: { a: 1, b: 1 }, M2: { a: 1, b: 1 }, D: { a: 2, b: 1 }, E: { a: 1, b: 2 } };
function ancestors(c, seen = new Set()) {           // c and everything reachable through parents
  if (!seen.has(c)) { seen.add(c); parents[c].forEach(p => ancestors(p, seen)); }
  return seen;
}
const isAnc = (x, y) => ancestors(y).has(x);          // like git merge-base --is-ancestor x y
function mergeBases(x, y) {                           // best common ancestors
  const common = [...ancestors(x)].filter(c => ancestors(y).has(c));
  return common.filter(c => !common.some(d => d !== c && isAnc(c, d)));
}
function merge3(base, ours, theirs) {                 // key by key, like line by line
  const out = {};
  for (const k in base) {
    if (ours[k] === theirs[k] || theirs[k] === base[k]) out[k] = ours[k];
    else if (ours[k] === base[k]) out[k] = theirs[k];
    else out[k] = "CONFLICT(" + ours[k] + "|" + theirs[k] + ")";
  }
  return out;
}
const show = o => Object.entries(o).map(([k, v]) => k + "=" + v).join(" ");
const bases = mergeBases("D", "E");
console.log("merge bases of D and E:", bases.join(", "));
console.log("B ancestor of C?", isAnc("B", "C"), "| C ancestor of B?", isAnc("C", "B"));
console.log("common ancestors:", [...ancestors("D")].filter(c => ancestors("E").has(c)).join(" "));
const inner = mergeBases(bases[0], bases[1]);          // the bases' own merge base
const virtual = merge3(file[inner[0]], file[bases[0]], file[bases[1]]);
console.log("virtual base = merge(B, C) over " + inner + ":", show(virtual));
console.log("ort with virtual base :", show(merge3(virtual, file.D, file.E)));
console.log("resolve, picks base B :", show(merge3(file.B, file.D, file.E)));

আউটপুট:

merge bases of D and E: B, C
B ancestor of C? false | C ancestor of B? false
common ancestors: B A C
virtual base = merge(B, C) over A: a=1 b=1
ort with virtual base : a=2 b=2
resolve, picks base B : a=2 b=CONFLICT(1|2)

কেন দুটি base: common ancestor তিনটি (B, A, C); A বাদ, কারণ isAnc("A", "B") সত্য। B আর C-র মধ্যে কেউ কারও ancestor নয় (false, false) — তাই কাউকেই বাদ দেওয়া যায় না। Virtual base: B আর C-কে তাদের base A দিয়ে মেলালে a=1 b=1 — আসল Git-এর 491859d tree-র সাথে মিলে গেল। শেষ দুই লাইন আসল Git-এর ort আর resolve-এর ফলের হুবহু প্রতিচ্ছবি। নিজে পরীক্ষা করুন: file.C-তে a: 5 দিন — তখন virtual base বানাতেই conflict হবে। আসল ort এমন হলে virtual base-এর ভেতরেই conflict marker রেখে দেয় (সাধারণের চেয়ে লম্বা marker দিয়ে, যাতে বাইরের marker-এর সাথে না গুলায়), আর সেই অংশ আসল merge-এ সাধারণত conflict হিসেবে আপনার সামনে আসে।

Virtual base জাদু নয়। Documentation নিজেই একটি চেনা বিস্ময়ের কথা বলে: কোনো বদল দুই branch-এই করা হলো, পরে এক branch-এ revert করা হলো — merge-এর ফলে বদলটি থেকে যায়। কারণ merge শুধু দুই tip আর base দেখে, মাঝের commit-গুলো নয়; revert করা branch-এর tip base-এর মতোই দেখায়, তাই সেটিকে “কোনো বদল নেই” ধরা হয়। Criss-cross-এ এমন বিস্ময় বাড়ে। সবচেয়ে ভালো ওষুধ হলো criss-cross কম তৈরি করা: main থেকে branch-এ আনা চলুক, কিন্তু branch থেকে main-এ শুধু pull request-এর শেষে।

৩ · Merge strategy — ort, resolve, octopus, ours আর subtree

“Strategy” মানে merge-এর পুরো পদ্ধতি: কয়টি branch নিতে পারে, base একাধিক হলে কী করে, rename চেনে কি না। git merge -s <নাম> দিয়ে বাছা হয়; কিছু না বললে দুই branch-এর জন্য ort, দুইয়ের বেশির জন্য octopus. পর্ব ১৩-এর বার্তা Merge made by the 'ort' strategy. এখান থেকেই আসে।

Strategyকয়টি headএকাধিক baseRenameকখন
ort২virtual baseচেনেDefault (Git ২.৩৪ থেকে); প্রায় সব কাজে
recursive২virtual baseচেনেv0.99.9k থেকে ২.৩৩ পর্যন্ত default; ২.৫০ থেকে ort-এরই আরেক নাম
resolve২একটি বেছে নেয়, সতর্কভাবেচেনে নাপুরোনো, সরল; আজ খুব কম দরকার
octopus৩ বা বেশি——অনেক branch একসাথে, conflict নেই এমন অবস্থায়
oursযেকোনোদেখেই না—পুরোনো branch-কে “বাতিল হিসেবে merge করা” (অংশ ৪)
subtree২ort-এর মতোচেনেঅন্য project একটি sub-folder-এ বসানো থাকলে (পর্ব ১৯)

আমাদের জানা মতে ort লিখেছেন Elijah Newren, কয়েক বছরের কাজ শেষে Git ২.৩৩-এ এটি বিকল্প হিসেবে আসে আর ২.৩৪-এ (২০২১) default হয়। নামটি একটি সংক্ষিপ্ত রূপ — documentation-এর ভাষায় “Ostensibly Recursive's Twin”, অর্থাৎ recursive-এর “আপাত যমজ”: একই ধারণা, নতুন করে লেখা কোড, rename-এর ক্ষেত্রে বেশি নির্ভুল আর বড় repository-তে অনেক দ্রুত। Git ২.৫৩-এ -s recursive দিলে বার্তায় এখনো 'recursive' লেখা আসে, কিন্তু documentation বলছে ২.৫০ থেকে এটি ort-এরই আরেক নাম। আপনার Git-এ কী সত্য, git help merge-এর “MERGE STRATEGIES” অংশে দেখে নিন।

Octopus — একসাথে অনেক branch

একটি কাল্পনিক অনলাইন বাজার অ্যাপের release-এর আগে তিনটি ছোট branch তৈরি: docs (নির্দেশিকা), ci (CI-এর config), i18n (বাংলা লেখা)। প্রতিটি আলাদা ফাইল ছুঁয়েছে। এর মধ্যে main-এ README হালনাগাদ হয়েছে। একটি command-এই তিনটি:

$ git merge-base --octopus main docs ci i18n
869be076d4aa9bfdead89538d877166814f7f337
$ git merge docs ci i18n
Trying simple merge with docs
Trying simple merge with ci
Trying simple merge with i18n
Merge made by the 'octopus' strategy.
 GUIDE.md | 1 +
 bn.txt   | 1 +
 ci.yml   | 1 +
 3 files changed, 3 insertions(+)
 create mode 100644 GUIDE.md
 create mode 100644 bn.txt
 create mode 100644 ci.yml
$ git log --oneline --graph
*---.   c2986bb Merge branches 'docs', 'ci' and 'i18n'
|\ \ \
| | | * b3a4c20 Add Bangla strings
| | * | 3686306 Add CI config
| | |/
| * / 26c2b7c Add order guide
| |/
* / a215ca2 Update README
|/
* 869be07 Start
$ git cat-file -p HEAD | grep parent
parent a215ca2a566564422f18778057a0558e840a3950
parent 26c2b7cf4ec3e3889a87ce611da4d6b8f05a8925
parent 3686306e0af29d367150dffe021031439fe67724
parent b3a4c2028c0c6fba100042c66951edaec479f891

একাধিক branch দেওয়ায় Git নিজেই octopus বাছল (-s লাগেনি)। --octopus base হলো 869be07 (“Start”), যেখান থেকে চারটি পথ শুরু। প্রতিটি branch একে একে “simple merge” দিয়ে যোগ হয়েছে, আর ফল একটি commit, যার চারটি parent — প্রথমটি আগের main, তারপর দেওয়ার ক্রমে docs, ci, i18n. Graph-এ *---. চিহ্নটি এই বহু-parent commit-এর প্রতীক। এমন commit-কে বলে octopus merge.

Octopus-এর মূল শর্ত: মানুষের হাতে সমাধান লাগবে এমন জটিল merge সে করবে না। নিচে দুটি branch একই লাইনে ভিন্ন ফি লিখেছে (fee-a: ৫০, fee-b: ৭০)। প্রথমবার দুটিই দেওয়া হলো; দ্বিতীয়বার তাদের পরে আরেকটি নিরীহ branch note:

$ git merge fee-a fee-b
Trying simple merge with fee-a
Trying simple merge with fee-b
Simple merge did not work, trying automatic merge.
Auto-merging shop.conf
ERROR: content conflict in shop.conf
fatal: merge program failed
Automatic merge failed; fix conflicts and then commit the result.
$ git status --short
UU shop.conf
# (git merge --abort; a fresh trial branch)
$ git merge fee-a fee-b note
Trying simple merge with fee-a
Trying simple merge with fee-b
Simple merge did not work, trying automatic merge.
Auto-merging shop.conf
ERROR: content conflict in shop.conf
fatal: merge program failed
Automated merge did not work.
Should not be doing an octopus.
Merge with strategy octopus failed.
$ git status --short

সূক্ষ্ম কিন্তু আসল আচরণ। প্রথম ক্ষেত্রে conflict হলো একেবারে শেষ branch-এ, তাই Git সাধারণ merge-এর মতো conflict রেখে থামল (UU shop.conf) — সমাধান করে commit দিলে তিন-parent-এর merge হবে। দ্বিতীয় ক্ষেত্রে conflict হলো মাঝপথে, পরে আরও branch বাকি; octopus তখন Should not be doing an octopus. বলে পুরো merge বাতিল করে, working tree আগের মতো রাখে (git status --short খালি)। শিক্ষা: octopus “সব পরিষ্কার” অবস্থার জন্য — conflict থাকলে branch-গুলো একটি একটি করে merge করুন।

Octopus কতটা ব্যবহৃত? কিছু বড় project-এ maintainer একসাথে অনেক topic branch মেলাতে এটি ব্যবহার করেন; Linux kernel-এর ইতিহাসে কয়েক ডজন parent-ওয়ালা merge আছে বলে জানা যায়, আর Linus Torvalds এমন অতিরিক্ত বড় octopus-এর সমালোচনাও করেছেন বলে শোনা যায়। দৈনন্দিন দলের কাজে এর দরকার কম: git bisect (পর্ব ৫৫) বা revert-এ বহু-parent commit সামলানো কঠিন।

৪ · -s ours বনাম -X ours — সবচেয়ে চেনা বিভ্রান্তি

দুটি দেখতে প্রায় একই, কিন্তু কাজ সম্পূর্ণ ভিন্ন। -s ours একটি strategy — পুরো পদ্ধতি বদলায়। -X ours একটি strategy option — ort-কে বলে, “conflict হলে আমাদের পক্ষ নিন”। দর্জির উদাহরণে ভাবুন: দুজন মাপ পাঠালেন। -X ours মানে — “যেখানে দুজনের মাপ মেলে না, শুধু সেখানে আমার মাপ নিন; বাকি সব বদল দুজনেরই নিন”। -s ours মানে — “অন্যজনের কাগজ না খুলেই ফেলে দিন, কিন্তু খাতায় লিখে রাখুন যে কাগজটি পাওয়া গেছে”।

পরীক্ষা: shop.conf-এর base-এ ফি ৬০ আর ফ্রি ডেলিভারির সীমা ১০০০। main ফি ৭০ করেছে। promo branch ফি ৫০ আর সীমা ৮০০ করেছে, সাথে নতুন ফাইল banner.txt. অর্থাৎ ফি-র লাইনে আসল conflict, আর সীমা ও ব্যানার promo-র পরিষ্কার বদল। প্রতিটি পরীক্ষা main থেকে খোলা আলাদা একটি পরীক্ষার branch-এ:

# (git switch -c try-default main)
$ git merge promo
Auto-merging shop.conf
CONFLICT (content): Merge conflict in shop.conf
Automatic merge failed; fix conflicts and then commit the result.
$ cat shop.conf
name = Bazar Mart
<<<<<<< HEAD
delivery_fee = 70
=======
delivery_fee = 50
>>>>>>> promo
cod = yes
free_over = 800
# (git merge --abort; git switch -c try-xours main)
$ git merge -X ours promo
Auto-merging shop.conf
Merge made by the 'ort' strategy.
 banner.txt | 1 +
 shop.conf  | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 banner.txt
$ cat shop.conf
name = Bazar Mart
delivery_fee = 70
cod = yes
free_over = 800
# (git switch -c try-xtheirs main)
$ git merge -X theirs promo
Auto-merging shop.conf
Merge made by the 'ort' strategy.
 banner.txt | 1 +
 shop.conf  | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)
 create mode 100644 banner.txt
$ cat shop.conf
name = Bazar Mart
delivery_fee = 50
cod = yes
free_over = 800
# (git switch -c try-sours main)
$ git merge -s ours promo
Merge made by the 'ours' strategy.
$ cat shop.conf
name = Bazar Mart
delivery_fee = 70
cod = yes
free_over = 1000
$ ls
shop.conf
$ git diff main try-sours --stat
$ git log --oneline --graph -3
*   77cd777 Merge branch 'promo' into try-sours
|\
| * d5cf90a Promo: fee 50, free over 800, banner
* | 4779563 Raise delivery fee to 70
|/
$ git merge -s theirs promo
Could not find merge strategy 'theirs'.
Available strategies are: octopus ours recursive resolve subtree.
  • Default: ফি-র লাইনে conflict, কিন্তু free_over = 800 নিজে থেকেই মিলেছে।
  • -X ours: শুধু conflict-ওয়ালা hunk-এ ours (ফি ৭০); promo-র পরিষ্কার বদল — সীমা ৮০০ আর banner.txt — ঢুকেছে।
  • -X theirs: উল্টো — conflict-এ theirs (ফি ৫০), বাকি সব একই। Stat-এ 4 ++--, কারণ এবার দুটি লাইন বদলেছে।
  • -s ours: ফাইল হুবহু main-এর মতো (সীমা ১০০০), banner.txt নেই, আর git diff main try-sours --stat কিছুই দেখায় না — tree একই। তবুও graph-এ 77cd777 একটি আসল merge commit, দ্বিতীয় parent d5cf90a. এখন থেকে Git ভাববে promo “merge হয়ে গেছে” — পরে আবার merge করলে তার পুরোনো বদল আর আসবে না।
  • -s theirs নেই: Git সরাসরি জানায় এমন strategy খুঁজে পায়নি। (লক্ষ করুন, ২.৫৩-এর এই তালিকায় ort নাম নেই, যদিও git merge -s ort ঠিকই কাজ করে আর সেটিই default.)
CommandConflict-এর hunkঅন্য পক্ষের পরিষ্কার বদলনতুন ফাইল (theirs)History
git merge Xmarker, আপনি ঠিক করবেনআসেআসেmerge commit (সমাধানের পরে)
-X oursoursআসেআসেmerge commit
-X theirstheirsআসেআসেmerge commit
-s oursপ্রশ্নই ওঠে নাআসে নাআসে নাmerge commit; X “merge হয়েছে” বলে গণ্য

একই চারটি ফল একটি ছোট প্রোগ্রামে, যাতে নিয়মটি হাতে-কলমে দেখা যায়। সরলতার জন্য ধরা হয়েছে লাইন শুধু বদলায়, যোগ বা বাদ হয় না (লাইন যোগ-বিয়োগসহ পূর্ণ LCS-ভিত্তিক সংস্করণ পর্ব ১৩-এর merge3.js-এ)। পাশাপাশি বদলানো লাইন এক hunk হয় — পর্ব ১৩-এর নিয়মমতো:

// -X ours / -X theirs (OPTIONS) vs -s ours (a STRATEGY). Simplified: lines are only replaced.
function merge3(base, ours, theirs, opt, name) {
  const out = []; let conflicts = 0;
  for (let i = 0, j; i < base.length; i = j) {
    j = i;                                       // a hunk = a run of lines changed by anyone
    while (j < base.length && (ours[j] !== base[j] || theirs[j] !== base[j])) j++;
    if (j === i) { out.push(base[i]); j = i + 1; continue; }
    const b = base.slice(i, j).join("\n"), o = ours.slice(i, j).join("\n"), t = theirs.slice(i, j).join("\n");
    if (o === b) out.push(t);                    // only theirs changed this hunk
    else if (t === b || o === t) out.push(o);    // only ours, or both the same
    else if (opt === "ours") out.push(o);        // -X ours: conflicting hunk only
    else if (opt === "theirs") out.push(t);      // -X theirs: conflicting hunk only
    else { conflicts++; out.push("<<<<<<< HEAD\n" + o + "\n=======\n" + t + "\n>>>>>>> " + name); }
  }
  return { text: out.join("\n"), conflicts };
}
function mergeTrees(base, ours, theirs, mode) {  // a tree = { path: [lines] }
  if (mode === "-s ours") return { tree: ours, conflicts: 0 };   // ignore theirs entirely
  const tree = {}; let conflicts = 0;
  for (const p of new Set([...Object.keys(ours), ...Object.keys(theirs)])) {
    if (!base[p]) { tree[p] = (ours[p] || theirs[p]); continue; }  // added on one side only
    const r = merge3(base[p], ours[p], theirs[p], mode.replace("-X ", ""), "promo");
    tree[p] = r.text.split("\n"); conflicts += r.conflicts;
  }
  return { tree, conflicts };
}
const base = { "shop.conf": ["name = Bazar Mart", "delivery_fee = 60", "cod = yes", "free_over = 1000"] };
const ours = { "shop.conf": ["name = Bazar Mart", "delivery_fee = 70", "cod = yes", "free_over = 1000"] };
const theirs = { "shop.conf": ["name = Bazar Mart", "delivery_fee = 50", "cod = yes", "free_over = 800"],
                 "banner.txt": ["Eid sale!"] };
for (const mode of ["default", "-X ours", "-X theirs", "-s ours"]) {
  const r = mergeTrees(base, ours, theirs, mode);
  console.log("=== " + mode + " | files: " + Object.keys(r.tree).join(", ") + " | conflicts: " + r.conflicts);
  console.log(r.tree["shop.conf"].join("\n"));
}

আউটপুট:

=== default | files: shop.conf, banner.txt | conflicts: 1
name = Bazar Mart
<<<<<<< HEAD
delivery_fee = 70
=======
delivery_fee = 50
>>>>>>> promo
cod = yes
free_over = 800
=== -X ours | files: shop.conf, banner.txt | conflicts: 0
name = Bazar Mart
delivery_fee = 70
cod = yes
free_over = 800
=== -X theirs | files: shop.conf, banner.txt | conflicts: 0
name = Bazar Mart
delivery_fee = 50
cod = yes
free_over = 800
=== -s ours | files: shop.conf | conflicts: 0
name = Bazar Mart
delivery_fee = 70
cod = yes
free_over = 1000

চারটি ফলই আসল Git-এর transcript-এর সাথে লাইন ধরে মেলে। পার্থক্যটি কোডেই দেখা যায়: -X শুধু merge3()-এর একেবারে শেষ শাখায় কাজ করে — যেখানে দুই পক্ষ ভিন্নভাবে বদলেছে। -s ours mergeTrees()-এর প্রথম লাইনেই ফিরে যায়; theirs খোলাই হয় না। একটি পরীক্ষা: theirs-এ cod = no করে দেখুন — তখন ফি আর cod পাশাপাশি লাইন বলে এক hunk হবে, আর -X ours সেই পুরো hunk-এ ours নেবে, ফলে theirs-এর cod = no-ও হারাবে। আসল Git-ও hunk ধরে সিদ্ধান্ত নেয়, লাইন ধরে নয়।

কখন কোনটি বৈধ

  • -s ours: documentation-এর ভাষায়, পুরোনো পার্শ্ব-history-কে “বাতিল ঘোষণা” করতে। যেমন একটি পরীক্ষামূলক branch-এর কাজ অন্যভাবে নতুন করে লেখা হয়েছে; branch-টি history-তে রাখতে চান, কিন্তু তার কোড নয়। অথবা পুরোনো release branch-এর একটি hotfix main-এ আগেই অন্যভাবে করা হয়েছে — -s ours দিয়ে “merge হয়েছে” চিহ্ন দিলে পরের merge-এ সেটি আবার আসার চেষ্টা করবে না।
  • -X ours/-X theirs: যন্ত্রে বানানো ফাইল বা যেখানে “এক পক্ষ সবসময় ঠিক” নীতি সত্যিই প্রযোজ্য। সাধারণ কোডে বিপজ্জনক: অন্য পক্ষের conflict-ওয়ালা কাজ নীরবে হারায়, কোনো সতর্কবার্তা ছাড়া, আর পাশের লাইনও এক hunk-এ গিয়ে হারাতে পারে।
  • -s theirs-এর অভাব: কেউ সত্যিই “পুরো tree ওদের” চাইলে প্রচলিত কৌশল হলো উল্টো দিক থেকে -s ours: আগে অন্য branch-এ গিয়ে git merge -s ours main, তারপর main-এ ফিরে সেই branch fast-forward. এটি ইচ্ছাকৃতভাবে কঠিন রাখা — অন্যের সব কাজ মুছে ফেলা সহজ কাজ হওয়া উচিত নয়।

৫ · আরও strategy option আর merge-এ rename খোঁজা

-X দিয়ে ort-কে আরও কিছু নির্দেশ দেওয়া যায়। Documentation (git help merge) অনুযায়ী প্রধানগুলো:

Optionকী করেকখন কাজে লাগে
-X ignore-space-change (আরও ignore-all-space, ignore-space-at-eol, ignore-cr-at-eol)শুধু whitespace-এর বদলকে “বদল নয়” ধরে। এক পক্ষ শুধু indentation বদলালে অন্য পক্ষের আসল বদল জেতে।কেউ পুরো ফাইল reformat করেছে, অন্যজন একটি লাইন বদলেছে
-X renormalizeতিন সংস্করণকেই একবার কাল্পনিক checkout-checkin করিয়ে line ending বা clean filter একরকম করে তারপর মেলায়।এক branch-এ .gitattributes-এর line ending নিয়ম বদলেছে (পর্ব ১০)
-X diff-algorithm=patience (বা histogram, minimal, myers)মেলানোর সময় অন্য diff algorithm. পুরোনো লেখা -X patience এখন এর deprecated প্রতিশব্দ।আলাদা function-এর মিল থাকা } লাইনের কারণে ভুল জায়গায় মেলানো
-X find-renames=<n>Rename খোঁজার similarity threshold ঠিক করে (default চালু, ৫০%)নাম বদলের সাথে বড় বদল থাকলে ধরা বা না-ধরা
-X no-renamesRename খোঁজা বন্ধখুব বড় repository-তে সময় বাঁচাতে, বা ভুল জোড়া এড়াতে
-X subtree=<path>এক পক্ষের tree একটি sub-folder-এ সরিয়ে মেলায়অন্য project একটি folder-এ রাখা থাকলে

Diff algorithm-এর পটভূমি পর্ব ৯-এ (History আর পার্থক্য দেখা) দেখেছেন। একটি খুঁটিনাটি মনে রাখার মতো: documentation বলছে ort merge করার সময় default হিসেবে histogram diff algorithm ব্যবহার করে, অথচ সাধারণ git diff চলে diff.algorithm config অনুযায়ী (না থাকলে myers). তাই git diff-এ যে hunk দেখেন, merge-এর hunk-এর সীমানা কখনো তার থেকে একটু আলাদা হতে পারে।

Rename detection — Git নাম বদল “অনুমান” করে

Git-এর commit নাম বদল আলাদা করে লিখে রাখে না; প্রতিটি commit শুধু একটি পূর্ণ snapshot (পর্ব ৫২)। Merge-এর সময় ort base থেকে প্রতিটি পক্ষের পার্থক্য দেখে: এক পক্ষে একটি path মুছেছে আর নতুন একটি path এসেছে, আর দুটির বিষয়বস্তু যথেষ্ট মেলে — তাহলে এটি rename. “যথেষ্ট” মাপা হয় similarity index দিয়ে, default threshold ৫০%. Rename ধরা পড়লে অন্য পক্ষের পুরোনো নামে করা বদল নতুন নামের ফাইলে বসে যায়।

পরীক্ষা: main-এ price.js-এর নাম pricing.js করা হলো, সাথে নাম গুছানো আর একটি নতুন লাইন। একই সময়ে fix branch পুরোনো price.js-এ ডেলিভারি ফি ৬০ থেকে ৫০ করল:

$ git diff -M --summary HEAD~1 HEAD
 rename price.js => pricing.js (60%)
$ git diff -M --stat HEAD~1 HEAD
 price.js => pricing.js | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
# (git switch -c try-default main)
$ git merge fix
Auto-merging pricing.js
Merge made by the 'ort' strategy.
 pricing.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ grep -n 'return 50' pricing.js
7:  return 50;
# (git switch -c try-strict main)
$ git merge -X find-renames=90% fix
CONFLICT (modify/delete): price.js deleted in HEAD and modified in fix.  Version fix of price.js left in tree.
Automatic merge failed; fix conflicts and then commit the result.
$ git status --short
DU price.js

Git-এর হিসাবে দুই সংস্করণের মিল ৬০%. Default threshold ৫০%-এর বেশি, তাই ort rename ধরল আর fix-এর বদল নতুন নামের ফাইলে বসাল: Auto-merging pricing.js, আর pricing.js-এর ৭ নম্বর লাইনে return 50;. বার্তায় “rename” শব্দটি নেই — rename চুপচাপ সামলানো হয়। Threshold ৯০% করলে ৬০% আর যথেষ্ট নয়: Git-এর চোখে main ফাইলটি মুছেছে আর নতুন একটি ফাইল যোগ করেছে, আর fix পুরোনো ফাইল বদলেছে — তাই modify/delete conflict (DU = deleted by us, updated by them). -X no-renames দিলেও হুবহু একই conflict আসে। দুই পক্ষ একই ফাইলের দুটি ভিন্ন নাম দিলে ort CONFLICT (rename/rename) জানায়, আর দুই নামই আপনার সামনে রেখে দেয়।

৬০% কোথা থেকে এল? Git ফাইলকে ছোট টুকরো (মোটামুটি লাইন) করে hash করে, তারপর দেখে কত byte দুই সংস্করণে মিলেছে, আর সেটিকে বড় ফাইলের আকার দিয়ে ভাগ করে। নিচের Python প্রোগ্রামে একই ধারণা standard library-র difflib দিয়ে — মিলে যাওয়া লাইনের মোট byte ÷ বড় ফাইলের byte. সাথে একটি অপ্রাসঙ্গিক নতুন ফাইল report.js, যাতে বোঝা যায় Git কেন সবচেয়ে মিল থাকা জোড়াটি বাছে:

# Rename detection: pair a deleted path with an added path if their contents are similar enough.
from difflib import SequenceMatcher
old = {"price.js": """function vat(amount) {
  return amount * 0.15;
}
function delivery(total) {
  if (total >= 1000) return 0;
  return 60;
}
function discount(total) {
  return total * 0.05;
}
function round(x) {
  return Math.round(x);
}
"""}
new_pricing = "const VAT_RATE = 0.15;\n" + old["price.js"].replace("function vat(", "function vatOf(") \
    .replace("amount * 0.15", "amount * VAT_RATE").replace("function round(", "function roundTaka(")
new = {"pricing.js": new_pricing,
       "report.js": "function report(orders) {\n  return orders.length + ' orders';\n}\n"}

def similarity(a, b):        # bytes in matching lines / size of the bigger file (like Git)
    la, lb = a.splitlines(True), b.splitlines(True)
    blocks = SequenceMatcher(None, la, lb).get_matching_blocks()
    same = sum(len("".join(la[k.a:k.a + k.size])) for k in blocks)
    return same / max(len(a), len(b))

for threshold in (0.50, 0.90):
    print(f"threshold {threshold:.0%}:")
    for o, a in old.items():
        for n, b in new.items():
            s = similarity(a, b)
            verdict = "rename" if s >= threshold else "delete + add"
            print(f"  {o} -> {n:<11} similarity {s:4.0%}  {verdict}")
print("sizes:", {k: len(v) for k, v in {**old, **new}.items()})

আউটপুট:

threshold 50%:
  price.js -> pricing.js  similarity  60%  rename
  price.js -> report.js   similarity   1%  delete + add
threshold 90%:
  price.js -> pricing.js  similarity  60%  delete + add
  price.js -> report.js   similarity   1%  delete + add
sizes: {'price.js': 220, 'pricing.js': 253, 'report.js': 64}

হিসাবটি সহজ: পুরোনো ফাইল ২২০ byte, নতুনটি ২৫৩ byte. তিনটি লাইন বদলেছে (vat-এর নাম, 0.15-এর জায়গায় VAT_RATE, round-এর নাম) — সেগুলোর মোট ৬৭ byte. মিলে যাওয়া ২২০ − ৬৭ = ১৫৩ byte; ১৫৩ ÷ ২৫৩ ≈ ০.৬০ = ৬০%, ঠিক Git-এর (60%). ভাগ বড় ফাইল দিয়ে, তাই নতুন লাইন যোগ করলেও মিল কমে। আসল Git-এর হিসাব লম্বা লাইন ভেঙে টুকরো করে আর কিছু খুঁটিনাটিতে আলাদা — তাই সব ক্ষেত্রে এই প্রোগ্রাম হুবহু মিলবে না, কিন্তু ধারণা একই। ব্যবহারিক শিক্ষা: নাম বদল আর বড় সম্পাদনা দুটি আলাদা commit-এ করুন; তাহলে rename-এর commit-এ মিল প্রায় ১০০%, আর merge, log --follow, blame সব ঠিকঠাক কাজ করে।

বড় repository-তে rename: হাজারো ফাইল একসাথে সরালে সম্ভাব্য জোড়ার সংখ্যা বিশাল। Git তাই একটি সীমা রাখে (merge.renameLimit; না থাকলে diff.renameLimit) — সীমা ছাড়ালে সতর্কবার্তা দিয়ে কিছু rename খোঁজা বাদ দেয়। ort-এর একটি বড় উন্নতি ছিল এই খোঁজা অনেক দ্রুত করা, যেমন একই নামের ফাইল অন্য folder-এ গেলে আগে সেটি মিলিয়ে দেখা। সংখ্যাগুলো সংস্করণ-ভেদে বদলায়; দরকার হলে git help config দেখুন।

৬ · Conflict চলাকালে index — stage 1, 2, 3

পর্ব ১৩-এ এক ঝলক দেখেছেন, conflict-এর সময় index-এ একটি ফাইলের তিনটি entry থাকে। এবার পুরো ছবিটি। Index-এর প্রতিটি entry-তে path আর blob-এর hash-এর পাশে একটি ছোট সংখ্যা থাকে — stage. সাধারণ অবস্থায় সব entry stage 0. Merge কোনো ফাইল পরিষ্কারভাবে মেলাতে না পারলে stage 0 মুছে তিনটি entry লেখে: 1 = base, 2 = ours, 3 = theirs. Working tree-র ফাইলে marker থাকে ঠিকই, কিন্তু “ফাইলটি unresolved” — এই তথ্যের আসল উৎস index-এর এই stage-গুলো। git status-এর UU, git diff-এর combined diff, git commit-এর আপত্তি — সবই এখান থেকে।

একটি কাল্পনিক ফলাফল পোর্টালের marks.txt: base-এ বাংলা ৭৮; অফিস main-এ ৮০ করেছে, আর recheck branch-এ পুনর্নিরীক্ষার পরে ৮২:

marks.txt — index, working tree আর status
Command
Index
Working tree-তে বাংলা
git status --short
  1. Merge-এর আগে। git ls-files -s index-এর প্রতিটি entry দেখায়: mode, blob hash, stage, path. marks.txt stage 0-তে, blob 6ee16a4 (বাংলা ৮০)।
  2. Conflict. git merge recheck-এর পরে stage 0 নেই; তিনটি entry — base 6198895 (৭৮), ours 6ee16a4 (৮০, merge-এর আগের সেই একই blob), theirs 46dc5af (৮২)। git ls-files -u শুধু এমন unmerged entry দেখায়।
  3. প্রতিটি সংস্করণ পড়া। git show :1:marks.txt, :2:, :3: — কোলনের পরের সংখ্যাই stage. Index বদলায় না; শুধু পড়া হলো।
  4. একটি পক্ষ নেওয়া। git checkout --theirs marks.txt stage 3-এর blob working tree-তে লেখে: ফাইলে এখন ৮২, marker নেই। কিন্তু index-এ এখনো তিনটি stage — Git-এর কাছে ফাইলটি এখনো unresolved. Status এখনো UU.
  5. ভুল হলে আবার শুরু। git checkout -m marks.txt stage 1, 2, 3 থেকে conflict আবার তৈরি করে (Recreated 1 merge conflict). লেবেল এবার ours/theirs, branch-এর নাম নয়। হাতে করা সম্পাদনা হারাতে পারে — সাবধানে।
  6. Resolve. আবার --theirs, তারপর git add: তিনটি stage মুছে stage 0-তে 46dc5af. এই blob-টি হুবহু theirs-এর blob, কারণ বিষয়বস্তু একই — content-addressed storage (পর্ব ৫২)। Status M — commit-এর জন্য প্রস্তুত।
$ git ls-files -s
100644 6ee16a42b20f8aa31b5ac040581a4a280505aeef 0	marks.txt
$ git merge recheck
Auto-merging marks.txt
CONFLICT (content): Merge conflict in marks.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git ls-files -u
100644 6198895ad62944f7b3182daedcd17cac80e5eb80 1	marks.txt
100644 6ee16a42b20f8aa31b5ac040581a4a280505aeef 2	marks.txt
100644 46dc5afee4515e03146243e9f38b7cacd2fccb53 3	marks.txt
$ git show :1:marks.txt
roll = 7
name = Karim
bangla = 78
math = 90
$ git checkout --theirs marks.txt
Updated 1 path from the index
$ git ls-files -u
100644 6198895ad62944f7b3182daedcd17cac80e5eb80 1	marks.txt
100644 6ee16a42b20f8aa31b5ac040581a4a280505aeef 2	marks.txt
100644 46dc5afee4515e03146243e9f38b7cacd2fccb53 3	marks.txt
$ git checkout -m marks.txt
Recreated 1 merge conflict
$ cat marks.txt
roll = 7
name = Karim
<<<<<<< ours
bangla = 80
=======
bangla = 82
>>>>>>> theirs
math = 90
$ git checkout --theirs marks.txt
Updated 1 path from the index
$ git add marks.txt
$ git ls-files -s
100644 46dc5afee4515e03146243e9f38b7cacd2fccb53 0	marks.txt

Stepper-এর প্রতিটি ধাপ এখানে আসল আউটপুটে। সবচেয়ে গুরুত্বপূর্ণ জোড়া: git checkout --theirs-এর ঠিক পরের git ls-files -u — তিনটি stage অক্ষত। অর্থাৎ checkout --ours/--theirs (বা Git ২.২৩-এর পরের git restore --ours/--theirs) শুধু working tree বদলায়; “সমাধান হয়েছে” ঘোষণা করে কেবল git add (বা git rm).

কোন conflict-এ কোন stage থাকে

সব conflict-এ তিনটি stage থাকে না। যে সংস্করণের অস্তিত্বই নেই, তার stage-ও নেই — আর এটি দেখেই conflict-এর ধরন চেনা যায়:

Conflictstage 1 (base)stage 2 (ours)stage 3 (theirs)status
content (দুই পক্ষ বদলেছে)আছেআছেআছেUU
add/add (দুই পক্ষ একই নামে নতুন ফাইল)নেইআছেআছেAA
modify/delete, theirs মুছেছেআছেআছেনেইUD
modify/delete, ours মুছেছেআছেনেইআছেDU

checkout --ours বনাম -X ours — সময় আর পরিধি

  • কখন: -X ours merge শুরুর আগে দেওয়া নির্দেশ — conflict তৈরিই হয় না। checkout --ours conflict হয়ে যাওয়ার পরে, একটি নির্দিষ্ট ফাইলে।
  • কতটুকু: -X ours শুধু conflict-ওয়ালা hunk-এ ours নেয়, ফাইলের অন্য পরিষ্কার বদল রাখে। checkout --ours পুরো ফাইলটি stage 2 থেকে লেখে — theirs-এর পরিষ্কারভাবে মেলানো বদলও সেই ফাইল থেকে হারায়।
  • মাঝামাঝি পথ: conflict-এর পরে শুধু একটি ফাইলে “-X ours-এর মতো” ফল চাইলে অংশ ৭-এর git merge-file --ours stage 1, 2, 3 থেকে বানানো যায়।
  • Rebase-এ উল্টো: rebase-এর সময় ours মানে যে branch-এর উপর বসানো হচ্ছে, theirs মানে আপনার নিজের commit (পর্ব ১৫)। Stage 2/3-ও সেই অর্থ মানে।

৭ · নিচের স্তরে merge — git merge-file আর git merge-tree

git merge একটি বড় কাজ: base খোঁজা, প্রতিটি ফাইল মেলানো, index আর working tree লেখা, commit বানানো। Git-এ এর টুকরো টুকরো কাজের জন্য আলাদা plumbing command আছে, যা script, bot আর server-এ কাজে লাগে।

git merge-file — যেকোনো তিনটি ফাইল

git merge-file কোনো commit বা branch চায় না, শুধু তিনটি ফাইল: ours, base, theirs (এই ক্রমে)। একটি চায়ের দোকানের মেনু — base-এ চা ১০, ডিম ১৫; ours চা ১২; theirs চা ১৫ আর ডিম ২০:

$ git merge-file -p ours.txt base.txt theirs.txt; echo "exit=$?"
<<<<<<< ours.txt
tea = 12
=======
tea = 15
>>>>>>> theirs.txt
rice = 50
egg = 20
exit=1
$ git merge-file -p --diff3 -L ours -L base -L theirs ours.txt base.txt theirs.txt
<<<<<<< ours
tea = 12
||||||| base
tea = 10
=======
tea = 15
>>>>>>> theirs
rice = 50
egg = 20
$ git merge-file -p --theirs ours.txt base.txt theirs.txt
tea = 15
rice = 50
egg = 20
$ git merge-file -p --union ours.txt base.txt theirs.txt
tea = 12
tea = 15
rice = 50
egg = 20
# (theirs2.txt changes only egg = 18)
$ git merge-file ours.txt base.txt theirs2.txt; echo "exit=$?"
exit=0
$ cat ours.txt
tea = 12
rice = 50
egg = 18

-p ফল পর্দায় দেখায়; না দিলে ফল ours ফাইলেই লিখে দেয় (শেষ উদাহরণ) — আসল ফাইল নিয়ে চালানোর আগে কপি রাখুন। Exit code = conflict-এর সংখ্যা (০ মানে পরিষ্কার; ঋণাত্মক মান ত্রুটি)। -L marker-এর লেবেল ঠিক করে, --diff3 base-ও দেখায় (--zdiff3-ও আছে)। --theirs conflict-এ theirs নেয় — -X theirs-এর মতোই, ডিম ২০ ঠিকই এসেছে। --union দুই পক্ষের লাইনই রাখে: tea = 12 আর tea = 15 দুটোই — config-এ যা প্রায় সবসময় ভুল। Conflict-এর মাঝে একটি ফাইলের তিন সংস্করণ বের করে মেলাতে পারেন: git show :1:f > base, :2: আর :3: দিয়ে ours আর theirs, তারপর git merge-file. এটি Git-এর বাইরে, যেকোনো তিনটি লেখা মেলানোর জন্যও কাজে লাগে।

git merge-tree --write-tree — working tree না ছুঁয়ে পুরো merge

একটি hosting platform বা merge bot জানতে চায়: “এই pull request কি conflict ছাড়া merge হবে?” Server-এ সাধারণত working tree থাকে না (bare repository), আর হাজারো pull request-এর জন্য checkout করা ধীর। git merge-tree --write-tree ort দিয়ে পুরো merge হিসাব করে, ফলের tree object-টি object store-এ লেখে, আর index, working tree বা কোনো branch ছোঁয় না। আমাদের জানা মতে এই আধুনিক রূপটি এসেছে Git ২.৩৮-এ (২০২২); পুরোনো --trivial-merge রূপটি এখন deprecated. অংশ ৪-এর repository-তে:

$ git merge-tree --write-tree main promo; echo "exit=$?"
e3c4071da68d8b809f25f610d75e4df317371bc0
100644 5beb3cf3ec9caab08c6338306311d4b82dfd6287 1	shop.conf
100644 594a1b00215a233fe15f4bab22c245e9373c1cc7 2	shop.conf
100644 bc71ecf24355c8de85adbf4b0ed1346c33874a25 3	shop.conf

Auto-merging shop.conf
CONFLICT (content): Merge conflict in shop.conf
exit=1
$ git merge-tree --write-tree --name-only main promo
e3c4071da68d8b809f25f610d75e4df317371bc0
shop.conf

Auto-merging shop.conf
CONFLICT (content): Merge conflict in shop.conf
$ git show e3c4071:shop.conf
name = Bazar Mart
<<<<<<< main
delivery_fee = 70
=======
delivery_fee = 50
>>>>>>> promo
cod = yes
free_over = 800
$ git merge-tree --write-tree main readme; echo "exit=$?"
a91166ddc8e527da7ef467d9174d4c0bf42d4091
exit=0
$ git ls-tree $(git merge-tree --write-tree main readme)
100644 blob f4a02e4ca06d4f2b1c39877b68bcf0854db71725	README.md
100644 blob 594a1b00215a233fe15f4bab22c245e9373c1cc7	shop.conf
$ git commit-tree -p main -p readme -m 'Merge readme (server side)' $(git merge-tree --write-tree main readme)
44d31a9711a2988b86d8a28b68c01f41a67f2823
$ git status --short
$ git log --oneline -1
4779563 Raise delivery fee to 70

Conflict-এর ক্ষেত্রে আউটপুটের তিনটি অংশ: প্রথম লাইনে ফলের tree (e3c4071); তারপর conflict-ওয়ালা ফাইলের stage-এর তালিকা — ঠিক git ls-files -u-এর বিন্যাসে; একটি ফাঁকা লাইনের পরে মানুষের পড়ার বার্তা। Exit code 1. --name-only শুধু ফাইলের নাম দেয় — bot-এর জন্য সুবিধাজনক। মজার বিষয়: conflict থাকলেও tree লেখা হয়েছে, আর তার ভেতরের shop.conf-এ marker বসানো (লেবেল branch-এর নাম, HEAD নয়)। Clean ক্ষেত্রে শুধু tree-র hash আর exit 0. git ls-tree দেখাচ্ছে দুই পক্ষের ফাইল একসাথে। তারপর git commit-tree সেই tree আর দুই parent দিয়ে একটি merge commit বানাল (44d31a9) — কিন্তু কোনো branch সেটি দেখায় না; main সরাতে git update-ref লাগবে (অনুশীলন ২)। শেষ দুই লাইন প্রমাণ: working tree পরিষ্কার, main এখনো 4779563-এ।

অনেক hosting platform সার্ভারে pull request-এর “merge করা যাবে কি না” এভাবেই হিসাব করে বলে জানা যায়, যদিও কোন platform ঠিক কোন command ব্যবহার করে তা তাদের ভেতরের ব্যাপার। নিজের CI বা bot-এ (পর্ব ৫৮ — Continuous integration) এই কৌশল সরাসরি কাজে লাগে: merge-এর আগেই conflict ধরা, কোনো checkout ছাড়া।

৮ · rerere — একবারের সমাধান মনে রাখা

একটি দীর্ঘদিনের branch-এর কথা ভাবুন — যেমন একটি release-এর রক্ষণাবেক্ষণের branch, বা একটি বড় feature যা প্রতি সপ্তাহে main-এর সাথে মিলিয়ে দেখা হয় (পরীক্ষামূলক merge), অথবা বারবার rebase করা হয়। প্রতিবার একই conflict: একই লাইন, একই দুই সংস্করণ, একই সমাধান। পরীক্ষার খাতায় একই অঙ্ক বারবার কষার মতো — একবার কষে খাতার মার্জিনে উত্তর লিখে রাখলে পরের বার শুধু মিলিয়ে নিলেই চলে। rerere (“reuse recorded resolution”) ঠিক এই মার্জিনের নোট।

কীভাবে কাজ করে: rerere চালু থাকলে প্রতিটি conflict-এ Git marker-ওয়ালা অংশটি normalize করে — branch-এর লেবেল বাদ দেয় আর দুই পক্ষকে একটি নির্দিষ্ট ক্রমে সাজায়, যাতে ours/theirs-এর অদলবদলে কিছু না বদলায় — তারপর তার hash নেয়। .git/rr-cache-এ সেই hash-নামের folder-এ conflict-এর চেহারা রাখে (preimage). আপনি সমাধান করে commit দিলে সমাধানটিও রাখে (postimage). পরে একই hash-এর conflict এলে postimage বসিয়ে দেয়।

Terminal, .git/rr-cache আর shop.conf
Command
Git-এর বার্তা
rr-cache
shop.conf-এ ফি
index
  1. চালু করা। rerere default-এ বন্ধ। git config rerere.enabled true (সব repository-তে চাইলে --global). প্রথম ব্যবহারে Git নিজেই .git/rr-cache folder বানায়।
  2. প্রথম conflict — preimage. main ফি ৭০, topic ফি ৫০. পরীক্ষার branch-এ merge করতেই conflict, আর নতুন একটি লাইন: Recorded preimage for 'shop.conf'. Conflict-এর normalized রূপের hash 17c3f75…-নামের folder-এ preimage ফাইল।
  3. সমাধান — postimage. দল ঠিক করল ফি ৫৫. Marker মুছে ৫৫ লিখে git add আর git commit; commit-এর সময় Git জানাল Recorded resolution for 'shop.conf'. Folder-এ এখন postimage-ও আছে।
  4. একই conflict আবার — rebase-এ। কিছুদিন পরে topic-কে main-এর উপর rebase. একই লাইনে একই দুই মান, যদিও rebase-এ ours/theirs উল্টো। Normalize করা রূপ একই, তাই hash একই: Resolved 'shop.conf' using previous resolution. ফাইলে আপনা থেকেই ৫৫. তবে index-এ এখনো UU — যাচাইয়ের দায়িত্ব আপনার।
  5. যাচাই করে এগোনো। ফাইল পড়ে নিশ্চিত হয়ে git add, তারপর git rebase --continue. নতুন commit e7042f7-এ ফি ৫৫. চাইলে rerere.autoUpdate চালু করে এই git add-ও স্বয়ংক্রিয় করা যায় — কিন্তু তখন অন্ধ বিশ্বাসের ঝুঁকি বাড়ে।
  6. ভুল সমাধান মুছে ফেলা। পরে বোঝা গেল ৫৫ ভুল ছিল। অন্য একটি merge-এ একই conflict আবার আপনা থেকে মিটলে git rerere forget shop.conf রেকর্ডটি মোছে। ফাইল নিজে বদলায় না — git checkout -m shop.conf দিয়ে marker ফিরিয়ে আনুন, নতুন করে সমাধান করুন; নতুন সমাধানটি আবার রেকর্ড হবে।
$ git config rerere.enabled true
# (git switch -c trial main)
$ git merge topic
Auto-merging shop.conf
CONFLICT (content): Merge conflict in shop.conf
Recorded preimage for 'shop.conf'
Automatic merge failed; fix conflicts and then commit the result.
$ git rerere status
shop.conf
$ git rerere diff
--- a/shop.conf
+++ b/shop.conf
@@ -1,8 +1,8 @@
 name = Bazar Mart
-<<<<<<<
-delivery_fee = 50
-=======
+<<<<<<< HEAD
 delivery_fee = 70
->>>>>>>
+=======
+delivery_fee = 50
+>>>>>>> topic
 cod = yes
 free_over = 1000
$ find .git/rr-cache -type f
.git/rr-cache/17c3f75953c55480055d848cf7dc2996db8b4e4d/preimage
# (edit shop.conf: delivery_fee = 55, remove the markers)
$ git add shop.conf
$ git commit -q --no-edit
Recorded resolution for 'shop.conf'.
$ find .git/rr-cache -type f
.git/rr-cache/17c3f75953c55480055d848cf7dc2996db8b4e4d/postimage
.git/rr-cache/17c3f75953c55480055d848cf7dc2996db8b4e4d/preimage

git rerere status দেখায় কোন ফাইলের preimage রাখা হয়েছে। git rerere diff preimage থেকে এখনকার ফাইলের পার্থক্য দেখায় — আর এখানেই normalize করা রূপটি চোখে পড়ে: preimage-এর marker-এ কোনো লেবেল নেই (<<<<<<< একা), আর ৫০ আগে, ৭০ পরে — যদিও ফাইলে ours (৭০) আগে। দুই পক্ষকে নির্দিষ্ট ক্রমে সাজানো হয়েছে। .git/rr-cache-এর folder-এর নাম সেই normalize করা conflict-এর hash.

# (git switch topic)
$ git rebase main
Rebasing (1/1)Auto-merging shop.conf
CONFLICT (content): Merge conflict in shop.conf
error: could not apply f979c51... Topic: fee 50
hint: Resolve all conflicts manually, mark them as resolved with
…
Resolved 'shop.conf' using previous resolution.
Could not apply f979c51... # Topic: fee 50
$ cat shop.conf
name = Bazar Mart
delivery_fee = 55
cod = yes
free_over = 1000
$ git status --short
UU shop.conf
$ git add shop.conf
$ git rebase --continue
[detached HEAD e7042f7] Topic: fee 50
 1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/topic.
$ git show HEAD:shop.conf
name = Bazar Mart
delivery_fee = 55
cod = yes
free_over = 1000

Rebase থেমেছে conflict-এ (hint লাইনগুলো সংক্ষেপ করা হয়েছে, … চিহ্নে), কিন্তু শেষে Resolved 'shop.conf' using previous resolution. — ফাইলে ইতিমধ্যে ৫৫, কোনো marker নেই। তবুও UU: rerere ফাইল ঠিক করে দেয়, “resolved” ঘোষণা করে না, যাতে আপনি একবার দেখে নেন। তারপর git add আর --continue. একটি সূক্ষ্মতা: এখানে postimage রেকর্ড হয়েছিল merge-এ, আর ব্যবহার হলো rebase-এ — normalize করার সুফল।

একই কৌশল ছোট একটি প্রোগ্রামে। Hash-এর জন্য এখানে FNV-1a ব্যবহার হয়েছে, যা Git-এর আসল hash-এর (SHA-1) প্রতিনিধি মাত্র; দুই পক্ষ sort করে normalize করা হয়েছে, ঠিক উপরের preimage-এর মতো:

// Toy rerere: remember "this conflict -> this resolution" and replay it next time.
function fnv1a(s) {                       // 32-bit FNV-1a; stands in for Git's SHA-1 here
  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");
}
// Normalise like rerere: drop branch labels, sort the two sides so ours/theirs order is irrelevant.
function conflictId(sideA, sideB) {
  const [x, y] = [sideA, sideB].sort();
  return fnv1a("<<<<<<<\n" + x + "\n=======\n" + y + "\n>>>>>>>\n");
}
const rrCache = new Map();                // .git/rr-cache: id -> { preimage, postimage }
function onConflict(where, ours, theirs) {
  const id = conflictId(ours, theirs), entry = rrCache.get(id);
  if (entry && entry.postimage) {
    console.log(where + ": Resolved using previous resolution [" + id + "] -> " + entry.postimage);
    return entry.postimage;
  }
  if (!entry) rrCache.set(id, { preimage: [ours, theirs].sort(), postimage: null });
  console.log(where + ": Recorded preimage [" + id + "], please resolve by hand");
  return null;
}
function onCommit(ours, theirs, resolved) {   // rerere records the postimage when you commit
  rrCache.get(conflictId(ours, theirs)).postimage = resolved;
  console.log("commit: Recorded resolution -> " + resolved);
}
// 1) test merge of topic into main: fee 70 (ours) vs fee 50 (theirs)
if (!onConflict("merge  ", "delivery_fee = 70", "delivery_fee = 50"))
  onCommit("delivery_fee = 70", "delivery_fee = 50", "delivery_fee = 55");
// 2) rebase topic onto main: same conflict, but ours and theirs are swapped
onConflict("rebase ", "delivery_fee = 50", "delivery_fee = 70");
// 3) a different conflict gets a different id
onConflict("merge 2", "delivery_fee = 70", "delivery_fee = 45");
console.log("rr-cache entries:", rrCache.size);

আউটপুট:

merge  : Recorded preimage [ebf582b8], please resolve by hand
commit: Recorded resolution -> delivery_fee = 55
rebase : Resolved using previous resolution [ebf582b8] -> delivery_fee = 55
merge 2: Recorded preimage [3cb6ea46], please resolve by hand
rr-cache entries: 2

Merge আর rebase-এ দুই পক্ষের ক্রম উল্টো, তবুও id একই (ebf582b8), কারণ hash নেওয়ার আগে sort করা হয়েছে — তাই rebase-এ সমাধান আপনা থেকে এল। তৃতীয় conflict-এ একটি মান ভিন্ন (৪৫), তাই নতুন id আর নতুন preimage. পরীক্ষা: conflictId() থেকে .sort() সরিয়ে দিন — rebase-এর লাইনে আর মিল হবে না। আসল rerere marker-এর আশপাশের কিছু খুঁটিনাটিও normalize করে আর এক ফাইলে একাধিক conflict সামলায়; ধারণা একই।

ব্যবহারিক নিয়ম

  • কোথায় সবচেয়ে কাজের: দীর্ঘদিনের branch-এ পরীক্ষামূলক merge (merge করে দেখে, abort করে পরে আবার), বারবার rebase, আর বহু branch থেকে প্রতিদিন একটি integration branch নতুন করে বানানো।
  • git rerere remaining দেখায় কোন ফাইল rerere মেটাতে পারেনি; git rerere clear চলমান merge-এর অর্ধেক রেকর্ড মোছে।
  • পুরোনো রেকর্ড: git rerere gc (আর git gc) default-এ ১৫ দিনের বেশি পুরোনো অমীমাংসিত আর ৬০ দিনের বেশি পুরোনো মীমাংসিত রেকর্ড মোছে (gc.rerereUnresolved, gc.rerereResolved).
  • ঝুঁকি: rerere ভুল সমাধানও একইভাবে মনে রাখে আর চুপচাপ বসায়। প্রতিবার git diff দেখে git add দিন, আর merge-এর পরে test চালান।

৯ · Merge driver আর binary ফাইলের conflict

ফাইলের তিন সংস্করণ মেলানোর ফাইল-স্তরের পদ্ধতিকে বলে merge driver. পর্ব ১০-এ (.gitignore, .gitattributes আর git config) দেখেছেন, .gitattributes path ধরে Git-কে নির্দেশ দেয়; merge=… attribute দিয়ে driver বাছা হয়। Built-in driver তিনটি: text (সাধারণ লাইনভিত্তিক three-way, marker-সহ), binary (ours-এর সংস্করণ working tree-তে রেখে ফাইলটি conflict অবস্থায় রাখে), আর union (conflict-এ দুই পক্ষের লাইনই রাখে)। নিজস্ব driver config-এ লেখা হয়: merge.<নাম>.driver-এ একটি command, যেখানে %O = base, %A = ours (ফল এখানেই লিখতে হবে), %B = theirs-এর অস্থায়ী ফাইল, আর %P = আসল path. Command 0 দিয়ে শেষ হলে পরিষ্কার merge, অন্য কিছু হলে conflict.

পরীক্ষা: একটি repository-তে তিনটি ফাইল — changelog (CHANGELOG.md), একটি ছবি (logo.png, binary), আর একটি lockfile (app.lock). দুই branch তিনটিই বদলেছে। প্রথমে কোনো attribute ছাড়া, তারপর attribute দিয়ে:

$ git merge feat
Auto-merging CHANGELOG.md
CONFLICT (content): Merge conflict in CHANGELOG.md
Auto-merging app.lock
CONFLICT (content): Merge conflict in app.lock
warning: Cannot merge binary files: logo.png (HEAD vs. feat)
Auto-merging logo.png
CONFLICT (content): Merge conflict in logo.png
Automatic merge failed; fix conflicts and then commit the result.
$ git status --short
UU CHANGELOG.md
UU app.lock
UU logo.png
# (git merge --abort; commit a .gitattributes on main)
$ cat .gitattributes
CHANGELOG.md merge=union
app.lock merge=keepours
$ git config merge.keepours.name "Keep our lockfile, regenerate later"
$ git config merge.keepours.driver true
# (git switch -c try-attr main)
$ git merge feat
Auto-merging CHANGELOG.md
Auto-merging app.lock
warning: Cannot merge binary files: logo.png (HEAD vs. feat)
Auto-merging logo.png
CONFLICT (content): Merge conflict in logo.png
Automatic merge failed; fix conflicts and then commit the result.
$ git status --short
M  CHANGELOG.md
UU logo.png
$ cat CHANGELOG.md
# Changelog

- 1.1: eid discount
- 1.1: wallet payment
- 1.0: first release
$ cat app.lock
lock from main
$ git checkout --theirs logo.png
Updated 1 path from the index
$ git add logo.png
$ git status --short
M  CHANGELOG.md
M  logo.png
  • Attribute ছাড়া: তিনটিতেই conflict. ছবির জন্য বিশেষ সতর্কবার্তা Cannot merge binary files — byte-এর ফাইলে লাইন মেলানোর অর্থ নেই।
  • merge=union: changelog-এ conflict নেই, দুই লাইনই এসেছে। কিন্তু ফল দেখুন — দুটি “1.1”! Union লেখার স্তরে মেলায়, অর্থ বোঝে না; documentation নিজেই সতর্ক করে যে যোগ হওয়া লাইনের ক্রম এলোমেলো হতে পারে আর ফল যাচাই করতে হবে। এখানে মানুষকে ঠিক করতে হবে: “1.2: wallet payment”?
  • নিজস্ব keepours driver: command true কিছু না করে 0 দিয়ে শেষ হয় — ফলে %A (ours) যেমন ছিল তেমন থাকে, আর Git একে পরিষ্কার merge ধরে। Lockfile-এ lock from main. খেয়াল করুন, driver-এর সংজ্ঞা .git/config-এ, commit-এ নয় — প্রত্যেক developer-কে নিজে config করতে হয়।
  • Binary: attribute থাকলেও ছবিটি conflict-এ থাকে; working tree-তে ours-এর ছবি। দুটি ছবি দেখে সিদ্ধান্ত নিয়ে git checkout --theirs logo.png (বা --ours) আর git add. -X ours দিলে binary ফাইলে পুরো ours-এর সংস্করণ নেওয়া হয়।

Lockfile-এর সঠিক পথ সাধারণত “ours রাখা” নয়। উপরের keepours driver theirs-এর নতুন dependency নীরবে বাদ দেয় — merge পরিষ্কার, কিন্তু build ভাঙতে পারে। বেশিরভাগ package manager-এর পরামর্শ হলো conflict হলে manifest ফাইল (যেমন package.json) ঠিক করে lockfile আবার তৈরি করা; কিছু টুল নিজেই lockfile-এর conflict মেটাতে পারে বলে তাদের documentation-এ লেখা আছে — আপনার টুলেরটি দেখে নিন। Changelog-এর জন্যও অনেক দল একটি ফাইলের বদলে প্রতিটি পরিবর্তনের জন্য আলাদা ছোট ফাইল (“changelog fragment”) রাখে, release-এর সময় একত্র করে — তাহলে conflict-ই হয় না। বড় binary ফাইলের জন্য Git LFS-এ lock করার সুবিধা আছে (পর্ব ১৯)।

আরও গভীরে: Git-এর নিজের documentation-ই এই বিষয়ের সবচেয়ে নির্ভরযোগ্য উৎস — git help merge (“MERGE STRATEGIES” অংশ), git help merge-base-এর “DISCUSSION”, git help rerere আর git help gitattributes-এর “Performing a three-way merge”. ort-এর নকশার যুক্তি Elijah Newren-এর Git mailing list-এর ধারাবাহিক patch আলোচনায় পাওয়া যায়। Three-way merge-এর গাণিতিক দিক নিয়ে Sanjeev Khanna, Keshav Kunal আর Benjamin C. Pierce-এর প্রবন্ধ “A Formal Investigation of Diff3” (২০০৭) দেখায়, diff3-এর ফল সবসময় স্বজ্ঞাত হয় না — merge যে লেখার স্তরের, অর্থের নয়, তার তাত্ত্বিক প্রমাণ।

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

  • git merge -s ours আর git merge -X ours-এর পার্থক্য কী? — প্রথমটি অন্য branch-এর সব বদল ফেলে শুধু history-তে merge লেখে; দ্বিতীয়টি শুধু conflict-ওয়ালা hunk-এ ours নেয়, বাকি বদল রাখে।
  • দুটি commit-এর merge base কি সবসময় একটি? — না; criss-cross history-তে একাধিক best common ancestor থাকতে পারে, git merge-base --all সব দেখায়, আর ort সেগুলো merge করে virtual base বানায়।
  • Conflict চলাকালে index-এ কী থাকে? — ফাইলটির stage 1 (base), 2 (ours), 3 (theirs); git add এগুলো মুছে stage 0 লেখে।
  • rerere কী আর কোথায় জমা রাখে? — একবারের conflict-সমাধান মনে রেখে আবার বসায়; .git/rr-cache-এ preimage আর postimage হিসেবে, rerere.enabled চালু থাকলে।
  • Checkout ছাড়া দুটি branch merge হবে কি না কীভাবে জানবেন? — git merge-tree --write-tree A B; exit 0 মানে পরিষ্কার, 1 মানে conflict.

১০ · AI-এর যুগে — merge-এর গভীর প্রশ্নে AI-কে যাচাই

Conflict হলে বা “এই দুই branch কীভাবে মেলাব” জিজ্ঞেস করলে AI assistant দ্রুত উত্তর দেয়। Merge-এর ভেতরের বিষয়ে তার কিছু চেনা ভুল আছে:

AI-এর ভুলকীভাবে ধরবেন
-s ours আর -X ours গুলিয়ে ফেলা, বা অস্তিত্বহীন -s theirs সাজেস্ট করাThrowaway branch-এ চালিয়ে git diff দিয়ে দেখুন অন্য পক্ষের কোন বদল এল; git help merge
“Conflict এড়াতে -X theirs দিন” — অন্যের কাজ নীরবে মুছেMerge-এর পরে git diff HEAD^1 HEAD আর HEAD^2 দিয়ে দুই পক্ষের তুলনা
বানানো flag বা ভুল সংস্করণ (যেমন “ort Git ২.২০-এ এসেছে”)git --version, আপনার Git-এর git help, release notes
Conflict দেখে git reset --hard বা push --force, সতর্কবার্তা ছাড়াআগে git merge --abort; ধ্বংসাত্মক কিছু হলে আগে একটি throwaway clone-এ
Lockfile-এ merge=union বা “ours রাখুন” পরামর্শPackage manager-এর documentation; lockfile আবার তৈরি করে build আর test
rerere চালু করে rerere.autoUpdate-ও, কোনো যাচাইয়ের কথা না বলেপ্রতিটি আপনা-থেকে-মেটানো ফাইল git diff দিয়ে পড়ুন; test চালান
Prompt-এ পুরো config বা .env — গোপন মানসহপাঠানোর আগে password আর token মুছুন (পর্ব ৫৭)

নিচের prompt AI-কে সিদ্ধান্তের আগে প্রমাণ দেখাতে আর প্রতিটি command-এর প্রভাব লিখতে বাধ্য করে — যাতে আপনি নিজে মিলিয়ে নিতে পারেন:

আমি Git ২.৫৩ ব্যবহার করি। দুটি branch merge করতে হবে: main আর release-2.0. নিচে দিচ্ছি: - git merge-base --all main release-2.0 এর আউটপুট - git merge-tree --write-tree --name-only main release-2.0 এর আউটপুট [এখানে আউটপুট, কোনো password বা token ছাড়া] ১. merge base কয়টি, আর একাধিক হলে ort কীভাবে সামলাবে তা ব্যাখ্যা করো। ২. কোন ফাইলে conflict আর কেন — অনুমান করলে স্পষ্ট বলো "অনুমান"। ৩. -s ours, -X ours বা -X theirs সাজেস্ট করলে প্রতিটির ক্ষেত্রে কোন বদল হারাবে তা লেখো। ৪. reset --hard বা force push দেবে না। প্রতিটি command working tree, index আর ref-এ কী বদলায় তা লেখো। ৫. merge-এর পরে কোন test বা পরীক্ষা চালালে ভুল ধরা পড়বে, তা বলো।

AI-এর merge-পরামর্শ হাতে পেলে — চেকলিস্ট

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

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

প্র ০১যে recursive strategy পনেরো বছরের বেশি default ছিল, Git সেটিকে নতুন করে লেখা ort দিয়ে কেন বদলাল?

Git-এর documentation অনুযায়ী recursive strategy v0.99.9k (২০০৫) থেকে v2.33 পর্যন্ত দুই branch-এর merge-এর default ছিল। এর মূল ধারণা — একাধিক merge base থাকলে আগে সেগুলো merge করে virtual base বানানো — সফল প্রমাণিত হয়েছিল; documentation বলে Linux kernel-এর আসল merge-এ পরীক্ষায় এটি কম conflict দিয়েছিল। তাহলে সমস্যা কোথায়?

সমস্যা ছিল বাস্তবায়নে। Recursive-এর কোড অনেক বছর ধরে ধাপে ধাপে বেড়েছিল, আর কাজের মাঝে সে working tree আর index-কে সরাসরি ছুঁত। ফলে কিছু জটিল কোণে — বিশেষত directory-র নাম বদল, rename-এর সাথে conflict, আর virtual base-এর ভেতরের conflict — অদ্ভুত ফল বা ধীরগতি দেখা যেত। বড় repository-তে, যেখানে একটি rebase-এ শত শত commit বসাতে হয় আর প্রতিটিতে হাজারো ফাইলের rename খুঁজতে হয়, সময় বিরক্তিকর হয়ে উঠত। আমাদের জানা মতে Elijah Newren এই সমস্যাগুলো নিয়ে কয়েক বছর কাজ করেন, আর ছোট সংশোধনের বদলে নতুন করে লেখার পথ নেন — নাম “Ostensibly Recursive's Twin”, অর্থাৎ বাইরে থেকে recursive-এর মতোই, ভেতরে নতুন।

ort-এর একটি গুরুত্বপূর্ণ নকশা-সিদ্ধান্ত হলো merge-এর হিসাব আগে পুরোটা memory-তে করা, তারপর ফল লেখা। এর সুবিধা দুটি: অনেক দ্রুত, আর working tree ছাড়াই merge করা সম্ভব। ঠিক এই কারণেই পরে git merge-tree --write-tree (২.৩৮) এসেছে — server-এ checkout ছাড়া merge যাচাই। ২.৩৩-এ ort বিকল্প হিসেবে আসে, ২.৩৪-এ default হয়, আর documentation বলছে ২.৫০ থেকে recursive নামটিই ort-এর আরেক নাম।

Software engineering-এর দিক থেকে এখানে একটি বড় শিক্ষা আছে। পুরোনো, ব্যাপকভাবে ব্যবহৃত কোড নতুন করে লেখা ঝুঁকির — Joel Spolsky-র মতো লেখকেরা এর বিরুদ্ধে সতর্ক করেছেন। ort-এর ক্ষেত্রে ঝুঁকি কমানো হয়েছিল ধাপে ধাপে: আগে বিকল্প হিসেবে, বিস্তৃত test suite দিয়ে, তারপর default, আর অনেক পরে পুরোনো কোড সরানো। পর্ব ৪৩-এর (Technical debt আর legacy code) ভাষায়, এটি পরিকল্পিতভাবে technical debt শোধের উদাহরণ।

মূল উপলব্ধি: ধারণা ঠিক থাকলেও বাস্তবায়ন পুরোনো হয়ে যায়। ort recursive-এর ধারণা রেখে কোড নতুন করে লিখেছে, আর সতর্ক ধাপে ধাপে বদলে Git-এর merge দ্রুত, নির্ভুল আর server-বান্ধব হয়েছে।

প্র ০২Virtual merge base কি সবসময় “ঠিক” উত্তর দেয়? একাধিক base-এর সমস্যার আর কী কী সমাধান হতে পারত?

প্রথমে মনে রাখা দরকার, “ঠিক” merge-এর কোনো গাণিতিক সংজ্ঞা নেই — merge হলো দুই মানুষের উদ্দেশ্য অনুমান করা। Three-way merge একটি যুক্তিসংগত নিয়ম: base থেকে যে পক্ষ বদলেছে, তার কথা মানা হয়। কিন্তু base একাধিক হলে “base থেকে বদল” কথাটির অর্থই অস্পষ্ট হয়ে যায়।

তিনটি সম্ভাব্য পথ। (১) একটি base বেছে নেওয়া — resolve strategy মূলত এটাই করে। সহজ, কিন্তু অংশ ২-এ দেখেছেন: যে base বাছা হলো না, তার দিক থেকে আগেই মেলানো বদল আবার “নতুন” মনে হয়ে অপ্রয়োজনীয় conflict দেয়। আরও খারাপ ক্ষেত্রে, conflict না দিয়ে ভুল দিকে মিলতে পারে। (২) virtual base — recursive আর ort-এর পথ। Base-গুলো নিজেরা মিলে গেলে দারুণ কাজ করে। কিন্তু base-গুলোর মধ্যেই conflict থাকলে? ort তখন virtual base-এর ভেতরে conflict marker রেখে দেয়, আর সেই অংশ আসল merge-এ সাধারণত আবার conflict হয়ে আসে — যেখানে marker-এর ভেতরে marker দেখে মানুষ বিভ্রান্ত হতে পারেন। (৩) প্রতিটি base দিয়ে আলাদা merge করে ফল তুলনা — কিছু অন্য version control ব্যবস্থা এমন ধারণা পরীক্ষা করেছে বলে জানা যায়, যেখানে ফল মিললে নেওয়া হয়, না মিললে মানুষকে জিজ্ঞেস করা হয়।

Git-এর documentation নিজেই একটি সীমা স্বীকার করে: merge শুধু দুই tip আর base দেখে, মাঝের commit নয়। তাই কোনো বদল দুই branch-এ করা হয়ে এক branch-এ revert হলে ফলে বদলটি থেকে যায়। Criss-cross-এ এমন বিস্ময় আরও বাড়ে, কারণ virtual base নিজেই একটি অনুমান। তাত্ত্বিক দিক থেকে Khanna, Kunal আর Pierce-এর ২০০৭ সালের diff3 নিয়ে গবেষণা দেখায় যে লাইনভিত্তিক three-way merge-এর কিছু ফল স্বজ্ঞার বিপরীত হতে পারে।

ব্যবহারিক উত্তর তাই প্রযুক্তিগত যতটা, ততটাই অভ্যাসের। এক দিকে merge-এর প্রবাহ রাখলে (main থেকে branch-এ, আর branch থেকে main-এ শুধু শেষে) criss-cross কমে। ছোট, স্বল্পায়ু branch হলে base-এর জট তৈরিই হয় না। আর merge-এর পরে test সেই ভুলগুলো ধরে, যা কোনো algorithm ধরতে পারে না।

মূল উপলব্ধি: Virtual base একাধিক base-এর সমস্যার সবচেয়ে ব্যবহারিক সমাধান, কিন্তু নিখুঁত নয়। দলের merge-অভ্যাস আর test-ই শেষ প্রতিরক্ষা।

প্র ০৩-s ours আছে কিন্তু -s theirs নেই — এটি কি ভুল, না ইচ্ছাকৃত নকশা?

Documentation-এর ভাষাতেই উত্তরের ইঙ্গিত আছে: -X theirs-এর বর্ণনায় বলা হয়েছে, “ours”-এর মতো “theirs” নামে কোনো strategy নেই যার সাথে এটি গুলিয়ে যেতে পারে। অর্থাৎ অনুপস্থিতিটি সচেতন। Git-এর mailing list-এ বিভিন্ন সময় -s theirs যোগ করার প্রস্তাব এসেছে, আর রক্ষণাবেক্ষণকারীরা এর বিরোধিতা করেছেন বলে জানা যায়।

যুক্তিটি বোঝা যায় দুই strategy-র উদ্দেশ্য তুলনা করলে। -s ours-এর বৈধ কাজ হলো “পুরোনো পার্শ্ব-history-কে বাতিল ঘোষণা”: আমার branch-এ যা আছে সেটাই সত্য, ওই পুরোনো branch-এর কাজ আর লাগবে না, কিন্তু history-তে রাখি যে সেটি বিবেচনা করা হয়েছে। এতে নিজের কাজ রক্ষা পায়, শুধু অন্যের পুরোনো কাজ বাদ যায় — যা সাধারণত সেই কাজের মালিকের সম্মতিতেই হয়। -s theirs হতো উল্টো: নিজের branch-এর সব কাজ ফেলে দিয়ে অন্যের tree নেওয়া, অথচ history-তে দেখানো যে “merge” হয়েছে। প্রায় সব ক্ষেত্রে এর চেয়ে সৎ বিকল্প আছে: যদি সত্যিই অন্য branch-এর অবস্থাই চান, git reset বা branch-এর নাম সরিয়ে দেওয়া পরিষ্কারভাবে বলে দেয় কী হলো। Merge commit-এর আড়ালে নিজের কাজ মুছে ফেলা পরে অনুসন্ধানকারীকে বিভ্রান্ত করে।

তবুও কিছু বাস্তব পরিস্থিতিতে মানুষের এটি দরকার হয় — যেমন একটি দীর্ঘদিনের release branch পুরোপুরি main-এর অবস্থায় আনতে, কিন্তু history রেখে। তখন প্রচলিত কৌশল: অন্য branch-এ গিয়ে git merge -s ours, তারপর ফিরে fast-forward. এই দুই ধাপের অসুবিধাটুকুই একটি ছোট “গতিরোধক” — বড় ধ্বংসাত্মক কাজ এক শব্দে হওয়া উচিত নয়।

এখানে টুলের নকশায় একটি সাধারণ নীতি দেখা যায়: যে কাজ সহজে অপব্যবহার হয় আর যার ফল নীরবে ক্ষতিকর, তা ইচ্ছাকৃতভাবে কঠিন রাখা। git push --force-with-lease, protected branch, বা rm-এর -i একই পরিবারের ধারণা। AI assistant যখন আত্মবিশ্বাসের সাথে git merge -s theirs লিখে দেয়, তখন এই ইতিহাস জানা থাকলে ভুলটি সাথে সাথে ধরা পড়ে।

মূল উপলব্ধি: -s theirs-এর অনুপস্থিতি ইচ্ছাকৃত — নিজের কাজ নীরবে মুছে ফেলার এক-শব্দের পথ Git দেয় না। কঠিন পথটুকু ভাবার সময় দেয়।

প্র ০৪rerere কোন প্রয়োজন থেকে জন্মেছিল, আর এটি কেন default-এ বন্ধ থাকে?

rerere-এর জন্ম Git-এর নিজের রক্ষণাবেক্ষণের কাজ থেকে। আমাদের জানা মতে Git-এর দীর্ঘদিনের রক্ষণাবেক্ষণকারী Junio C Hamano ২০০৬ সালের দিকে এটি যোগ করেন। Git project-এ বহু contributor-এর topic branch আলাদা থাকে, আর রক্ষণাবেক্ষণকারী প্রায় প্রতিদিন সেগুলো একসাথে merge করে একটি integration branch নতুন করে বানান (একসময় যার নাম ছিল “pu”, পরে “seen”). একটি topic আরেকটির সাথে conflict করলে সেই conflict প্রতিদিন ফিরে আসত — একই সমাধান বারবার হাতে করা। rerere সেই পুনরাবৃত্তি দূর করে।

এই উৎস থেকেই বোঝা যায় rerere কাদের জন্য সবচেয়ে কাজের: যাঁরা দীর্ঘদিনের branch রাখেন, পরীক্ষামূলক merge করে abort করেন, বা বারবার rebase করেন। অংশ ৮-এ দেখেছেন, merge-এ রেকর্ড করা সমাধান rebase-এ কাজ করেছে, কারণ normalize করার সময় দুই পক্ষ নির্দিষ্ট ক্রমে সাজানো হয়। Git-এর contrib folder-এ rerere-train.sh নামের একটি script আছে বলে জানা যায়, যা পুরোনো merge commit থেকে সমাধান “শিখিয়ে” rr-cache ভরে দেয় — যাতে নতুন কেউ দলের আগের সমাধানগুলো পান।

তাহলে default-এ বন্ধ কেন? কারণ rerere একটি স্মৃতি, আর স্মৃতি ভুলও মনে রাখে। একবার ভুল সমাধান রেকর্ড হলে পরের বার Git সেটি চুপচাপ বসাবে — আর ব্যস্ত developer যদি না দেখে git add দেন, ভুলটি আবার ঢুকবে। আরও সূক্ষ্ম সমস্যা: একই লেখার conflict ভিন্ন প্রেক্ষাপটে ভিন্ন সমাধান চাইতে পারে — একটি release branch-এ ফি ৫৫ ঠিক, অন্যটিতে হয়তো ৬০। rerere শুধু লেখা দেখে, প্রেক্ষাপট নয়। এ কারণেই Git সমাধান বসিয়েও ফাইলটি unresolved রাখে (যতক্ষণ না rerere.autoUpdate চালু করা হয়), আর git rerere forget রাখা হয়েছে ভুল মুছতে।

ব্যক্তিগত অভ্যাস হিসেবে অনেক অভিজ্ঞ developer rerere সবসময় চালু রাখেন আর autoUpdate বন্ধ রাখেন — Git বসাবে, মানুষ দেখে নেবেন। এটি “স্বয়ংক্রিয়তা, কিন্তু চূড়ান্ত সিদ্ধান্ত মানুষের” নীতির ভালো উদাহরণ, যা AI-সহায়তায় কাজের ক্ষেত্রেও সমানভাবে খাটে।

মূল উপলব্ধি: rerere বারবার আসা conflict-এর পুনরাবৃত্তি দূর করে, কিন্তু ভুলও মনে রাখে। তাই এটি ঐচ্ছিক, আর বসানো সমাধান মানুষের যাচাইয়ের অপেক্ষায় থাকে।

প্র ০৫Changelog আর lockfile — যন্ত্রে বা নিয়মে বানানো ফাইলে বারবার conflict হয়। Merge driver কি এর ভালো সমাধান, নাকি সমস্যা অন্য জায়গায়?

দুই ধরনের ফাইলেই conflict-এর কারণ কাঠামোগত। একটি CHANGELOG.md-এ সবাই নতুন লাইন যোগ করেন একই জায়গায় — ফাইলের মাথায়। দুই branch একই জায়গায় ভিন্ন লাইন যোগ করলে Git-এর চোখে এটি conflict, যদিও মানুষের চোখে উত্তর স্পষ্ট: দুটিই রাখা হোক। Lockfile-এ সমস্যা ভিন্ন: package manager পুরো dependency graph থেকে ফাইলটি বানায়, তাই দুই branch-এ দুটি dependency যোগ করলে ফাইলের অনেক জায়গায় বদল হয়, যার লাইনভিত্তিক merge প্রায়ই অর্থহীন।

Merge driver-এর পক্ষে: merge=union changelog-এর ক্ষেত্রে সাধারণ ক্ষেত্রটি সামলায়। অংশ ৯-এ দেখেছেন conflict ছাড়াই দুই লাইন এল। কিন্তু ফলটিও দেখেছেন — দুটি “1.1”. Documentation নিজেই বলে union-এর ফলে লাইনের ক্রম এলোমেলো হতে পারে আর যাচাই করতে হবে। Lockfile-এ নিজস্ব driver দিয়ে “ours রাখা”-র নিয়ম করা আরও বিপজ্জনক: theirs-এর নতুন dependency নীরবে বাদ, merge পরিষ্কার, কিন্তু CI-তে build ভাঙে — এক ধরনের semantic conflict (পর্ব ১৩)। আর driver-এর সংজ্ঞা .git/config-এ থাকে, commit-এ নয়; যার config নেই তার কাছে attribute থাকলেও driver চলে না — দলের সদস্যদের মধ্যে ভিন্ন আচরণ।

সমস্যার উৎসে সমাধান: অনেক open-source project changelog-এর জন্য একটি ফাইলের বদলে প্রতিটি পরিবর্তনের জন্য আলাদা ছোট ফাইল রাখে (“changelog fragment”), আর release-এর সময় একটি টুল সেগুলো একত্র করে — conflict-এর কারণই থাকে না। কেউ কেউ commit বার্তা থেকে changelog বানান (Conventional Commits, পর্ব ১৮)। Lockfile-এর ক্ষেত্রে বেশিরভাগ package manager-এর পরামর্শ: manifest ঠিক করুন, lockfile আবার তৈরি করুন, তারপর test. কিছু টুল lockfile-এর conflict নিজেই মেটাতে পারে বলে documentation-এ লেখা আছে — সেটি merge driver-এর চেয়ে নিরাপদ, কারণ টুলটি ফাইলের অর্থ বোঝে।

এখানে একটি সাধারণ software engineering শিক্ষা আছে: বারবার একই ধরনের conflict হলে প্রশ্ন করুন, “ফাইলের নকশাই কি conflict তৈরি করছে?” Hotspot ফাইল (যেখানে সবাই লেখে) ভাঙা, যন্ত্রের ফাইল যন্ত্রকে দিয়ে বানানো, আর merge-এর পরে CI — এগুলো driver-এর কৌশলের চেয়ে টেকসই।

মূল উপলব্ধি: Merge driver লক্ষণ সামলায়; টেকসই সমাধান ফাইলের নকশা আর কাজের ধারা বদলানো। Driver ব্যবহার করলেও merge-এর পরে ফল পড়ুন আর test চালান।

অনুশীলন

আউটপুট অনুমান, command-এর ক্রম, index পড়া আর নীতির সমালোচনা — প্রতিটির পূর্ণ উত্তর দেওয়া আছে।

  1. আউটপুট অনুমান করুন (-X ours বনাম -s ours): একটি কাল্পনিক ফলাফল পোর্টালের result.txt, base commit-এ:
    roll = 1
    bangla = 78
    name = Karim
    math = 90
    main-এ অফিস শুধু bangla = 80 করেছে। teacher-b branch-এ bangla = 85, math = 92, আর নতুন ফাইল remarks.txt. main থেকে খোলা তিনটি আলাদা পরীক্ষার branch-এ (ক) git merge -X ours teacher-b, (খ) git merge -X theirs teacher-b, (গ) git merge -s ours teacher-b চালালে প্রতিটিতে result.txt-এর বাংলা আর গণিত কত হবে, আর remarks.txt থাকবে কি না?

    বিশ্লেষণ: বাংলার লাইনে দুই পক্ষ ভিন্নভাবে বদলেছে (৮০ বনাম ৮৫) → conflict-এর hunk. গণিত শুধু theirs বদলেছে, আর মাঝে name লাইনটি অপরিবর্তিত, তাই এটি আলাদা পরিষ্কার hunk. remarks.txt শুধু theirs-এ নতুন। (ক) -X ours: বাংলা ৮০, গণিত ৯২, remarks.txt আছে। (খ) -X theirs: বাংলা ৮৫, গণিত ৯২, remarks.txt আছে। (গ) -s ours: বাংলা ৮০, গণিত ৯০ (base-এর মান, কারণ theirs-এর কিছুই আসে না), remarks.txt নেই। Git ২.৫৩-এর আসল আউটপুট (সংক্ষেপে):

    $ git merge -X ours teacher-b
    Auto-merging result.txt
    Merge made by the 'ort' strategy.
     remarks.txt | 1 +
     result.txt  | 2 +-
     2 files changed, 2 insertions(+), 1 deletion(-)
     create mode 100644 remarks.txt
    $ cat result.txt
    roll = 1
    bangla = 80
    name = Karim
    math = 92
    $ git merge -X theirs teacher-b
    …
    $ cat result.txt
    roll = 1
    bangla = 85
    name = Karim
    math = 92
    $ git merge -s ours teacher-b
    Merge made by the 'ours' strategy.
    $ cat result.txt
    roll = 1
    bangla = 80
    name = Karim
    math = 90
    $ ls
    result.txt

    গুরুত্বপূর্ণ সতর্কতা: (গ)-তে শিক্ষক B-এর গণিতের সংশোধন (৯২) কোনো conflict ছাড়াই হারিয়ে গেল, আর এখন থেকে Git ধরে নেবে teacher-b merge হয়ে গেছে — পরে আবার git merge teacher-b দিলে বলবে “Already up to date”. আর name লাইনটি না থাকলে বাংলা আর গণিত পাশাপাশি হতো, এক hunk — তখন (ক)-তে গণিতও ৯০ থাকত।

  2. Command-এর ক্রম — একটি merge bot: একটি server-এর bare repository-তে (working tree নেই) একটি bot চালাতে হবে, যা (১) main আর pr branch conflict ছাড়া মিলবে কি না দেখবে, (২) conflict হলে কোন ফাইলে তা জানাবে, (৩) পরিষ্কার হলে একটি merge commit বানিয়ে main-কে সেখানে সরাবে — কিন্তু এর মধ্যে কেউ main-এ push করে থাকলে সরাবে না। কোন command, কোন ক্রমে?

    ধাপ: (১) old=$(git rev-parse main) — শুরুর অবস্থা মনে রাখা। (২) tree=$(git merge-tree --write-tree --name-only main pr); exit code 1 হলে conflict — আউটপুটের প্রথম লাইন tree, তারপরের লাইনগুলো ফাইলের নাম; সেগুলো pull request-এ জানিয়ে থামুন। (৩) exit 0 হলে new=$(git commit-tree -p main -p pr -m "Merge pr" $tree) — দুই parent-ওয়ালা commit. (৪) git update-ref refs/heads/main $new $old — শেষ argument হলো “main এখনো এখানে থাকলেই সরাও”। একটি bare clone-এ Git ২.৫৩-এর আসল ফল:

    $ old=$(git rev-parse main)
    $ tree=$(git merge-tree --write-tree main pr); echo "exit=$? $tree"
    exit=0 8892efa8f9bb1abdbe35610fd79a72825d7b2e97
    $ new=$(git commit-tree -p main -p pr -m "Merge pr (bot)" $tree); echo $new
    8ce90ce4e1ffa1bd25878e7def00ad1a0dbc4500
    $ git update-ref refs/heads/main $new $old
    $ git log --oneline --graph -4 main
    *   8ce90ce Merge pr (bot)
    |\
    | * caa7db2 Add class
    * | d57ba96 Office: bangla 80
    |/
    * de3d070 Base result
    # (if main had moved meanwhile:)
    $ git update-ref refs/heads/main $new 1111111111111111111111111111111111111111
    fatal: update_ref failed for ref 'refs/heads/main': cannot lock ref 'refs/heads/main': is at 8ce90ce4e1ffa1bd25878e7def00ad1a0dbc4500 but expected 1111111111111111111111111111111111111111

    শেষ লাইনটি পুরোনো মানের পাহারা দেখায়: প্রত্যাশিত মান না মিললে Git ref সরাতে অস্বীকার করে — দুই bot একসাথে চললেও একজনের কাজ অন্যজন মুছবে না। সতর্কতা: যে repository-তে main checkout করা আছে, সেখানে update-ref দিলে working tree আর index পুরোনো থেকে যায়; এই কৌশল bare repository-র জন্য। আর merge পরিষ্কার মানেই ঠিক নয় — commit-এর আগে CI চালানো উচিত (পর্ব ৫৮)।

  3. Index পড়ুন: git merge cleanup-এর পরে git ls-files -u এই আউটপুট দিল। কোন ফাইলে কোন ধরনের conflict, git status --short কী দেখাবে, আর দল যদি ঠিক করে “cleanup-এর ব্যানার নেব, কুপন ফাইল মুছে দেব”, কোন command-গুলো লাগবে?
    100644 26f343c083952217e74b016dda8c88e11cf5b394 2	banner.txt
    100644 f27cfcc34229f1e599397e1a3b76e0ffcb4d82a3 3	banner.txt
    100644 587be6b4c3f93f93c489c0111bba5596147a26cb 1	coupon.txt
    100644 975fbec8256d3e8a3797e7a3611380f27c49f4ac 2	coupon.txt

    banner.txt: stage 1 নেই, 2 আর 3 আছে → base-এ ফাইলটি ছিল না, দুই পক্ষই যোগ করেছে → add/add, status AA. coupon.txt: stage 1 আর 2 আছে, 3 নেই → base-এ ছিল, ours-এ আছে (বদলানো), theirs-এ নেই → theirs মুছেছে, ours বদলেছে → modify/delete, status UD (“deleted by them”). Git ২.৫৩-এর আসল git status --short:

    AA banner.txt
    UD coupon.txt

    Command: git checkout --theirs banner.txt (stage 3 থেকে লেখা), git add banner.txt; git rm coupon.txt (মুছে ফেলাকে resolved ঘোষণা); তারপর git status --short দিয়ে দেখা যে সব resolved, আর git commit — বার্তায় সিদ্ধান্তটি লিখুন। লক্ষ করুন, coupon.txt-এ --theirs কাজ করবে না, কারণ stage 3-এর অস্তিত্বই নেই।

  4. নীতির সমালোচনা: একটি কাল্পনিক অনলাইন বাজার অ্যাপের দলের লিখিত merge-নীতি: “(১) সময় বাঁচাতে সব merge -X theirs দিয়ে, যাতে নতুন কোড জেতে। (২) package-lock.json আর CHANGELOG.md দুটিতেই merge=union. (৩) সবার জন্য rerere আর rerere.autoUpdate চালু। (৪) release-এর দিনে বারোটি feature branch একটি octopus merge-এ main-এ। (৫) প্রতিটি developer যখন খুশি main-কে নিজের branch-এ আর নিজের branch-কে main-এ merge করবেন।” সমস্যাগুলো কী, আর কীভাবে নতুন করে লিখবেন?
    • (১) -X theirs সবসময়: “theirs” মানে আনা branch, “নতুন” নয়; conflict-এর hunk-এ বর্তমান branch-এর কাজ নীরবে হারায়, পাশের লাইনসহ। Rebase-এ অর্থ উল্টে যায়। নতুন নিয়ম: conflict হাতে, merge.conflictStyle zdiff3 দিয়ে, দরকার হলে দুই লেখকের আলোচনায়।
    • (২) Lockfile-এ union: লাইনভিত্তিক মিশ্রণ JSON ভেঙে দিতে পারে বা অসংগত dependency তৈরি করতে পারে। নতুন নিয়ম: lockfile-এর conflict-এ manifest ঠিক করে lockfile আবার তৈরি, তারপর test. Changelog-এ union চলতে পারে, কিন্তু ফল পড়তে হবে (দুটি একই version-এর লাইন), অথবা changelog fragment.
    • (৩) autoUpdate: ভুল সমাধানও চুপচাপ stage হয়ে commit-এ চলে যায়। নতুন নিয়ম: rerere চালু থাকুক, autoUpdate বন্ধ; আপনা থেকে মেটানো ফাইল git diff দেখে git add.
    • (৪) বারো branch-এর octopus: একটিতেও জটিল conflict থাকলে octopus পুরো merge বাতিল করে; সফল হলেও কোন branch কোন bug আনল তা bisect বা revert-এ আলাদা করা কঠিন। আর release-এর দিনে সব একসাথে — “integration hell”. নতুন নিয়ম: কাজ শেষ হলেই একটি একটি করে merge, প্রতিটিতে CI; release-এর দিন শুধু tag.
    • (৫) দুই দিকে যখন-খুশি merge: এটাই criss-cross তৈরির রেসিপি — একাধিক merge base, virtual base-এর বিস্ময়, জটিল history. নতুন নিয়ম: main থেকে branch-এ আনা চলবে (বা rebase), branch থেকে main-এ শুধু review-শেষে pull request দিয়ে।
    • যা নেই: merge-এর আগে CI আর merge-tree দিয়ে conflict-যাচাই, কে conflict সমাধান করবেন, আর merge commit রাখা হবে কি না — এসবও লিখে রাখা দরকার (পর্ব ১৬, ১৭)।

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

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

ক্রমCriss-cross history-তে git merge side দিলে ort যে ধাপগুলো করে, আর rerere চালু থাকলে তারপর যা ঘটে — সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. দুই tip-এর সব best common ancestor খোঁজা (যেমন merge-base --all)
  2. Base-গুলোকে নিজেদের মধ্যে merge করে virtual merge base বানানো
  3. Virtual base-এর সাপেক্ষে ours আর theirs-এর three-way merge
  4. না মেলা ফাইলের জন্য index-এ stage 1, 2, 3 লেখা আর rerere-এর preimage রাখা
  5. মানুষ ফাইল ঠিক করে git add দেন
  6. git commit — merge commit তৈরি আর rerere-এর postimage রেকর্ড

Base-এ fee = 60 আর limit = 1000 (মাঝে একটি অপরিবর্তিত লাইন)। main শুধু fee = 70 করেছে; promo করেছে fee = 50 আর limit = 800. main-এ থেকে git merge -s ours promo দিলে ফাইলে কী থাকবে?

  • fee = 70, limit = 800
  • fee = 50, limit = 800
  • fee = 70, limit = 1000
  • Conflict marker দিয়ে থামবে

একটি repository-তে git merge-base --all main side দুটি ভিন্ন commit hash ছাপল। এর সবচেয়ে সঠিক অর্থ কী?

  • Repository-র history নষ্ট হয়ে গেছে, git fsck দরকার
  • দুটি best common ancestor আছে, কেউ কারও ancestor নয় — সম্ভবত criss-cross merge-এর ফল
  • Merge অবশ্যই fast-forward হবে
  • প্রথম hash-টিই আসল merge base, দ্বিতীয়টি তার parent

Conflict চলাকালে git ls-files -u একটি ফাইলের জন্য শুধু stage 1 আর stage 3 দেখাচ্ছে, stage 2 নেই। কোন অবস্থা?

  • দুই পক্ষ একই নামে নতুন ফাইল যোগ করেছে (add/add)
  • দুই পক্ষ ফাইলটির একই লাইন ভিন্নভাবে বদলেছে
  • Theirs ফাইলটি মুছেছে, ours বদলেছে
  • Ours (বর্তমান branch) ফাইলটি মুছেছে, theirs বদলেছে

একটি CI bot একটি bare repository-তে জানতে চায় main আর feature conflict ছাড়া মিলবে কি না — কোনো checkout বা branch না বদলে। কোন command সবচেয়ে উপযুক্ত?

  • git merge-tree --write-tree main feature
  • git merge --no-commit feature
  • git merge-file main feature
  • git merge-base --all main feature

একজন developer rerere.enabled true চালু রেখে একটি merge-এ conflict হাতে সমাধান করে commit করলেন। পরের সপ্তাহে একই branch rebase করার সময় একই conflict এল। ডিফল্ট সেটিংসে কী ঘটবে?

  • Rebase-এ ours/theirs উল্টো, তাই rerere কিছুই করবে না
  • Git সমাধান বসিয়ে ফাইলটি stage করে rebase নিজেই চালিয়ে যাবে
  • ফাইলে আগের সমাধান বসবে, কিন্তু git add দিয়ে নিশ্চিত করা বাকি থাকবে
  • Rebase নিজে থেকে abort হবে

একসাথে তিনটি branch দিয়ে git merge a b c দেওয়া হলো। b মেলাতে গিয়ে একটি জটিল conflict হলো, আর c এখনো বাকি। Octopus strategy কী করবে?

  • পুরো merge বাতিল করবে; branch-গুলো আলাদা করে merge করতে হবে
  • b-এর conflict-এ ours বেছে নিয়ে এগিয়ে যাবে
  • শুধু a আর b দিয়ে একটি merge commit বানাবে
  • নিজে থেকে ort strategy-তে বদলে যাবে

শব্দকোষ

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

মনে রাখুন

  1. Merge base = best common ancestor, আর DAG-এ এটি একাধিক হতে পারে: git merge-base --all. Criss-cross-এ ort base-গুলো আগে merge করে virtual merge base বানায়; resolve একটি বেছে নিয়ে অপ্রয়োজনীয় conflict দিতে পারে।
  2. Strategy (-s): ort (default, ২.৩৪ থেকে), resolve, octopus (৩+ head, জটিল conflict-এ রাজি নয়), ours, subtree. Option (-X): ours, theirs, ignore-space-change, renormalize, diff-algorithm, find-renames.
  3. -X ours = শুধু conflict-এর hunk-এ ours; -s ours = theirs-এর কিছুই নয়, শুধু history-তে merge. -s theirs নেই।
  4. Conflict-এ index-এ stage 1/2/3 = base/ours/theirs; checkout --ours/--theirs শুধু working tree বদলায়, resolve করে git add/git rm. merge-file তিন ফাইল মেলায়, merge-tree --write-tree checkout ছাড়া পুরো merge.
  5. rerere: rerere.enabled, .git/rr-cache-এ preimage আর postimage; একই conflict আবার এলে বসায় কিন্তু যাচাই আপনার। Merge driver (union, নিজস্ব) লক্ষণ সামলায় — ফল পড়ুন, test চালান।

পরের পর্বে: Merge ঠিকমতো হলো, কিন্তু কোনো এক সময় একটি bug ঢুকেছে — কোন commit-এ? পর্ব ৫৫-এ Git দিয়ে bug খোঁজা: git bisect-এর binary search, git blame দিয়ে লাইনের ইতিহাস, আর git log -S দিয়ে কোনো লেখা কখন এল বা গেল। সেখানে আজকের merge commit-গুলো কীভাবে খোঁজাকে প্রভাবিত করে, তাও দেখবেন।

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

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