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

Merge আর conflict — fast-forward, three-way merge আর conflict সমাধান

Git Merge and Conflicts — Fast-forward, Merge Base, Three-way Merge and Resolving Conflicts

Taught in Bangla, this lesson explains merging two lines of work, fast-forward versus --no-ff merge commits, finding the merge base and how a three-way merge decides hunk by hunk. Runnable simulators compute merge bases, perform a line-based three-way merge with conflict markers, show a semantic conflict and estimate conflict odds. It walks through reading markers and resolving a conflict end to end, plus reducing conflicts.

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

  • Fast-forward কখন হয়, --ff-only আর --no-ff কী করে, আর দল কেন merge commit রাখে
  • Merge base খোঁজা (commit graph-এ lowest common ancestor) আর three-way merge-এর hunk-ভিত্তিক নিয়ম
  • Conflict-এর ধরন (content, modify/delete, add/add), marker পড়া আর diff3/zdiff3 style
  • Conflict সমাধানের পুরো ধাপ: status, diff, log --merge, --ours/--theirs, mergetool, abort আর continue
  • Semantic conflict, conflict কমানোর অভ্যাস, আর log --graph ও --first-parent দিয়ে merge-এর history পড়া

১ · Merge কী — দুটি কাজের ধারা এক করা

পর্ব ১২-এ (Branch — pointer, HEAD, switch আর detached HEAD) দেখেছেন, Git-এ একটি branch আসলে একটি commit-এর দিকে নির্দেশক একটি নাম, আর HEAD বলে দেয় আপনি এখন কোন branch-এ আছেন। Branch বানানো সহজ, কিন্তু branch-এর আসল উদ্দেশ্য পূর্ণ হয় তখনই, যখন আলাদা করে করা কাজটি আবার মূল কোডে ফিরে আসে। এই ফিরিয়ে আনার নাম merge.

একটি বিয়েবাড়ির আয়োজনের তালিকার কথা ভাবুন। মূল তালিকা বড় ভাইয়ের খাতায়। ছোট বোন একটি কপি নিয়ে গেলেন মেহমানের তালিকা ঠিক করতে, আর মামা আরেকটি কপি নিয়ে গেলেন রান্নার বাজার মেলাতে। সন্ধ্যায় সবাই ফিরলে মূল খাতায় দুজনের বদল তুলতে হবে। বোন মেহমানের অংশ বদলেছেন, মামা বাজারের অংশ — কোনো ঝামেলা নেই, দুটোই তোলা হলো। কিন্তু দুজনেই যদি “গরুর মাংস কত কেজি” লাইনটি ভিন্নভাবে বদলে থাকেন — একজন ২০, আরেকজন ২৫ — তখন বড় ভাইকে থামতে হবে, দুজনের সাথে কথা বলে ঠিক করতে হবে। আর এটা ঠিক করতে সবচেয়ে কাজে লাগে সকালের মূল তালিকায় কী লেখা ছিল, সেটি জানা। Git-এর merge ঠিক এভাবে চিন্তা করে।

Command-টির চেহারা সরল:

$ git switch main          # যে branch-এ কাজ আনবেন, আগে সেখানে যান
$ git merge feature/x      # feature/x-এর কাজ main-এ আনুন

দুটি বিষয় শুরুতেই পরিষ্কার করা দরকার:

  • দিক: git merge X সবসময় বর্তমান branch-কে বদলায় — X-কে নয়। X যেমন ছিল, তেমনই থাকে। তাই আগে git switch দিয়ে লক্ষ্য branch-এ যাওয়া অভ্যাস করুন (git switch এসেছে Git ২.২৩-এ; পুরোনো লেখায় একই কাজে git checkout main দেখবেন)।
  • নাম: merge-এর আলোচনায় বর্তমান branch-কে বলা হয় ours (“আমাদের”, অর্থাৎ HEAD), আর যে branch আনা হচ্ছে সেটি theirs (“ওদের”)। এই দুটি শব্দ পুরো পর্বে বারবার আসবে।

Merge-এর আগে working tree পরিষ্কার রাখা ভালো, অর্থাৎ commit না করা বদল যেন না থাকে (git status দেখে নিন; পর্ব ৯ — History আর পার্থক্য দেখা)। অসমাপ্ত বদল থাকলে Git প্রায়ই merge শুরুই করতে দেয় না, আর conflict হলে সেই বদল ও merge-এর বদল গুলিয়ে যায়। Merge-এর ফলাফল তিন রকম হতে পারে, আর Git নিজেই ঠিক করে কোনটি হবে:

Fast-forward

বর্তমান branch পেছনে পড়ে আছে, নিজে কোনো নতুন commit করেনি। শুধু নামটি সামনে সরানো হয়।

নতুন commit নেই

Three-way merge

দুই branch-ই নিজের পথে এগিয়েছে। Git তিনটি সংস্করণ মিলিয়ে একটি merge commit বানায়।

দুই parent

Conflict

Three-way merge-এর মাঝে কোনো জায়গায় দুই পক্ষের বদল মেলে না। Git থামে, মানুষের সিদ্ধান্ত চায়।

আপনার কাজ

Already up to date

অন্য branch-এর সব commit আগেই বর্তমান branch-এ আছে। কিছুই করার নেই।

কিছু বদলায় না

এই পর্বের সব Git আউটপুট আসল — একটি খালি পরীক্ষার folder-এ Git ২.৫৩ দিয়ে চালানো। Commit-এর hash যেন পাঠক হিসেবে আপনার কাছেও একই আসে, তাই লেখকের নাম “Rahim”, email rahim@example.com, আর সময় GIT_AUTHOR_DATE ও GIT_COMMITTER_DATE দিয়ে ঠিক করা: ২০২৬-০১-১৫ সকাল ১০:০১ (+06:00) থেকে শুরু, প্রতিটি commit আর প্রতিটি git merge চালানোর আগে এক মিনিট করে বাড়িয়ে (ব্যর্থ --ff-only চেষ্টাটি বাদে)। ফাইলের লেখা, নাম, সময় — একটিও ভিন্ন হলে hash ভিন্ন হবে; সেটা স্বাভাবিক। গল্পটি একটি কাল্পনিক অনলাইন বাজার অ্যাপের config ফাইল shop.conf নিয়ে। শুরুতে main-এ দুটি commit আছে: 8373eb1 (“Add shop config”) আর d273b9b (“Add README”). ফাইলটি এমন:

name = Bazar Mart
currency = BDT
delivery_fee = 60
free_over = 1000
discount = 0

২ · Fast-forward — যখন শুধু pointer সরালেই চলে

প্রথম কাজ: ঈদের ছাড়। একজন developer main থেকে feature/eid-discount branch খুলে দুটি commit করলেন। এর মধ্যে main-এ আর কেউ কিছু করেননি। (Transcript-এ # দিয়ে শুরু লাইনগুলো মন্তব্য — সেখানে ফাইল সম্পাদনা আর git add হয়েছে।)

$ git switch -c feature/eid-discount
Switched to a new branch 'feature/eid-discount'
# (edit shop.conf: discount = 0 -> 10; git add shop.conf)
$ git commit -m "Set eid discount to 10%"
[feature/eid-discount b922684] Set eid discount to 10%
 1 file changed, 1 insertion(+), 1 deletion(-)
# (append a line to README.md; git add README.md)
$ git commit -m "Document eid discount"
[feature/eid-discount f424824] Document eid discount
 1 file changed, 1 insertion(+)
$ git log --oneline --graph --all
* f424824 Document eid discount
* b922684 Set eid discount to 10%
* d273b9b Add README
* 8373eb1 Add shop config
$ git switch main
Switched to branch 'main'
$ git merge-base main feature/eid-discount
d273b9b3e1ea352fd7ce325bf2fdd2602986f09f
$ git rev-parse --short main
d273b9b
$ git merge feature/eid-discount
Updating d273b9b..f424824
Fast-forward
 README.md | 1 +
 shop.conf | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
$ git log --oneline --graph
* f424824 Document eid discount
* b922684 Set eid discount to 10%
* d273b9b Add README
* 8373eb1 Add shop config

git merge-base বলছে দুই branch-এর common ancestor d273b9b — আর main নিজেও ঠিক d273b9b-এ। অর্থাৎ main হলো feature/eid-discount-এর পূর্বপুরুষ (ancestor). এমন অবস্থায় মেলানোর কিছু নেই: feature-এর history-তে main-এর সব কাজ আগে থেকেই আছে। Git তাই Updating d273b9b..f424824 আর Fast-forward লেখে — একে বলে fast-forward merge — main নামটি সামনে সরে f424824-এ বসে। কোনো নতুন commit হয়নি; শেষের log-এ শুধু চারটি পুরোনো commit, এক সরলরেখায়।

Fast-forward-এর আগে আর পরে

Only the main pointer moves; no new commit is created

আগে 8373eb1d273b9bb922684f424824 HEAD → main feature/eid-discount পরে · Fast-forward 8373eb1d273b9bb922684f424824 HEAD → main feature/eid-discount main আগে এখানে ছিল
ছবি ১৩.১ — তীর child থেকে parent-এর দিকে (Git-এর নিয়মে প্রতিটি commit তার parent-কে মনে রাখে)। Fast-forward-এ graph বদলায় না, বদলায় শুধু main নামের জায়গা।

কখন fast-forward সম্ভব — একটি নিখুঁত শর্ত

Commit-গুলো parent-এর দিকে নির্দেশ করে বলে পুরো history একটি DAG (directed acyclic graph — দিকযুক্ত, চক্রহীন graph). কোনো commit X থেকে parent ধরে ধরে পেছনে গিয়ে Y-তে পৌঁছানো গেলে Y হলো X-এর ancestor. তাহলে শর্তটি:

ours যদি theirs-এর ancestor হয় → fast-forward সম্ভব
theirs যদি ours-এর ancestor হয় → “Already up to date”

উল্টোটা কেন “কিছু করার নেই”? কারণ theirs-এর সব কাজ ours-এর history-তে আগেই আছে। আর দুটির কোনোটিই অন্যটির ancestor না হলে দুই branch আলাদা পথে গেছে (diverged) — তখন three-way merge লাগে (অংশ ৪)।

দুটি option fast-forward-কে নিয়ন্ত্রণ করে:

  • git merge --ff-only X — শুধু fast-forward সম্ভব হলে merge করুন, না হলে কিছু না করে থামুন। নিজের local main-কে নিরাপদে হালনাগাদ করতে এটি জনপ্রিয়; ভুল করে merge commit তৈরির ভয় থাকে না। git config merge.ff only দিয়ে একে default-ও করা যায়।
  • git merge --no-ff X — fast-forward সম্ভব হলেও একটি merge commit বানাতে বাধ্য করুন। কেন কেউ এটা চাইবেন, পরের অংশে।

৩ · --no-ff আর merge commit — দল কেন ইচ্ছে করে merge commit রাখে

পরের কাজ: payment-এর নতুন পদ্ধতি। এবারও main সরেনি, অর্থাৎ fast-forward সম্ভব। কিন্তু দলের নিয়ম — প্রতিটি feature একটি merge commit দিয়ে ঢুকবে:

$ 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 dcebc15] Add mobile wallet payment
 1 file changed, 1 insertion(+)
 create mode 100644 payment.conf
$ git switch main
Switched to branch 'main'
$ git merge --no-ff feature/wallet
Merge made by the 'ort' strategy.
 payment.conf | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 payment.conf
$ git log --oneline --graph
*   d08a9a3 Merge branch 'feature/wallet'
|\  
| * dcebc15 Add mobile wallet payment
|/  
* f424824 Document eid discount
* b922684 Set eid discount to 10%
* d273b9b Add README
* 8373eb1 Add shop config
$ git cat-file -p HEAD
tree e00a4f5ffb4807231885c1230ceeefd3a2d901fc
parent f424824320ab1c81cdac409b4adbbd257063a918
parent dcebc15ce21e1765da497e38129da947e853163d
author Rahim <rahim@example.com> 1768450020 +0600
committer Rahim <rahim@example.com> 1768450020 +0600

Merge branch 'feature/wallet'

Fast-forward সম্ভব ছিল, তবুও --no-ff-এর কারণে Git একটি নতুন commit d08a9a3 বানাল, বার্তা Merge branch 'feature/wallet'. বার্তাটি Merge made by the 'ort' strategy. — ort হলো Git-এর এখনকার default merge strategy. git log --graph-এ branch-টি একটি আলাদা “কুঁজ” হয়ে দেখা যাচ্ছে। আর git cat-file -p HEAD commit-টির কাঁচা রূপ দেখায়: একটি tree (পুরো project-এর snapshot), আর দুটি parent লাইন — প্রথমটি f424824 (merge-এর আগে main যেখানে ছিল), দ্বিতীয়টি dcebc15 (feature/wallet-এর tip). দুই parent-ওয়ালা commit-ই merge commit. Author লাইনের 1768450020 +0600 হলো Unix সময় (সেকেন্ডে) আর সময় অঞ্চল।

Parent-দের ক্রম গুরুত্বপূর্ণ। প্রথম parent সবসময় merge-এর সময় যে branch-এ ছিলেন তার আগের tip — একে বলে first parent. অনেক দল main-এ সরাসরি commit করে না; সব কাজ branch থেকে --no-ff merge হয়ে ঢোকে। তখন main-এর first-parent পথটি হয়ে যায় “কোন দিন কোন feature ঢুকল” তার পরিষ্কার তালিকা (অংশ ৯-এ দেখবেন)।

বিষয়Fast-forward--no-ff merge commit
History-র চেহারাসরলরেখা; কোন commit কোন feature-এর, বোঝা কঠিনপ্রতিটি feature একটি কুঁজ; শুরু আর শেষ স্পষ্ট
পুরো feature ফেরত নেওয়াপ্রতিটি commit আলাদা করে revert করতে হয়একটি merge commit revert করলেই হয় (git revert -m 1; পর্ব ১১ — ভুল শোধরানো)
“কখন ঢুকল” খোঁজাfeature-এর commit-গুলোর তারিখ দেখে আন্দাজmerge commit-এর তারিখ আর বার্তা
অসুবিধা—ছোট কাজেও বাড়তি commit; graph ভিড় হতে পারে

কোনোটিই “ঠিক” বা “ভুল” নয় — এটা দলের নীতি। Git Flow নামের branching মডেল --no-ff রাখতে বলে; অনেক দল আবার squash বা rebase করে সরলরেখা রাখে। বিভিন্ন hosting platform-এ pull request merge করার সময় “merge commit”, “squash” বা “rebase” বাছাইয়ের সুযোগ থাকে (নাম আর default platform-ভেদে আলাদা)। এসব নীতি নিয়ে বিস্তারিত পর্ব ১৬ আর ১৭-এ; rebase পর্ব ১৫-এ।

৪ · Three-way merge — merge base আর তিনটি সংস্করণ

এখন আসল ঘটনা। দুই branch-ই যদি নিজের পথে এগিয়ে যায়, pointer সরিয়ে আর কাজ হবে না — main-কে সামনে সরালে main-এর নিজের নতুন commit হারিয়ে যাবে। নিচের stepper-এ একই জোড়া branch দিয়ে দুটি পরিস্থিতি দেখুন। বৃত্তের অক্ষরগুলো commit-এর নাম; তীর parent-এর দিকে।

ABCD EM feature HEAD → main HEAD → main HEAD → main HEAD → main base base base = B = main → fast-forward সম্ভব main শুধু B থেকে D-তে সরল base = B, কিন্তু main এখন E M-এর parent: E (১ম) আর D (২য়) --no-ff: M-এর parent B আর D
অবস্থা
main
feature
merge base
সিদ্ধান্ত
নতুন commit
  1. শুরু। main আছে B-তে। B থেকে feature খোলা হয়েছিল, তাতে দুটি commit C আর D. main নিজে কিছু করেনি। দুটির merge base হলো B — আর B-ই main. অর্থাৎ main হলো feature-এর ancestor.
  2. Fast-forward. git merge feature দিলে Git শুধু main-কে D-তে সরায়। কোনো নতুন commit নেই, graph অপরিবর্তিত। এখন দুই নাম একই commit-এ।
  3. এবার অন্য একটি দিন কল্পনা করুন। শুরুর অবস্থা থেকে, feature merge হওয়ার আগেই কেউ main-এ একটি জরুরি commit E করলেন (E-র parent B). এখন main (E) আর feature (D) কেউ কারও ancestor নয় — দুই পথ আলাদা হয়ে গেছে। Merge base এখনো B.
  4. Three-way merge. main-কে D-তে সরালে E হারিয়ে যেত। তাই Git তিনটি snapshot মেলায় — base B, ours E, theirs D — আর ফলাফল নিয়ে নতুন commit M বানায়, যার প্রথম parent E, দ্বিতীয় parent D. main সরে M-এ যায়; feature D-তেই থাকে।
  5. প্রথম দিনে --no-ff দিলে। ধাপ ১-এর অবস্থায় git merge --no-ff feature: fast-forward সম্ভব হলেও Git merge commit M বানায়, parent B আর D. M-এর snapshot আর D-র snapshot হুবহু এক — শুধু history-তে feature-এর সীমানা চিহ্নিত থাকে।

Merge base — commit graph-এ lowest common ancestor

Three-way merge-এর প্রাণ হলো merge base. সংজ্ঞাটি graph-তত্ত্বের: ours আর theirs দুটি commit-এর common ancestor হলো এমন commit, যা দুটিরই ancestor (নিজেকেও নিজের ancestor ধরা হয়)। এদের মধ্যে যেটি আর কোনো common ancestor-এর ancestor নয় — অর্থাৎ সবচেয়ে “নিচের”, সবচেয়ে সাম্প্রতিক — সেটি best common ancestor, অর্থাৎ lowest common ancestor (LCA). Tree-তে LCA সবসময় একটিই থাকে (DSA কোর্সে Tree-এর কৌশল — LCA), কিন্তু DAG-এ একাধিক থাকতে পারে, কারণ merge commit-এর একাধিক parent আছে।

হিসাবের সরল পদ্ধতি: একটি commit থেকে BFS চালিয়ে তার সব ancestor-এর set বানান, অন্যটির জন্যও তাই; দুই set-এর ছেদ = common ancestor. তারপর যেগুলো অন্য কোনো common ancestor-এর ancestor, সেগুলো বাদ দিন। Git নিজে আরও দক্ষ পদ্ধতি ব্যবহার করে (commit-এর সময় ধরে সাজানো queue আর রং দিয়ে চিহ্নিত করা), কিন্তু ফল একই। নিচের প্রোগ্রামে একটি ছোট DAG-এ merge base বের করে fast-forward না three-way, তা ঠিক করা হয়েছে; শেষ জোড়াটি একটি criss-cross merge:

// A commit DAG: each commit lists its parents (a merge commit has two).
const parents = {
  A: [], B: ["A"], C: ["B"], D: ["C"],   // main:    A - B - C - D
  E: ["B"], F: ["E"],                    // feature: branched at B, then E, F
  G: ["D"], H: ["G"],                    // hotfix:  branched at D, then G, H
  M: ["D", "F"],                         // merge commit: first parent D, second F
  P: ["M", "H"], Q: ["H", "M"]           // criss-cross: two merges in opposite directions
};
function ancestors(c) {                  // BFS over parent links; includes c itself
  const seen = new Set([c]), queue = [c];
  while (queue.length)
    for (const p of parents[queue.shift()])
      if (!seen.has(p)) { seen.add(p); queue.push(p); }
  return seen;
}
function mergeBases(x, y) {              // best common ancestors (lowest in the DAG)
  const ax = ancestors(x), ay = ancestors(y);
  const common = [...ax].filter(c => ay.has(c));
  return common.filter(c => !common.some(d => d !== c && ancestors(d).has(c)));
}
function decide(ours, theirs) {
  const bases = mergeBases(ours, theirs);
  let kind;
  if (bases.includes(theirs)) kind = "already up to date";
  else if (bases.includes(ours)) kind = "fast-forward: move pointer " + ours + " -> " + theirs;
  else kind = "three-way merge, new commit with parents " + ours + " and " + theirs;
  return "base " + bases.join(",").padEnd(4) + "| " + kind;
}
for (const [o, t] of [["D", "H"], ["H", "D"], ["D", "F"], ["M", "H"], ["P", "Q"]])
  console.log(("ours " + o + ", theirs " + t).padEnd(18) + decide(o, t));
console.log("common ancestors of D and F:", [...ancestors("D")].filter(c => ancestors("F").has(c)).join(" "));

আউটপুট:

ours D, theirs H  base D   | fast-forward: move pointer D -> H
ours H, theirs D  base D   | already up to date
ours D, theirs F  base B   | three-way merge, new commit with parents D and F
ours M, theirs H  base D   | three-way merge, new commit with parents M and H
ours P, theirs Q  base M,H | three-way merge, new commit with parents P and Q
common ancestors of D and F: B A

D আর H: H-এর ancestor-এর মধ্যে D আছে, তাই base = D = ours → fast-forward. উল্টো দিকে (H-এ বসে D merge): base = D = theirs → কিছু করার নেই। D আর F: common ancestor দুটি — B আর A; A নিজেই B-র ancestor, তাই বাদ; best = B, আর B কোনো পক্ষ নয় → three-way. M আর H: M একটি merge commit, তবুও নিয়ম একই; base D. P আর Q (criss-cross): P = merge(M, H), Q = merge(H, M) — দুই branch একে অপরকে আড়াআড়ি merge করেছে। এখন M আর H দুটিই best common ancestor, কারণ কেউ কারও ancestor নয়। git merge-base --all এমন ক্ষেত্রে দুটিই দেখায়। Git-এর ort (আর আগের recursive) strategy এমন হলে base-গুলোকে আগে নিজেরা merge করে একটি “virtual” base বানায় — বিস্তারিত পর্ব ৫৪-এ।

কেন তিনটি সংস্করণ? দুটি দিয়ে কেন হয় না

ধরুন Git শুধু দুই tip দেখল: ours-এ free_over = 1000, theirs-এ free_over = 800. কোনটি রাখবে? দুটি মাত্র লেখা দেখে বলা অসম্ভব — হয়তো ours ৮০০ থেকে ১০০০ করেছে, হয়তো theirs ১০০০ থেকে ৮০০। Base দেখলেই উত্তর মেলে: base-এ ১০০০ থাকলে ours কিছু বদলায়নি, theirs বদলেছে — তাই ৮০০। এই কারণেই একে three-way merge বলে: base, ours আর theirs. বিয়েবাড়ির উদাহরণে “সকালের মূল তালিকা” ঠিক এই base.

আমাদের গল্পে ফিরি। feature/free-delivery-এ ফ্রি ডেলিভারির সীমা ১০০০ থেকে ৮০০ করা হলো; একই সময়ে main-এ দোকানের নাম বদলানো হলো। দুই পথ আলাদা হয়ে গেছে:

$ git switch -c feature/free-delivery
Switched to a new branch 'feature/free-delivery'
# (edit shop.conf: free_over = 1000 -> 800; git add)
$ git commit -m "Lower free delivery limit to 800"
[feature/free-delivery 76d3dca] Lower free delivery limit to 800
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
# (edit shop.conf: name = Bazar Mart -> Bazar Mart Online; git add)
$ git commit -m "Rename shop"
[main ec81376] Rename shop
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --all
* ec81376 Rename shop
| * 76d3dca Lower free delivery limit to 800
|/  
*   d08a9a3 Merge branch 'feature/wallet'
|\  
| * dcebc15 Add mobile wallet payment
|/  
* f424824 Document eid discount
* b922684 Set eid discount to 10%
* d273b9b Add README
* 8373eb1 Add shop config
$ git merge-base main feature/free-delivery
d08a9a3cfc64ad87406e4e776dde7a863b121355
$ git merge --ff-only feature/free-delivery
hint: Diverging branches can't be fast-forwarded, you need to either:
hint:
hint: 	git merge --no-ff
hint:
hint: or:
hint:
hint: 	git rebase
hint:
hint: Disable this message with "git config set advice.diverging false"
fatal: Not possible to fast-forward, aborting.
$ git merge feature/free-delivery
Auto-merging shop.conf
Merge made by the 'ort' strategy.
 shop.conf | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ cat shop.conf
name = Bazar Mart Online
currency = BDT
delivery_fee = 60
free_over = 800
discount = 10
$ git log --oneline --graph -6
*   2693581 Merge branch 'feature/free-delivery'
|\  
| * 76d3dca Lower free delivery limit to 800
* | ec81376 Rename shop
|/  
*   d08a9a3 Merge branch 'feature/wallet'
|\  
| * dcebc15 Add mobile wallet payment
|/  
* f424824 Document eid discount

প্রথম log-এ দুই branch আলাদা রেখায়, দুটিরই শুরু d08a9a3 থেকে — git merge-base সেটাই বলছে। --ff-only সরাসরি অস্বীকার করল: fatal: Not possible to fast-forward, aborting. (সাথের hint লেখাটি Git-এর নতুন সংস্করণে দেখা যায়; পুরোনোগুলোতে শুধু fatal লাইনটি আসে।) সাধারণ git merge: Auto-merging shop.conf মানে Git ফাইলটির ভেতরে লাইন ধরে মিলিয়েছে, আর Merge made by the 'ort' strategy. মানে সফল, নতুন merge commit 2693581. ফাইলে দুই পক্ষের বদলই আছে: নতুন নাম (main থেকে) আর ৮০০ (feature থেকে)। Graph-এ * | আর | * দুই সমান্তরাল পথ দেখায়, |/ যেখানে তারা base-এ মিলেছে।

Merge commit — দুই parent-এর মিলনবিন্দু

Before and after a three-way merge; the feature pointer does not move

আগে d08a9a3base76d3dcaec81376 feature/free-delivery HEAD → main পরে · git merge feature/free-delivery d08a9a376d3dcaec813762693581 feature/free-delivery HEAD → main parent ১ parent ২
ছবি ১৩.২ — Merge commit 2693581-এর দুটি parent. প্রথমটি (ec81376) merge-এর আগে main যেখানে ছিল; দ্বিতীয়টি (76d3dca) আনা branch-এর tip. Merge-এর পরে feature/free-delivery নামটি মুছে ফেলা নিরাপদ — তার সব commit এখন main-এর history-তে।

৫ · Hunk ধরে ধরে মেলানো — Git কীভাবে সিদ্ধান্ত নেয়

Three-way merge প্রতিটি ফাইলে আলাদাভাবে চলে। যে ফাইল শুধু এক পক্ষ বদলেছে, সেটির ক্ষেত্রে সহজ — সেই পক্ষের সংস্করণ নেওয়া হয়। দুই পক্ষই একটি ফাইল বদলালে Git লাইনের স্তরে নামে: base থেকে ours-এর পার্থক্য আর base থেকে theirs-এর পার্থক্য বের করে (পর্ব ৯-এর git diff-এর মতো), তারপর বদলানো অংশগুলো — যাকে hunk বলে — base-এর লাইন-নম্বর ধরে পাশাপাশি সাজায়। প্রতিটি অংশে নিয়মটি এমন:

base-এর তুলনায় oursbase-এর তুলনায় theirsফল
অপরিবর্তিতঅপরিবর্তিতbase-এর লাইন
বদলেছেঅপরিবর্তিতours-এর লাইন
অপরিবর্তিতবদলেছেtheirs-এর লাইন
বদলেছেহুবহু একইভাবে বদলেছেসেই লাইন, একবার
বদলেছেভিন্নভাবে বদলেছেconflict

নিচের stepper-এ একটি সাত লাইনের shop.conf — main (ours) আর feature (theirs) দুজনেই তিনটি করে লাইন বদলেছেন। Git ২.৫৩-এ এই একই তিনটি ফাইল দিয়ে আসল merge চালিয়ে শেষ ধাপের ফল মেলানো হয়েছে।

এখন যে অংশ দেখা হচ্ছে
অংশ
base
ours (main)
theirs (feature)
নিয়ম
ফলে যোগ হলো
shop.confGit ২.৫৩-এর আসল ফল
name = Bazar Mart Online
currency = BDT
<<<<<<< HEAD
delivery_fee = 70
=======
delivery_fee = 50
>>>>>>> feature
cod = yes
free_over = 800
vat = 5
discount = 10
  1. তিনটি সংস্করণ। Base-এ সাতটি লাইন। main বদলেছে লাইন ১ (নাম), ৩ (ডেলিভারি ফি) আর ৭ (ছাড়); feature বদলেছে লাইন ৩, ৫ (ফ্রি ডেলিভারির সীমা) আর ৭. Git দুটি diff বের করে: base→ours আর base→theirs.
  2. লাইন ১ — শুধু ours বদলেছে। Base আর theirs-এ “Bazar Mart”, ours-এ “Bazar Mart Online”. যে পক্ষ বদলেছে তার কথাই থাকে: নতুন নাম। লাইন ২ (currency) কেউ ছোঁয়নি — base থেকে হুবহু।
  3. লাইন ৩ — দুই পক্ষ ভিন্নভাবে বদলেছে। Base ৬০, ours ৭০, theirs ৫০. কোনটি ঠিক, Git জানে না — এটা ব্যবসার সিদ্ধান্ত। তাই দুই লেখাই marker দিয়ে ঘিরে রাখে। লক্ষ করুন, লাইন ৪ (cod = yes) কেউ ছোঁয়নি বলেই conflict লাইন ৩-এ সীমাবদ্ধ থাকল।
  4. লাইন ৫ — শুধু theirs বদলেছে। Base আর ours-এ ১০০০, theirs-এ ৮০০ → ৮০০। দুই tip পাশাপাশি দেখে এই সিদ্ধান্ত নেওয়া যেত না; base-ই বলে দিল কে বদলেছে। লাইন ৬ অপরিবর্তিত।
  5. লাইন ৭ — দুই পক্ষ হুবহু একই বদল। দুজনেই ০ থেকে ১০ করেছেন। একই কাজ দুবার হয়েছে, বিরোধ নেই — একবার রাখা হয়। (Git এটা লাইনের লেখা মিলিয়ে বোঝে, অর্থ নয়: একজন discount = 10 আর অন্যজন discount=10 লিখলে conflict হতো।)
  6. ফল। চারটি বদলানো অংশের তিনটি নিজে থেকে মিলেছে, একটিতে conflict. Git পরিষ্কার অংশগুলো ফাইলে বসিয়ে দেয়, conflict-এর জায়গায় marker রাখে, আর commit না করে থামে। পরের অংশে এই marker পড়া আর সমাধান।

পাশের লাইনও “একই জায়গা”

একটি সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ নিয়ম: Git-এর কাছে দুই পক্ষের বদল যদি পাশাপাশি লাইনে হয় — মাঝে একটিও অপরিবর্তিত লাইন না থাকে — তাহলে সেটিও conflict. Base-এ a = 1, b = 2, c = 3; main বদলাল b, অন্য branch বদলাল c:

$ git merge other
Auto-merging f.txt
CONFLICT (content): Merge conflict in f.txt
Automatic merge failed; fix conflicts and then commit the result.
$ cat f.txt
a = 1
<<<<<<< HEAD
b = 20
c = 3
=======
b = 2
c = 30
>>>>>>> other

দুটি ভিন্ন লাইন, তবুও conflict! কারণ মাঝে কোনো “নোঙর” লাইন নেই, যা দিয়ে Git নিশ্চিত হবে দুই বদল আলাদা। তাই সে দুটি লাইন একসাথে এক hunk ধরে দুই পক্ষের পুরো রূপ দেখায়। উল্টো দিকে, দুই পক্ষ হুবহু একই বদল করলে (এখানে দুজনেই b = 20) কোনো conflict নেই:

$ git merge same2
Merge made by the 'ort' strategy.
$ cat f.txt
a = 1
b = 20
c = 3

নিচের প্রোগ্রামটি ছোট একটি three-way merge — Git-এর মতো একই নিয়মে। প্রথমে base থেকে প্রতিটি পক্ষে কোন লাইন অক্ষত আছে, তা longest common subsequence (LCS) দিয়ে বের করে (LCS-এর বিস্তারিত: DP-র ক্লাসিক সমস্যা ২)। যে base-লাইন দুই পক্ষেই অক্ষত, সেটি “নোঙর”। দুই নোঙরের মাঝের অংশে অংশ ৫-এর টেবিলের নিয়ম খাটে। আসল Git-এর diff আর merge অ্যালগরিদম অনেক উন্নত (Myers diff, histogram, ort-এর rename খোঁজা), তবে এই ছোট উদাহরণগুলোর ফল একই:

// Line-based three-way merge: base, ours, theirs -> merged lines or conflict markers.
function match(a, b) {                   // LCS: m[i] = index in b of line a[i], or -1
  const L = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
  for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--)
    L[i][j] = a[i] === b[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
  const m = new Array(a.length).fill(-1);
  for (let i = 0, j = 0; i < a.length && j < b.length; )
    if (a[i] === b[j]) m[i++] = j++; else if (L[i + 1][j] >= L[i][j + 1]) i++; else j++;
  return m;
}
function merge3(base, ours, theirs, name) {
  const mo = match(base, ours), mt = match(base, theirs), out = [], same = (x, y) => x.join() === y.join();
  let i = 0, io = 0, it = 0, conflicts = 0;
  for (;;) {
    if (i < base.length && mo[i] === io && mt[i] === it) { out.push(base[i]); i++; io++; it++; continue; }
    let j = i;                           // next base line kept by BOTH sides = sync point
    while (j < base.length && (mo[j] < 0 || mt[j] < 0)) j++;
    const eo = j < base.length ? mo[j] : ours.length, et = j < base.length ? mt[j] : theirs.length;
    const b = base.slice(i, j), o = ours.slice(io, eo), t = theirs.slice(it, et);
    if (!b.length && !o.length && !t.length) break;
    if (same(o, b)) out.push(...t);      // only theirs changed this hunk
    else if (same(t, b) || same(o, t)) out.push(...o);   // only ours, or both identical
    else { conflicts++; out.push("<<<<<<< HEAD", ...o, "=======", ...t, ">>>>>>> " + name); }
    i = j; io = eo; it = et;
  }
  console.log("--- merge " + name + ": " + (conflicts ? conflicts + " conflict(s)" : "clean")); console.log(out.join("\n"));
}
const base = ["name = Bazar Mart", "currency = BDT", "delivery_fee = 60", "free_over = 1000", "discount = 10"];
const ours = ["name = Bazar Mart Online", ...base.slice(1)];
merge3(base, ours, base.map(l => l.replace("1000", "800")), "feature/free-delivery");
merge3(base, ours.map(l => l.replace("= 60", "= 70")), base.map(l => l.replace("= 60", "= 50")), "hotfix/delivery");

আউটপুট:

--- merge feature/free-delivery: clean
name = Bazar Mart Online
currency = BDT
delivery_fee = 60
free_over = 800
discount = 10
--- merge hotfix/delivery: 1 conflict(s)
name = Bazar Mart Online
currency = BDT
<<<<<<< HEAD
delivery_fee = 70
=======
delivery_fee = 50
>>>>>>> hotfix/delivery
free_over = 1000
discount = 10

প্রথম merge: ours শুধু লাইন ১ বদলেছে, theirs শুধু লাইন ৪; মাঝে দুটি নোঙর লাইন (currency, delivery_fee) — পরিষ্কার merge, ঠিক অংশ ৪-এর আসল Git ফলের মতো। দ্বিতীয় merge: ours নাম আর ফি দুটোই বদলেছে, theirs শুধু ফি। নামের অংশ পরিষ্কারভাবে ours থেকে এল, আর ফি-র লাইনে conflict — marker-এর রূপ Git-এর মতোই, HEAD আর branch-এর নাম দিয়ে। merge3()-এর লাইনগুলো বদলে নিজে পরীক্ষা করুন: theirs-এ currency লাইনটি বদলালে প্রথম merge-ও conflict দেবে, কারণ তখন লাইন ১ আর ২ পাশাপাশি।

৬ · Conflict কখন হয়, আর marker কীভাবে পড়বেন

এবার গল্পে আসল merge conflict. ডেলিভারি ফি নিয়ে দুটি সিদ্ধান্ত একসাথে এল: একজন hotfix branch-এ ফি ৬০ থেকে ৫০ করলেন (প্রচারণার জন্য), আর অন্যজন সরাসরি main-এ ৭০ করলেন (খরচ বেড়েছে বলে)।

$ git switch -c hotfix/delivery
Switched to a new branch 'hotfix/delivery'
# (edit shop.conf: delivery_fee = 60 -> 50; git add)
$ git commit -m "Cut delivery fee to 50"
[hotfix/delivery 2960209] Cut delivery fee to 50
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
# (edit shop.conf: delivery_fee = 60 -> 70; git add)
$ git commit -m "Raise delivery fee to 70"
[main 5814e0c] Raise delivery fee to 70
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git merge hotfix/delivery
Auto-merging shop.conf
CONFLICT (content): Merge conflict in shop.conf
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   shop.conf

no changes added to commit (use "git add" and/or "git commit -a")
$ cat shop.conf
name = Bazar Mart Online
currency = BDT
<<<<<<< HEAD
delivery_fee = 70
=======
delivery_fee = 50
>>>>>>> hotfix/delivery
free_over = 800
discount = 10

তিনটি লাইন মন দিয়ে পড়ুন। CONFLICT (content): Merge conflict in shop.conf — বন্ধনীর content মানে conflict ফাইলের লেখায়। Automatic merge failed; fix conflicts and then commit the result. — Git commit করেনি, merge “চলমান” অবস্থায় আছে (ভেতরে .git/MERGE_HEAD ফাইলে theirs-এর hash রাখা থাকে)। git status বলছে You have unmerged paths, ফাইলটি both modified, আর দুটি পথ দেখিয়ে দিচ্ছে: ঠিক করে commit, অথবা git merge --abort. ফাইলে বাকি সব লাইন পরিষ্কারভাবে মিলেছে; শুধু ফি-র লাইনে marker.

Marker-এর শারীরস্থান

Markerঅর্থ
<<<<<<< HEADএখান থেকে ours-এর লেখা শুরু — বর্তমান branch, এখানে main
||||||| <base>(শুধু diff3/zdiff3 style-এ) এখান থেকে base-এর লেখা
=======বিভাজক: এর পরে theirs-এর লেখা
>>>>>>> hotfix/deliverytheirs-এর লেখা শেষ; পাশে যে branch আনা হচ্ছে তার নাম

প্রতিটি conflict marker সাতটি অক্ষরের, লাইনের শুরুতে। এগুলো সাধারণ লেখা — Git ফাইলে লিখে দিয়েছে মাত্র। তাই marker রেখে ফাইল commit করলে সেগুলো কোডেই থেকে যাবে, আর config বা প্রোগ্রাম ভেঙে যাবে। git diff --check বা অনেক CI pipeline এমন রয়ে যাওয়া marker ধরে।

তিন রকম conflict

লেখার conflict ছাড়াও Git ফাইলের অস্তিত্ব নিয়ে conflict চেনে। নিচের ছোট repository-তে cleanup branch coupon.txt মুছেছে আর নতুন banner.txt যোগ করেছে; একই সময়ে main coupon.txt বদলেছে আর নিজের একটি banner.txt যোগ করেছে:

$ git merge cleanup
Auto-merging banner.txt
CONFLICT (add/add): Merge conflict in banner.txt
CONFLICT (modify/delete): coupon.txt deleted in cleanup and modified in HEAD.  Version HEAD of coupon.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
	both added:      banner.txt
	deleted by them: coupon.txt

no changes added to commit (use "git add" and/or "git commit -a")
$ cat banner.txt
<<<<<<< HEAD
banner from main
=======
banner from cleanup
>>>>>>> cleanup
  • content — একই অংশ দুই পক্ষ ভিন্নভাবে বদলেছে (আগের উদাহরণ)। status-এ both modified.
  • add/add — দুই পক্ষই একই নামে নতুন ফাইল যোগ করেছে, লেখা ভিন্ন। Base-এ ফাইলটি নেই, তাই পুরো ফাইলই একটি conflict. status-এ both added.
  • modify/delete — এক পক্ষ বদলেছে, অন্য পক্ষ মুছেছে। Git ফাইলটি রেখে দেয় (Version HEAD of coupon.txt left in tree.), কিন্তু marker বসানোর কিছু নেই — মোছা আর রাখার মধ্যে আপনাকে বাছতে হবে। status-এ deleted by them; উল্টো হলে deleted by us. খেয়াল করুন, এখানে Git পরামর্শ দিচ্ছে git add/rm.

এ ছাড়া rename নিয়েও conflict হয় (যেমন দুই পক্ষ একই ফাইলের ভিন্ন নাম দিল — rename/rename), যা ort strategy আলাদা করে চেনে; এর বিস্তারিত পর্ব ৫৪-এ।

diff3 আর zdiff3 — base-ও দেখুন

Default marker-এ শুধু ours আর theirs দেখা যায়, base নয়। অথচ অংশ ৪-এ দেখেছেন, base ছাড়া “কে কী বদলেছে” বোঝা যায় না। merge.conflictStyle config বদলে base-ও দেখানো যায়। আগের delivery_fee merge-টি একবার abort করে zdiff3 style-এ আবার চালানো হলো:

$ git config merge.conflictStyle zdiff3
$ git merge hotfix/delivery
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 Online
currency = BDT
<<<<<<< HEAD
delivery_fee = 70
||||||| 2693581
delivery_fee = 60
=======
delivery_fee = 50
>>>>>>> hotfix/delivery
free_over = 800
discount = 10

নতুন অংশ ||||||| 2693581 — এর পরে base-এর লেখা delivery_fee = 60, আর 2693581 হলো merge base-এর ছোট hash (আগের three-way merge-এর commit-টি)। এখন এক নজরে গল্প পরিষ্কার: base ৬০ ছিল; আমরা ৭০ করেছি, ওরা ৫০. দুই পক্ষই বদলেছে, তাই সত্যিকারের সিদ্ধান্ত দরকার।

diff3 আর zdiff3-এর পার্থক্য বোঝা যায় যখন দুই পক্ষ conflict-এর আশপাশে কিছু একই বদলও করে। একটি চায়ের দোকানের মেনু: base-এ ভাত ৫০, চা ১০; দুই পক্ষই ভাত ৫৫ করেছে আর একটি একই নোট লাইন যোগ করেছে, কিন্তু চায়ের দাম একজন ১২, অন্যজন ১৫:

$ git -c merge.conflictStyle=diff3 merge theirs
Auto-merging menu.txt
CONFLICT (content): Merge conflict in menu.txt
Automatic merge failed; fix conflicts and then commit the result.
$ cat menu.txt
title = Menu
<<<<<<< HEAD
rice = 55
note = prices from Jan
tea = 12
||||||| 6d28d02
rice = 50
tea = 10
=======
rice = 55
note = prices from Jan
tea = 15
>>>>>>> theirs
end
$ git merge --abort
$ git -c merge.conflictStyle=zdiff3 merge theirs
Auto-merging menu.txt
CONFLICT (content): Merge conflict in menu.txt
Automatic merge failed; fix conflicts and then commit the result.
$ cat menu.txt
title = Menu
rice = 55
note = prices from Jan
<<<<<<< HEAD
tea = 12
||||||| 6d28d02
rice = 50
tea = 10
=======
tea = 15
>>>>>>> theirs
end

diff3: পুরো বদলানো অংশ marker-এর ভেতরে — rice = 55 আর নোট লাইন ours আর theirs দুই জায়গাতেই হুবহু একই, তবুও দুবার দেখানো হয়েছে। zdiff3 (“zealous diff3”): দুই পক্ষের মিল থাকা শুরুর লাইনগুলো marker-এর বাইরে তুলে আনে, ফলে আসল বিরোধ — শুধু চায়ের দাম — চোখে পড়ে। Base অংশটি অবশ্য দুই style-এই পুরো দেখানো হয়। Default style-এর (merge) ফলও zdiff3-এর মতোই মিল থাকা লাইন বাইরে রাখে, কিন্তু base দেখায় না। আমাদের জানা মতে zdiff3 এসেছে Git ২.৩৫-এ (২০২২); পুরোনো Git-এ diff3 ব্যবহার করুন। নিজের কম্পিউটারে সব repository-র জন্য চালু করতে: git config --global merge.conflictStyle zdiff3.

Conflict-এর সময় index-এ তিনটি সংস্করণ

Conflict চলাকালে staging area (index) একটি বিশেষ অবস্থায় থাকে। সাধারণত প্রতিটি ফাইলের একটিমাত্র entry থাকে (stage 0). Conflict-ওয়ালা ফাইলের থাকে তিনটি: stage 1 = base, stage 2 = ours, stage 3 = theirs. এগুলো দেখা আর পড়া যায়:

$ git diff
diff --cc shop.conf
index 1464268,1f0a4cd..0000000
--- a/shop.conf
+++ b/shop.conf
@@@ -1,5 -1,5 +1,9 @@@
  name = Bazar Mart Online
  currency = BDT
++<<<<<<< HEAD
 +delivery_fee = 70
++=======
+ delivery_fee = 50
++>>>>>>> hotfix/delivery
  free_over = 800
  discount = 10
$ git log --merge --oneline
5814e0c Raise delivery fee to 70
2960209 Cut delivery fee to 50
$ git ls-files -u
100644 960fa0af662d03fdea3bf18a4dd36bdb1108e033 1	shop.conf
100644 14642685fe346b8302320db59940606009daee46 2	shop.conf
100644 1f0a4cde2aab4d5f81a2ff7c4336d4c7a93069e7 3	shop.conf
$ git show :1:shop.conf
name = Bazar Mart Online
currency = BDT
delivery_fee = 60
free_over = 800
discount = 10
$ git merge --abort
$ git status --short

git diff conflict চলাকালে একটি combined diff দেখায় (diff --cc): দুটি কলামের +/- চিহ্ন — প্রথম কলাম ours-এর তুলনায়, দ্বিতীয়টি theirs-এর তুলনায়। ++ মানে দুই পক্ষের কোনোটিতেই এই লাইন নেই (marker), + মানে লাইনটি ours-এ আছে কিন্তু theirs-এ নেই (এখানে delivery_fee = 70), + মানে উল্টো — এই লাইনটি (delivery_fee = 50) theirs-এ আছে, ours-এ নেই। git log --merge শুধু সেই commit-গুলো দেখায় যেগুলো conflict-ওয়ালা ফাইল ছুঁয়েছে, merge base-এর পর থেকে দুই পক্ষে — এখানে ঠিক দুটি: কে ৭০ করলেন, কে ৫০. কাকে জিজ্ঞেস করতে হবে, এখান থেকেই জানা যায়। git ls-files -u (unmerged) তিনটি stage দেখায়; git show :1:shop.conf base-এর পুরো ফাইল দেখায় (:2: ours, :3: theirs). শেষে git merge --abort সব ফিরিয়ে দিল merge-এর আগের অবস্থায় — git status --short কিছুই দেখাচ্ছে না।

git merge --abort-এর সীমা: এটি merge-এর আগের অবস্থা ফিরিয়ে আনে, কিন্তু merge শুরুর সময় যদি working tree-তে commit না করা বদল থাকত, সেগুলো সবসময় ঠিকভাবে ফেরানো যায় না (Git-এর documentation নিজেই এ ব্যাপারে সতর্ক করে)। তাই merge-এর আগে বদলগুলো commit করুন বা git stash করুন (stash পর্ব ৫৬-এ)। আর conflict ঠিক করতে গিয়ে AI বা কোনো blog যদি git reset --hard বলে, মনে রাখুন — সেটা commit না করা সব কাজ মুছে দেয় (পর্ব ১১)।

৭ · Conflict সমাধান — শুরু থেকে শেষ

Conflict সমাধানের মূল কাজটি কারিগরি নয়, সিদ্ধান্তের: “ফাইলটি এখন ঠিক কেমন হওয়া উচিত?” কখনো উত্তর এক পক্ষ, কখনো অন্য পক্ষ, কখনো দুটির মিশ্রণ, কখনো একেবারে নতুন কিছু। নিচের stepper-এ ডেলিভারি ফি-র conflict পুরোটা সমাধান করা হয়েছে; প্রতিটি বার্তা আসল আউটপুট থেকে নেওয়া।

Terminal আর repository-র অবস্থা
Command
Git-এর বার্তা
index-এ shop.conf
merge চলছে?
shop.confzdiff3 style-এ marker
name = Bazar Mart Online
currency = BDT
<<<<<<< HEAD
delivery_fee = 70
||||||| 2693581
delivery_fee = 60
=======
delivery_fee = 50
>>>>>>> hotfix/delivery
free_over = 800
discount = 10
shop.confসমাধানের পরে — কোনো marker নেই
name = Bazar Mart Online
currency = BDT
delivery_fee = 50
free_over = 800
discount = 10
git log --oneline --graphপ্রথম ছয় লাইন
*   82b90b1 Merge branch 'hotfix/delivery'
|\
| * 2960209 Cut delivery fee to 50
* | 5814e0c Raise delivery fee to 70
|/
*   2693581 Merge branch 'feature/free-delivery'
  1. Merge, আর conflict. Git পরিষ্কার অংশগুলো মিলিয়ে ফেলেছে, কিন্তু shop.conf-এর একটি অংশে পারেনি। Merge এখন “চলমান”: commit হয়নি, MERGE_HEAD theirs-কে মনে রাখছে, আর index-এ ফাইলটির তিনটি stage.
  2. কোন কোন ফাইল? git status-এর “Unmerged paths” তালিকাই আপনার কাজের তালিকা। বড় merge-এ দশটি ফাইল বদলালেও হয়তো একটিতে conflict — বাকিগুলো Git ইতিমধ্যে মিলিয়ে stage করে রেখেছে।
  3. ফাইল খুলে marker পড়ুন। zdiff3 style চালু থাকায় তিনটি লেখাই দেখা যাচ্ছে: ours ৭০, base ৬০, theirs ৫০. শুধু marker-এর ভেতরের অংশ নিয়ে ভাবতে হবে; বাইরের লাইনগুলো ইতিমধ্যে মেলানো।
  4. কে, কেন বদলেছেন? git log --merge দেখাল দুটি commit. বার্তা পড়ে বোঝা যায় একটি খরচের কারণে, অন্যটি প্রচারণার জন্য। Git এই সিদ্ধান্ত নিতে পারে না — দুজনের সাথে কথা বলুন। ধরা যাক, দল ঠিক করল ঈদের মাসে ৫০ থাকবে।
  5. ফাইল ঠিক করুন। তিনটি marker লাইন আর বাদ পড়া সংস্করণ মুছে শুধু delivery_fee = 50 রাখা হলো। এখন ফাইলটি দেখতে যেমন হওয়া উচিত, ঠিক তেমন। কিন্তু Git এখনো জানে না যে আপনি শেষ করেছেন।
  6. git add = “এটা সমাধান হয়েছে”। Merge-এর সময় git add-এর বিশেষ অর্থ: index-এর তিনটি stage মুছে stage 0-তে আপনার সংস্করণ বসানো। git status এখন বলছে All conflicts fixed but you are still merging. (modify/delete-এর ক্ষেত্রে মুছতে চাইলে git rm.)
  7. Merge শেষ করুন। git merge --continue (Git ২.১২ থেকে) বা সাধারণ git commit — দুটোই merge commit বানায়, ডিফল্ট বার্তা Merge branch 'hotfix/delivery'. বার্তায় কেন ৫০ বাছা হলো তা যোগ করা ভালো অভ্যাস। MERGE_HEAD মুছে যায়।
  8. ফল দেখুন। নতুন merge commit 82b90b1, দুই parent: 5814e0c (ours) আর 2960209 (theirs). এরপর test চালানো বাকি — অংশ ৮-এ দেখবেন কেন।

একই ধাপগুলো আসল terminal-এ (ফাইল সম্পাদনার পরে):

# (edit shop.conf: keep only "delivery_fee = 50", remove the markers)
$ git add shop.conf
$ git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   shop.conf
$ git merge --continue
[main 82b90b1] Merge branch 'hotfix/delivery'

কাজের হাতিয়ার

  • এক পক্ষ পুরো নেওয়া: git checkout --ours <file> বা git checkout --theirs <file> conflict-ওয়ালা ফাইলটিকে index-এর stage 2 বা 3 থেকে লিখে দেয়। Git ২.২৩ থেকে একই কাজ git restore --ours/--theirs <file>. এরপরও git add লাগে। সতর্কতা: এটি পুরো ফাইলের জন্য — অন্য পক্ষের পরিষ্কারভাবে মেলানো বদলও হারাতে পারেন। আর rebase-এর সময় ours আর theirs-এর অর্থ উল্টে যায় (পর্ব ১৫)।
  • git mergetool: config-এ ঠিক করা একটি তুলনা-টুল (যেমন vimdiff, meld, বা কোনো editor-এর merge view) প্রতিটি conflict ফাইলের জন্য খুলে দেয়, সাধারণত base-ours-theirs-ফল চারটি অংশ পাশাপাশি। git config merge.tool <name> দিয়ে টুল বাছাই হয়; কোন নাম সমর্থিত, তা git mergetool --tool-help দেখায়। ডিফল্ট সেটিংসে (mergetool.keepBackup) এটি প্রায়ই .orig নামে backup ফাইল রেখে যায়, যা commit করবেন না।
  • Editor: অনেক আধুনিক editor marker চিনে “Accept current / incoming / both” বোতাম দেখায়। “Current” = ours, “incoming” = theirs. “Both” অন্ধভাবে চাপলে দুই সংস্করণই থেকে যায় — config-এ একই key দুবার, কোডে একই function দুবার।
  • ছোট কৌশল: সমাধানের পরে git diff --staged দিয়ে দেখুন merge commit-এ ঠিক কী যাচ্ছে; git diff --check রয়ে যাওয়া marker ধরে।

অংশ ৬-এর add/add আর modify/delete conflict-এর সমাধান এভাবে — ব্যানার নেওয়া হলো cleanup থেকে, আর কুপন ফাইল মুছে ফেলার সিদ্ধান্ত মানা হলো:

$ git checkout --theirs banner.txt
Updated 1 path from the index
$ cat banner.txt
banner from cleanup
$ git add banner.txt
$ git rm coupon.txt
rm 'coupon.txt'
$ git status --short
M  banner.txt
D  coupon.txt
$ git commit -m "Merge cleanup: drop coupon, take their banner"
[main d13d4c1] Merge cleanup: drop coupon, take their banner

git checkout --theirs banner.txt ফাইলটিকে theirs-এর সংস্করণে বদলে দিল (Updated 1 path from the index), git add সেটিকে resolved চিহ্নিত করল। git rm coupon.txt মুছে ফেলার সিদ্ধান্তকে resolved করল। git status --short-এ M আর D — দুটিই stage করা। তারপর commit, এবার নিজের বার্তায়, যাতে পরে কেউ বুঝতে পারেন কী সিদ্ধান্ত হয়েছিল।

মূল ধারণা

Conflict ভুল নয়, Git-এর সততা। দুই মানুষ একই জিনিস ভিন্নভাবে চেয়েছেন — Git আন্দাজে একটি বেছে না নিয়ে আপনাকে জানাচ্ছে। সমাধানের মান মাপা হয় তিনটি প্রশ্নে: কোনো marker রয়ে গেল কি? দুই পক্ষের উদ্দেশ্য কি রক্ষা হলো? Test কি পাস করছে?

৮ · Semantic conflict — পরিষ্কার merge, ভাঙা প্রোগ্রাম

সবচেয়ে বিপজ্জনক conflict হলো সেটি, যা Git দেখায়ই না। Git লাইনের লেখা মেলায়; প্রোগ্রামের অর্থ বোঝে না। দুটি পরিবর্তন আলাদা জায়গায় হলে merge পরিষ্কার — কিন্তু একসাথে তারা প্রোগ্রাম ভেঙে দিতে পারে। একে বলে semantic conflict (logical conflict নামেও পরিচিত)।

একটি ছোট bill.js: main-এ একজন fee() function-এর নাম বদলে deliveryFee() করলেন, আর পুরোনো সব জায়গায় নতুন নাম বসালেন — নিখুঁত refactoring. একই সময়ে feature/invoice-এ আরেকজন একটি নতুন function যোগ করলেন, যা পুরোনো নাম fee() ডাকে। আসল Git দিয়ে merge:

$ git merge feature/invoice
Auto-merging bill.js
Merge made by the 'ort' strategy.
 bill.js | 1 +
 1 file changed, 1 insertion(+)
$ git grep -nw fee
bill.js:5:function invoiceLine(total) { return "Delivery: " + fee(total); }

কোনো conflict নেই — অথচ লাইন ৫ এমন একটি function ডাকছে, যা আর নেই। নিচের প্রোগ্রাম একই চারটি সংস্করণে (base, ours, theirs, merged) একটি ছোট test চালায়:

// Semantic conflict: two clean, separate edits that break the program only together.
const base = [
  'function fee(total) { return total >= 1000 ? 0 : 60; }',
  'function checkout(total) { return total + fee(total); }',
  '// ---- invoice helpers ----',
  'function invoiceTitle() { return "Bazar Mart"; }'];
// main renames fee -> deliveryFee (changes lines 1-2 only)
const ours = base.map(l => l.replace(/\bfee\(/g, "deliveryFee("));
// feature adds a NEW caller of fee() after line 4 (an insertion only)
const added = 'function invoiceLine(total) { return "Delivery: " + fee(total); }';
const theirs = [...base, added];
// The two hunks touch lines 1-2 and "after 4": separated by unchanged lines -> no textual conflict.
const merged = [...ours, added];
function test(label, lines) {            // a tiny test runner
  const results = [];
  const check = (name, fn) => {
    try { const ok = fn(); results.push((ok ? "PASS " : "FAIL ") + name); }
    catch (e) { results.push("ERROR " + name + ": " + e.name + ": " + e.message); }
  };
  const api = new Function(lines.join("\n") +
    "\nreturn { checkout, invoiceLine: typeof invoiceLine === 'function' ? invoiceLine : null };")();
  check("checkout(500) is 560", () => api.checkout(500) === 560);
  if (api.invoiceLine) check("invoiceLine(500) shows 60", () => api.invoiceLine(500) === "Delivery: 60");
  console.log(label.padEnd(8) + results.join(" | "));
}
test("base", base);
test("ours", ours);
test("theirs", theirs);
test("merged", merged);
console.log("textual conflict markers in merged file:", merged.some(l => l.startsWith("<<<<<<<")));

আউটপুট:

base    PASS checkout(500) is 560
ours    PASS checkout(500) is 560
theirs  PASS checkout(500) is 560 | PASS invoiceLine(500) shows 60
merged  PASS checkout(500) is 560 | ERROR invoiceLine(500) shows 60: ReferenceError: fee is not defined
textual conflict markers in merged file: false

প্রতিটি branch আলাদাভাবে সম্পূর্ণ সবুজ: ours-এ সব test পাস, theirs-এও। দুজনের code review-ও হয়তো নিখুঁত ছিল। কিন্তু merged ফাইলে invoiceLine ভেঙে গেছে — ReferenceError: fee is not defined. আর merged ফাইলে একটিও conflict marker নেই (false). JavaScript-এর মতো ভাষায় এটা ধরা পড়ে শুধু কোড চালালে; typed ভাষায় (Java, TypeScript) compile-এর সময় ধরা পড়ে — কিন্তু তার জন্যও merge-এর পরে build চালাতে হয়। Merge-এর ফলাফল তাই একটি নতুন, কখনো-না-পরীক্ষিত প্রোগ্রাম।

এর প্রতিকার একটিই নীতি: merge-এর ফলাফলকে test করুন, শুধু branch-কে নয়। বাস্তবে এর রূপ:

  • Merge-এর পরে (বা আগে, নিজের branch-এ main merge করে) পুরো unit test আর build চালানো।
  • Continuous integration: প্রতিটি push আর pull request-এ স্বয়ংক্রিয় build-test (পর্ব ৫৮)। অনেক CI ব্যবস্থা pull request-কে লক্ষ্য branch-এর সাথে merge করা অবস্থায় test করে — তবে নির্দিষ্ট আচরণ platform-ভেদে আলাদা, আর PR পুরোনো হয়ে গেলে test-এর ফলও পুরোনো হয়।
  • কিছু দল “merge queue” ব্যবহার করে: merge-এর ঠিক আগে সর্বশেষ main-এর সাথে মিলিয়ে আবার test, পাস করলে তবেই merge.
  • Rename-এর মতো বড় refactoring দলকে জানিয়ে, ছোট ধাপে আর দ্রুত merge করা — যাতে অন্যরা পুরোনো নামের উপর বেশিদিন কাজ না করেন।

৯ · Conflict কমানো আর merge-এর history পড়া

Conflict পুরোপুরি এড়ানো যায় না, কিন্তু তার সংখ্যা আর আকার অনেকটাই আমাদের অভ্যাসের উপর নির্ভর করে। একটি সরল মডেল দিয়ে দেখি: ৩০০ লাইনের একটি ফাইল, দুটি branch প্রত্যেকে k-টি এলোমেলো লাইন বদলায়। Git-এর নিয়মে একই বা পাশাপাশি লাইন হলে conflict. প্রতিটি k-এর জন্য ২০০০ বার পরীক্ষা, seed-যুক্ত PRNG (mulberry32) দিয়ে, যাতে প্রতিবার একই ফল আসে:

// How often do two branches conflict? Each branch rewrites k random lines of a
// 300-line file. As in Git, a conflict happens when the two sides change the same
// line or two neighbouring lines (no unchanged line between their hunks).
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const LINES = 300, TRIALS = 2000;
function pick(rnd, k) {                  // k distinct random line numbers
  const s = new Set();
  while (s.size < k) s.add(Math.floor(rnd() * LINES));
  return s;
}
function conflictRate(k) {
  const rnd = mulberry32(2026);
  let hits = 0;
  for (let n = 0; n < TRIALS; n++) {
    const ours = pick(rnd, k), theirs = pick(rnd, k);
    let clash = false;
    for (const i of ours) if (theirs.has(i) || theirs.has(i - 1) || theirs.has(i + 1)) { clash = true; break; }
    if (clash) hits++;
  }
  return hits / TRIALS;
}
console.log("lines changed per branch   P(conflict)");
for (const k of [1, 3, 5, 10, 20, 40])
  console.log(String(k).padStart(12) + (100 * conflictRate(k)).toFixed(1).padStart(22) + "%");

আউটপুট:

lines changed per branch   P(conflict)
           1                   1.1%
           3                   9.3%
           5                  23.5%
          10                  64.3%
          20                  98.6%
          40                 100.0%

k = ১ হলে তত্ত্বীয় সম্ভাবনা প্রায় ৩/৩০০ = ১% (একই লাইন বা দুই পাশের একটি) — সিমুলেশন ১.১%. কিন্তু k দ্বিগুণ হলে সম্ভাবনা দ্বিগুণের অনেক বেশি বাড়ে: ৫ লাইনে ২৩.৫%, ১০ লাইনে ৬৪.৩%, ২০ লাইনে প্রায় নিশ্চিত। কারণ সংঘর্ষের সম্ভাব্য জোড়া বাড়ে মোটামুটি k × k হারে — জন্মদিন-সমস্যার (birthday problem) মতো। মডেলটি সরল: বাস্তবে বদল এলোমেলো নয়, কিছু “গরম” ফাইলে (route তালিকা, config, dependency-র তালিকা) ভিড় বেশি, তাই সেখানে সম্ভাবনা আরও বেশি। কিন্তু মূল শিক্ষা টেকে: বড় আর দীর্ঘজীবী branch-এ conflict দ্রুত বাড়ে।

যে অভ্যাসগুলো conflict কমায়

ছোট pull request

একটি PR-এ একটি উদ্দেশ্য। কম লাইন মানে কম সংঘর্ষ, আর review-ও সহজ (পর্ব ১৬, ৪৪)।

small batches

ঘন ঘন মেলানো

দিনে অন্তত একবার main-এর সাথে মেলান। Trunk-based development (পর্ব ১৭) আর অসমাপ্ত কাজ লুকাতে feature flag এর ভিত্তি।

integrate often

Formatting-এর নিয়ম

সবাই একই formatter আর line ending (পর্ব ১০-এর .gitattributes) ব্যবহার করলে “শুধু space বদল” থেকে আসা conflict হয় না। পুরো ফাইল reformat আলাদা commit-এ।

one style

কথা বলা

“আমি আজ checkout module-এর নাম বদলাচ্ছি” — একটি বার্তা অনেক conflict বাঁচায়। Kanban board-এ কে কী ধরেছেন তা দৃশ্যমান রাখুন (পর্ব ৬)।

communicate

ফাইলের নকশাও সাহায্য করে: সবাই যে তালিকার শেষে লাইন যোগ করেন (যেমন route-এর তালিকা), সেখানে প্রতিটি entry আলাদা লাইনে আর বর্ণানুক্রমে রাখলে দুই পক্ষের যোগ সাধারণত আলাদা জায়গায় পড়ে। আর একই conflict বারবার সমাধান করতে হলে Git-এর rerere (“reuse recorded resolution”) আগের সমাধান মনে রাখতে পারে — পর্ব ৫৪-এ।

History-তে merge commit পড়া

আমাদের গল্পের শেষে repository-র পুরো ছবি:

$ git log --oneline --graph
*   82b90b1 Merge branch 'hotfix/delivery'
|\  
| * 2960209 Cut delivery fee to 50
* | 5814e0c Raise delivery fee to 70
|/  
*   2693581 Merge branch 'feature/free-delivery'
|\  
| * 76d3dca Lower free delivery limit to 800
* | ec81376 Rename shop
|/  
*   d08a9a3 Merge branch 'feature/wallet'
|\  
| * dcebc15 Add mobile wallet payment
|/  
* f424824 Document eid discount
* b922684 Set eid discount to 10%
* d273b9b Add README
* 8373eb1 Add shop config
$ git log --oneline --first-parent
82b90b1 Merge branch 'hotfix/delivery'
5814e0c Raise delivery fee to 70
2693581 Merge branch 'feature/free-delivery'
ec81376 Rename shop
d08a9a3 Merge branch 'feature/wallet'
f424824 Document eid discount
b922684 Set eid discount to 10%
d273b9b Add README
8373eb1 Add shop config
$ git log --oneline --merges
82b90b1 Merge branch 'hotfix/delivery'
2693581 Merge branch 'feature/free-delivery'
d08a9a3 Merge branch 'feature/wallet'

--graph: তিনটি merge তিনটি কুঁজ। নিচ থেকে পড়ুন: প্রথম চারটি commit সরলরেখায় (ঈদের ছাড়ের fast-forward — কোনো চিহ্ন রাখেনি!), তারপর --no-ff-এর কুঁজ, যেখানে main-এর দিকে কোনো commit নেই, তারপর দুটি আসল three-way merge, যেখানে দুই দিকেই commit. --first-parent: শুধু প্রতিটি merge commit-এর প্রথম parent ধরে চলে — main-এর নিজের দৃষ্টিতে history. এখানে dcebc15, 76d3dca আর 2960209 নেই, কারণ সেগুলো branch-এর ভেতরের commit. main-এ সব কাজ যদি merge দিয়ে ঢুকত, এই তালিকা হতো শুধু “কোন feature কখন ঢুকল”। --merges: শুধু merge commit-গুলো (দুই বা বেশি parent). উল্টোটা --no-merges.

খেয়াল করুন, ঈদের ছাড়ের দুটি commit (b922684, f424824) fast-forward হওয়ায় এখন দেখে বোঝার উপায় নেই যে সেগুলো একটি আলাদা branch-এ হয়েছিল। এটাই অংশ ৩-এর বিনিময়: সরল history বনাম সীমানা-চিহ্নিত history. Rebase দিয়ে history সরলরেখায় রাখার পদ্ধতি আর merge বনাম rebase-এর তুলনা পর্ব ১৫-এ।

আরও গভীরে: Three-way merge-এর ধারণা Git-এর চেয়ে অনেক পুরোনো — Unix-এর diff3 টুল আর ১৯৮০-র দশকে Walter Tichy-র RCS-এর merge command একই নীতিতে কাজ করত। Git-এর প্রথম দিকের default strategy ছিল “recursive”, যা criss-cross ক্ষেত্রে একাধিক base-কে আগে merge করে নিত; Elijah Newren নতুন করে লেখা “ort” (Ostensibly Recursive’s Twin) strategy বানান, যা আমাদের জানা মতে Git ২.৩৩-এ আসে আর ২.৩৪ থেকে default. Git-এর Documentation/technical আর git help merge-এর “HOW CONFLICTS ARE PRESENTED” অংশ marker আর style-এর নির্ভরযোগ্য উৎস। Merge base, ort, rename-খোঁজা আর rerere-এর ভেতরের কথা পর্ব ৫৪-এ।

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

  • Fast-forward merge কখন হয়? — যখন বর্তমান branch-এর tip আনা branch-এর ancestor; Git শুধু pointer সরায়, নতুন commit হয় না।
  • Three-way merge-এর তিনটি “way” কী কী? — merge base (best common ancestor), ours (HEAD) আর theirs (আনা branch).
  • Merge commit আর সাধারণ commit-এর পার্থক্য? — merge commit-এর দুই (বা বেশি) parent; প্রথমটি merge-এর সময়ের বর্তমান branch.
  • Conflict হলে কী কী করবেন? — git status, ফাইল ঠিক করে marker মোছা, git add, git commit বা git merge --continue; ফিরে যেতে git merge --abort.
  • Conflict ছাড়া merge হলেও কেন test চালাবেন? — semantic conflict: লেখা আলাদা জায়গায়, কিন্তু অর্থ ভাঙে; শুধু build/test ধরে।

১০ · AI-এর যুগে — conflict সমাধানে AI-কে যাচাই

AI coding assistant এখন conflict-ওয়ালা ফাইল দেখে “সমাধান” লিখে দেয়, terminal-এর error দেখে command সাজেস্ট করে। দ্রুত, কিন্তু এই বিষয়ে এর কিছু চেনা ভুল আছে:

AI-এর ভুলকীভাবে ধরবেন
Conflict দেখেই git reset --hard বা git push --force — কোনো সতর্কবার্তা ছাড়াযেকোনো ধ্বংসাত্মক command-এর আগে জিজ্ঞেস করুন “কী হারাবে?”; আগে git merge --abort বা একটি throwaway clone-এ চালান
পুরো ফাইলে --ours/--theirs বা -X theirs — অন্য পক্ষের বদল নীরবে হারায়সমাধানের পরে git diff দিয়ে দেখুন দুই পক্ষের উদ্দেশ্য টিকে আছে কি না
“Accept both” — দুই সংস্করণ পাশাপাশি রেখে দেওয়া (দুবার একই key বা function)ফাইল পড়ুন; config parse করুন, কোড চালান
বানানো flag (যেমন অস্তিত্বহীন --resolve-all) বা ভুল Git সংস্করণের দাবিgit help merge আর অফিসিয়াল documentation; git --version
Rebase-এর সময় ours/theirs-এর অর্থ গুলিয়ে ফেলাmarker-এর পাশের নাম আর git status পড়ুন
Marker সরিয়েছে, কিন্তু semantic conflict-এর খোঁজ নেয়নিMerge-এর পরে পুরো test আর build; CI-এর ফল দেখুন
Prompt-এ পুরো config ফাইল — সাথে password বা API keyAI-কে দেওয়ার আগে গোপন মান মুছে দিন (পর্ব ৫৭)

নিচের prompt-এ AI-কে শুধু marker-এর ভেতরের অংশ আর base দেওয়া হয়েছে, আর তাকে নিজের সিদ্ধান্তের কারণ দেখাতে বলা হয়েছে — যাতে আপনি যাচাই করতে পারেন, অন্ধভাবে মেনে না নেন।

একটি Git merge conflict সমাধানে সাহায্য চাই। Conflict style zdiff3, তাই base-ও দেওয়া আছে: [এখানে শুধু marker-সহ অংশটি — password, token বা গ্রাহকের তথ্য মুছে] দুই পক্ষের commit বার্তা: ours = "...", theirs = "..." ১. প্রথমে বলো base থেকে ours কী বদলেছে আর base থেকে theirs কী বদলেছে — আলাদা করে। ২. দুই পক্ষের উদ্দেশ্য একসাথে রক্ষা করা যায় এমন একটি সমাধান দাও; না গেলে কোন প্রশ্ন দলকে জিজ্ঞেস করা উচিত তা বলো। ৩. কোনো reset, force push বা পুরো ফাইলে --ours/--theirs সাজেস্ট করবে না। যে Git command দেবে, তার প্রতিটি কী বদলায় (working tree, index, ref) তা লেখো। ৪. এই সমাধানের পরে কোন test বা পরীক্ষা চালালে semantic conflict ধরা পড়বে, তা বলো।

AI-এর দেওয়া conflict-সমাধান হাতে পেলে — চেকলিস্ট

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

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

প্র ০১পুরোনো centralized version control ব্যবস্থায় merge ছিল ভয়ের নাম। Git-এ কেন সেটা প্রতিদিনের সাধারণ কাজ হয়ে গেল?

পর্ব ৭-এ (Version control কী) দেখেছেন, Git-এর আগে CVS আর Subversion-এর মতো centralized ব্যবস্থা প্রচলিত ছিল। সেখানেও branch আর merge ছিল, কিন্তু দুটি বড় সমস্যা ছিল। প্রথমত, পুরোনো ব্যবস্থাগুলো অনেক দিন পর্যন্ত মনে রাখত না যে কোন branch কোন পর্যন্ত আগেই merge হয়েছে। CVS-এ একই branch দ্বিতীয়বার merge করলে আগের বদল আবার আসার চেষ্টা করত, তাই মানুষ হাতে tag বসিয়ে হিসাব রাখতেন। Subversion ২০০৮ সালের দিকে (১.৫ সংস্করণে) “merge tracking” যোগ করে, কিন্তু সেটি আলাদা metadata-র উপর নির্ভর করত।

Git-এ এই সমস্যা কাঠামোগতভাবেই নেই। Merge commit দুই parent মনে রাখে, তাই commit graph নিজেই বলে দেয় কী আগে মেলানো হয়েছে। পরের বার merge base হিসাব করলে সেটি স্বয়ংক্রিয়ভাবে শেষ merge-এর জায়গায় চলে আসে। আমাদের গল্পে হটফিক্সের merge base ছিল আগের three-way merge-এর commit 2693581 — Git জানত তার আগের সব কাজ মেলানো হয়ে গেছে। কেউ আলাদা হিসাব রাখেননি।

দ্বিতীয়ত, centralized ব্যবস্থায় branch বানানো মানে server-এ পুরো folder কপি (সস্তা কপি হলেও ধারণাটা ভারী), আর প্রতিটি কাজে network লাগত। Git-এ branch একটি ছোট ফাইল, যাতে একটি hash লেখা; merge হয় নিজের কম্পিউটারে, মুহূর্তে। Linus Torvalds ২০০৫ সালে Git বানানোর সময় Linux kernel-এর হাজারো patch দ্রুত মেলানোকেই মূল লক্ষ্য ধরেছিলেন — তাই merge-কে প্রথম শ্রেণির কাজ হিসেবে নকশা করা হয়।

তৃতীয় কারণ সামাজিক। Merge সস্তা হলে মানুষ ছোট branch বানায়, দ্রুত মেলায়, আর conflict ছোট থাকে। ভয় থাকলে মানুষ branch দীর্ঘদিন রাখে, তারপর “integration-এর সপ্তাহ” আসে — যেখানে conflict বিশাল। অর্থাৎ টুলের নকশা অভ্যাস বদলায়, আর অভ্যাস conflict-এর আকার বদলায় — অংশ ৯-এর সিমুলেশনের কথা মনে করুন।

মূল উপলব্ধি: Git-এ merge সহজ হয়েছে কারণ history একটি DAG, যেখানে merge commit নিজেই “কী মেলানো হয়েছে” তার রেকর্ড। আর সহজ merge ছোট, ঘন ঘন merge-এর অভ্যাস তৈরি করে।

প্র ০২“সবসময় --no-ff” বনাম “সবসময় সরলরেখা” — দলের নীতি ঠিক করার সময় কোন কোন বিনিময় বিবেচনা করবেন?

২০১০ সালে Vincent Driessen-এর একটি blog post (“A successful Git branching model”) Git Flow নামে পরিচিত হয়, আর সেখানে feature merge করার সময় --no-ff ব্যবহারের পরামর্শ ছিল। যুক্তি: history-তে প্রতিটি feature-এর সীমানা থাকবে, আর পুরো feature একটি commit revert করে ফেরানো যাবে। অন্যদিকে অনেক দল ও open-source project “linear history” পছন্দ করে — merge-এর আগে rebase বা squash করে সব commit এক সরলরেখায়। দুই পক্ষেরই যুক্তি আছে।

Merge commit-এর পক্ষে: (১) সত্যিকারের ইতিহাস — কোন কাজ কখন, কোন branch-এ, কোন base থেকে হয়েছিল, সব রক্ষা পায়। (২) git log --first-parent দিয়ে মূল branch-এর “feature-এর তালিকা” পাওয়া যায়। (৩) একটি git revert -m 1 দিয়ে পুরো feature ফেরানো যায় (পর্ব ১১)। (৪) কেউ history নতুন করে লেখেন না, তাই shared branch-এ নিরাপদ।

সরলরেখার পক্ষে: (১) git log পড়া সহজ, graph-এ জট নেই। (২) git bisect (পর্ব ৫৫) দিয়ে bug খোঁজার সময় প্রতিটি commit একটি স্পষ্ট ধাপ। (৩) squash করলে “typo ঠিক”, “আবার চেষ্টা” জাতীয় ছোট commit মূল history-তে আসে না। অসুবিধা: rebase আসলে নতুন commit বানায়, আর squash branch-এর ভেতরের ধাপগুলো হারায়।

বাস্তব সিদ্ধান্ত প্রায়ই মাঝামাঝি। অনেক দল নিজের branch-এ rebase করে পরিষ্কার রাখে, তারপর --no-ff merge করে সীমানা রাখে। অনেক hosting platform-এ pull request merge-এর তিন রকম বোতাম থাকে, আর repository-র মালিক কোনগুলো চালু থাকবে তা ঠিক করেন (বিস্তারিত platform-ভেদে আলাদা; পর্ব ১৬)। গুরুত্বপূর্ণ হলো নীতিটি লিখিত আর সবাই মানেন — এক দল অর্ধেক merge আর অর্ধেক squash করলে history-র কোনো চেহারাই নির্ভরযোগ্য থাকে না।

মূল উপলব্ধি: প্রশ্নটি “কোনটি ঠিক” নয়, “আমাদের দল history দিয়ে কোন প্রশ্নের উত্তর খোঁজে” — feature-এর সীমানা, bisect-এর সুবিধা, না সহজে পড়া। উত্তর দেখে নীতি বাছুন, লিখে রাখুন।

প্র ০৩Merge base একাধিক হলে কী হয়? “Criss-cross merge” কেন Git-এর নকশাকারদের মাথাব্যথা ছিল?

অংশ ৪-এর প্রোগ্রামে P আর Q-এর merge base ছিল দুটি — M আর H. এমন হয় যখন দুই branch একে অপরকে আড়াআড়ি merge করে: একজন main-কে feature-এ আনলেন, প্রায় একই সময়ে আরেকজন feature-কে main-এ আনলেন। এরপর দুই branch আবার মেলালে “সবচেয়ে কাছের common ancestor” আর একটি থাকে না, কারণ দুটি merge commit-এর কেউ কারও ancestor নয়। বড় দলে, যেখানে অনেকে নিয়মিত main থেকে নিজের branch-এ merge করেন, এটা অস্বাভাবিক নয়।

একটি base বাছাই করলে কী হয়? যে base বাছা হলো না, তার পরের বদলগুলো merge-এর চোখে “নতুন বদল” মনে হতে পারে, আর ভুল conflict দেখা দিতে পারে — কিংবা আরও খারাপ, কোনো বদল নীরবে উল্টে যেতে পারে। Git-এর পুরোনো “resolve” strategy একটি base বেছে নিত। ২০০৫-০৬ সালের দিকে Fredrik Kuivinen-এর লেখা “recursive” strategy সমাধান আনে: আগে base-গুলোকে নিজেদের মধ্যে merge করে একটি কাল্পনিক (virtual) base বানানো, তারপর সেটির সাপেক্ষে আসল merge. Base-গুলো মেলাতে গিয়ে আবার একাধিক base পেলে একই কাজ recursive-ভাবে — নাম সেখান থেকেই।

Recursive অনেক বছর default ছিল, কিন্তু এর কিছু জটিল কোণে (বিশেষত rename আর virtual base-এর ভেতরের conflict) ধীর বা অপ্রত্যাশিত আচরণ ছিল। Elijah Newren কয়েক বছরের কাজের পর “ort” strategy নতুন করে লেখেন, যা একই ধারণা রাখে কিন্তু অনেক দ্রুত আর rename-এর ক্ষেত্রে নির্ভুল। আমাদের জানা মতে ort ২.৩৪ থেকে default; এখনকার Git-এ -s recursive চাইলেও ভেতরে ort ব্যবহার হয় বলে documentation-এ বলা আছে — আপনার সংস্করণের git help merge দেখে নিশ্চিত হোন।

ব্যবহারিক দিক: criss-cross এড়ানোর সহজ উপায় হলো এক দিকে প্রবাহ — main থেকে branch-এ আনা চলবে, কিন্তু branch থেকে main-এ শুধু pull request-এর শেষে, একবার। Rebase-ভিত্তিক কাজের ধারাতেও criss-cross কম হয়। Virtual base-এর ভেতরের খুঁটিনাটি পর্ব ৫৪-এ।

মূল উপলব্ধি: DAG-এ LCA অনন্য নয়। Git একাধিক base-কে মিলিয়ে একটি virtual base বানিয়ে সমস্যাটি সামলায় — আর দলের পরিচ্ছন্ন merge-এর অভ্যাস সমস্যাটি কমায়।

প্র ০৪Semantic conflict যদি Git দেখতেই না পায়, তাহলে দলগুলো ইতিহাসে কীভাবে এর সমাধান খুঁজেছে?

সমস্যাটি পুরোনো। ১৯৯০-এর দশকে অনেক দল মাসের পর মাস আলাদা কাজ করত, তারপর project-এর শেষে একসাথে মেলাত — এই পর্যায়কে মানুষ ঠাট্টা করে “integration hell” বলত। প্রতিটি অংশ নিজে কাজ করত, কিন্তু একসাথে নয়: interface-এর ভিন্ন ধারণা, নাম বদল, একই data-র ভিন্ন অর্থ। এর অনেকটাই আজকের ভাষায় semantic conflict — লেখায় নয়, অর্থে বিরোধ।

উত্তর এসেছিল প্রক্রিয়া থেকে, টুল থেকে নয়। Kent Beck-এর Extreme Programming (১৯৯০-এর দশকের শেষ) “continuous integration”-কে একটি মূল চর্চা বানায়: প্রত্যেকে দিনে কয়েকবার মূল কোডে মেলান, প্রতিবার পুরো test চলে। Martin Fowler-এর “Continuous Integration” প্রবন্ধ (প্রথম ২০০০ সালে, পরে কয়েকবার হালনাগাদ) এর যুক্তি দেয়: মেলানোর ব্যবধান যত ছোট, প্রতিবারের বিরোধ তত ছোট আর সহজে খুঁজে পাওয়া যায়। Fowler তাঁর লেখায় semantic conflict নিয়ে আলাদা করে আলোচনাও করেছেন, আর বলেছেন এর বিরুদ্ধে প্রধান প্রতিরক্ষা self-testing code.

বড় open-source project-ও একই পথে গেছে। Linux kernel-এ ২০০৮ সাল থেকে “linux-next” নামে একটি tree প্রতিদিন বিভিন্ন subsystem-এর পরবর্তী কাজ একসাথে merge করে build করে — যাতে conflict আর build-ভাঙা মূল merge-এর আগেই ধরা পড়ে। অনেক কোম্পানি পরে “merge queue” বা “commit queue” চালু করে, যেখানে প্রতিটি পরিবর্তন সর্বশেষ মূল branch-এর সাথে মেলানো অবস্থায় test হয়, তবেই ঢোকে।

অংশ ৮-এর উদাহরণে দুই developer-ই ঠিক কাজ করেছিলেন, দুজনের test-ই সবুজ ছিল। ভুলটি কারও একার নয় — দুই পরিবর্তনের মিলনে। তাই সমাধানও ব্যক্তির নয়, ব্যবস্থার: merge-এর ফলকে test করা, typed ভাষা বা static analysis দিয়ে “অস্তিত্বহীন নাম” ধরা, আর বড় rename-এর মতো কাজ দ্রুত মেলানো। পর্ব ৫৮-এ CI pipeline-এর নকশা বিস্তারিত।

মূল উপলব্ধি: Git লেখা মেলায়, অর্থ নয়। অর্থের বিরোধ ধরে শুধু স্বয়ংক্রিয় test আর build — আর সেটা কার্যকর হয় যখন মেলানো হয় ঘন ঘন, ছোট ছোট ধাপে।

প্র ০৫Conflict সমাধানের দায়িত্ব কার? অন্যের লেখা কোডের conflict নিজে সমাধান করা কি ঠিক — আর “evil merge” কী?

Conflict সাধারণত সমাধান করেন যিনি merge করছেন। কিন্তু conflict-এর এক পক্ষ প্রায়ই অন্য কারও কাজ — যাঁর উদ্দেশ্য আপনি পুরো জানেন না। আমাদের ডেলিভারি ফি-র উদাহরণে ৭০ না ৫০, তা developer-এর সিদ্ধান্ত নয়, ব্যবসার। এমন ক্ষেত্রে সবচেয়ে নিরাপদ পথ git log --merge দিয়ে লেখকদের খুঁজে তাঁদের সাথে কথা বলা। কোডের ক্ষেত্রেও তাই: অন্যের function-এ কী বদলেছে, কেন বদলেছে — না বুঝে “আমারটা রাখি” বেছে নিলে তাঁর ঠিক করা bug আবার ফিরে আসতে পারে।

অনেক দল এজন্য নিয়ম করে: যিনি pull request খুলেছেন, তিনিই নিজের branch-এ main merge (বা rebase) করে conflict সমাধান করবেন, কারণ নিজের পরিবর্তনের অর্থ তিনিই সবচেয়ে ভালো জানেন। অন্য পক্ষের কাজ নিয়ে সন্দেহ হলে review-তে তাঁকে যুক্ত করেন (পর্ব ১৬ আর ৪৪)। কিছু platform-এর web interface থেকেই ছোট conflict সমাধান করা যায়; সুবিধাজনক, কিন্তু সেখানে test চালানো যায় না — তাই CI-এর ফল দেখা আরও জরুরি।

Evil merge: Git সম্প্রদায়ে এই নাম দেওয়া হয় এমন merge commit-কে, যা দুই parent-এর কোনোটিতে নেই এমন পরিবর্তন নিয়ে আসে — যেমন conflict সমাধানের সুযোগে পাশের কোনো ফাইলে একটি “ছোট ঠিক” ঢুকিয়ে দেওয়া। কখনো এটা অপরিহার্য (semantic conflict সারাতে, যেমন fee()-কে deliveryFee() বানানো), কিন্তু বিপদ হলো: সাধারণ git log -p merge commit-এর diff দেখায়ই না, আর review-তে মানুষ সাধারণত branch-এর commit-গুলো পড়েন, merge commit নয়। git show-এর combined diff এমন বদল দেখায় ঠিকই (কারণ সেটি দুই parent থেকেই আলাদা), কিন্তু কেউ আলাদা করে না খুললে বদলটি সহজে চোখ এড়ায়।

ভালো অভ্যাস: merge commit-এ শুধু conflict সমাধান রাখুন; বাড়তি পরিবর্তন আলাদা commit-এ। semantic conflict সারাতে merge-এর ভেতরে বদল করতেই হলে commit বার্তায় স্পষ্ট লিখুন। আর merge-এর পরে git show --stat বা git diff HEAD^1 HEAD দিয়ে দেখুন merge আসলে কী বদলাল।

মূল উপলব্ধি: Conflict সমাধান একটি নকশা-সিদ্ধান্ত, আর সিদ্ধান্তে দুই পক্ষের লেখককে যুক্ত করা উচিত। Merge commit-কে ছোট আর স্বচ্ছ রাখুন, যাতে পরে কেউ বুঝতে পারেন কী আর কেন মেলানো হয়েছিল।

অনুশীলন

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

  1. Merge base আর command-এর ক্রম: একটি repository-র commit graph (তীর parent-এর দিকে): A ← B ← C ← D, যেখানে main আছে D-তে; B ← E ← F, যেখানে feature আছে F-এ; D ← G, যেখানে hotfix আছে G-তে। (ক) main আর feature-এর merge base কোনটি? main আর hotfix-এর? (খ) main-এ থেকে আগে git merge hotfix, তারপর git merge feature দিলে প্রতিটি কী ধরনের merge হবে, আর নতুন কোনো commit-এর parent কী কী? (গ) শেষে git log --oneline আর git log --oneline --first-parent কয়টি করে commit দেখাবে? (ঘ) hotfix-এর merge-এ যদি দলের নিয়মে merge commit রাখতেই হয়, কোন command?

    (ক) main (D)-এর ancestor: D, C, B, A; feature (F)-এর: F, E, B, A. Common: B, A; best = B. main আর hotfix: G-এর ancestor-এর মধ্যে D আছে, তাই merge base = D = main নিজে।

    (খ) git merge hotfix: base = D = ours → fast-forward, main সরে G-তে, নতুন commit নেই। এরপর git merge feature: এখন ours = G, theirs = F, base = B (G-র ancestor G, D, C, B, A) — কেউ কারও ancestor নয় → three-way merge, নতুন merge commit M, প্রথম parent G, দ্বিতীয় parent F (conflict না হলে)।

    (গ) git log --oneline M থেকে পৌঁছানো সব commit দেখায়: M, G, D, C, B, A, F, E = ৮টি. --first-parent: M → G → D → C → B → A = ৬টি (E আর F বাদ, কারণ তারা M-এর দ্বিতীয় parent-এর পথে)।

    (ঘ) git switch main তারপর git merge --no-ff hotfix. তখন নতুন merge commit-এর parent হবে D আর G, আর পরের feature merge-এর প্রথম parent হবে সেই merge commit; --first-parent তখন ৬টি commit দেখাবে (দুই merge, D, C, B, A).

  2. আউটপুট অনুমান করুন: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের result.txt, main-এ base commit:
    roll = 1
    name = Karim
    bangla = 78
    english = 81
    math = 90
    base থেকে তিনটি branch: teacher-a শুধু bangla = 80 করেছে, teacher-b শুধু math = 92, teacher-c শুধু english = 85. main-এ কেউ কিছু করেননি। (ক) git merge teacher-a তারপর git merge teacher-b — দুটি command কী ছাপবে (প্রথম দুই লাইন), আর ফাইল কেমন হবে? (খ) শুরুর অবস্থা থেকে git merge teacher-a তারপর git merge teacher-c হলে?

    (ক) প্রথম merge-এ main = base, যা teacher-a-র ancestor → fast-forward. দ্বিতীয়টিতে main (এখন teacher-a) আর teacher-b আলাদা পথে → three-way; বদল লাইন ৩ আর লাইন ৫-এ, মাঝে লাইন ৪ অপরিবর্তিত → পরিষ্কার। Git ২.৫৩-এর আসল আউটপুট:

    $ git merge teacher-a
    Updating 9f01096..be99d94
    Fast-forward
     result.txt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    $ git merge teacher-b
    Auto-merging result.txt
    Merge made by the 'ort' strategy.
     result.txt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    $ cat result.txt
    roll = 1
    name = Karim
    bangla = 80
    english = 81
    math = 92

    (খ) এবারও প্রথমটি fast-forward. কিন্তু teacher-a লাইন ৩ আর teacher-c লাইন ৪ বদলেছে — পাশাপাশি লাইন, মাঝে কোনো অপরিবর্তিত লাইন নেই → conflict, আর marker-এর ভেতরে দুটি লাইনই:

    $ git merge teacher-a
    Updating 9f01096..be99d94
    Fast-forward
     result.txt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    $ git merge teacher-c
    Auto-merging result.txt
    CONFLICT (content): Merge conflict in result.txt
    Automatic merge failed; fix conflicts and then commit the result.
    $ cat result.txt
    roll = 1
    name = Karim
    <<<<<<< HEAD
    bangla = 80
    english = 81
    =======
    bangla = 78
    english = 85
    >>>>>>> teacher-c
    math = 90

    লক্ষ করুন, =======-এর পরের অংশে bangla = 78 — theirs (teacher-c) বাংলা নম্বর ছোঁয়নি, তাই base-এর মানই তার সংস্করণে। সঠিক সমাধান দুই বদলই রাখা: bangla = 80 আর english = 85. শুধু “theirs” বেছে নিলে teacher-a-র ৮০ নীরবে হারিয়ে যেত।

  3. এই conflict সমাধান করুন: একটি চায়ের দোকানের মেনু-ফাইল menu.txt-এ git merge theirs-এর পরে zdiff3 style-এ এই অবস্থা। Commit বার্তা: ours — “Our prices: tea 12 from January”, theirs — “Theirs prices: tea 15 because milk price rose”. মালিক জানালেন, দুধের দাম সত্যিই বেড়েছে, তবে জানুয়ারিতে চায়ের দাম ১৩ টাকা হবে। ফাইলটি কেমন হবে, আর কোন command-গুলো দিয়ে merge শেষ করবেন?
    title = Menu
    rice = 55
    note = prices from Jan
    <<<<<<< HEAD
    tea = 12
    ||||||| 6d28d02
    rice = 50
    tea = 10
    =======
    tea = 15
    >>>>>>> theirs
    end

    পড়া: base-এ ভাত ৫০, চা ১০. দুই পক্ষই ভাত ৫৫ আর নোট লাইন একইভাবে করেছে — zdiff3 সেগুলো marker-এর বাইরে রেখেছে, তাই ওগুলো নিয়ে ভাবার দরকার নেই। আসল বিরোধ শুধু চা: ours ১২, theirs ১৫. Base অংশে rice = 50 দেখে বিভ্রান্ত হবেন না — base অংশ মুছে ফেলার জন্য, রাখার জন্য নয়।

    সমাধানের পরে ফাইল (মালিকের সিদ্ধান্ত অনুযায়ী নতুন মান — দুই পক্ষের কোনোটিই নয়):

    title = Menu
    rice = 55
    note = prices from Jan
    tea = 13
    end

    Command: git diff --check (marker রয়ে গেল কি না), git add menu.txt, তারপর git commit (বা git merge --continue) — বার্তায় লিখুন “tea = 13 as agreed with the owner (milk price)”. শেষে git show --stat. এখানে git checkout --ours বা --theirs ভুল হতো, কারণ উত্তর তৃতীয় একটি মান।

  4. নীতির সমালোচনা: একটি কাল্পনিক অনলাইন বাজার অ্যাপের দলের লিখিত নিয়ম: “প্রত্যেকে নিজের feature branch-এ এক মাস কাজ করবেন। মাসের শেষ দিনে সবাই একসাথে main-এ merge করবেন। Conflict হলে সময় বাঁচাতে git merge -X ours ব্যবহার হবে। CI শুধু main-এ রাতে একবার চলবে।” কী কী সমস্যা দেখছেন, আর কীভাবে নতুন করে লিখবেন?
    • এক মাসের branch: অংশ ৯-এর মডেলে বদলানো লাইন বাড়লে conflict-এর সম্ভাবনা দ্রুত বাড়ে; এক মাসের কাজ মানে প্রায় নিশ্চিত, বড় conflict. নতুন নিয়ম: ছোট pull request, দিনে অন্তত একবার main-এর সাথে মেলানো, অসমাপ্ত কাজ feature flag-এর আড়ালে (পর্ব ১৭)।
    • সবাই একই দিনে merge: শেষ দিনে সবার merge একসাথে — “integration hell”-এর ক্লাসিক রূপ, আর কোন merge কোন bug আনল বোঝা কঠিন। নতুন নিয়ম: কাজ শেষ হলেই merge, একটি করে।
    • -X ours: এই option conflict-ওয়ালা অংশে নীরবে ours বেছে নেয় — অন্য পক্ষের কাজ কোনো সতর্কবার্তা ছাড়াই হারায় (পরিষ্কার অংশ অবশ্য মেলে)। এটা সমাধান নয়, লুকানো। নতুন নিয়ম: conflict হাতে সমাধান, দরকার হলে দুই লেখকের আলোচনা; merge.conflictStyle zdiff3 চালু।
    • রাতে একবার CI, শুধু main-এ: semantic conflict merge-এর পরে, পরের দিন ধরা পড়ে, আর ততক্ষণে আরও কাজ জমে। নতুন নিয়ম: প্রতিটি push আর pull request-এ CI, merge-এর আগে সর্বশেষ main-এর সাথে test; লাল হলে merge বন্ধ (পর্ব ৫৮)।
    • যা নেই: formatting-এর নিয়ম (একই formatter, .gitattributes-এ line ending), merge commit রাখা হবে কি না (--no-ff বনাম squash), আর কে conflict সমাধান করবেন — সবই লিখে রাখা দরকার।

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

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

ক্রমএকটি merge conflict সমাধানের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. git merge feature — CONFLICT বার্তা আসে
  2. git status দিয়ে unmerged ফাইলের তালিকা দেখা
  3. ফাইল খুলে marker পড়া, দরকার হলে লেখকদের সাথে কথা বলা
  4. ফাইল ঠিক করে সব marker মুছে ফেলা
  5. git add দিয়ে ফাইলটি resolved চিহ্নিত করা
  6. git commit বা git merge --continue দিয়ে merge commit তৈরি

একটি repository-তে main আছে commit B-তে। B থেকে খোলা feature branch-এ দুটি নতুন commit C আর D আছে, আর main-এ B-এর পরে কোনো commit হয়নি। main-এ থেকে git merge feature দিলে কী হবে?

  • দুই parent-ওয়ালা একটি নতুন merge commit তৈরি হবে
  • main pointer D-তে সরে যাবে, কোনো নতুন commit তৈরি হবে না
  • “Already up to date” বলে কিছুই করবে না
  • Conflict হবে, কারণ দুটি নতুন commit আছে

একটি ফাইলের লাইন merge base-এ limit = 1000, ours (HEAD)-এ limit = 1000, আর theirs-এ limit = 800. Three-way merge-এর ফলে লাইনটি কী হবে?

  • limit = 1000, কারণ ours-কে সবসময় অগ্রাধিকার দেওয়া হয়
  • Conflict, কারণ দুই tip-এ দুটি ভিন্ন মান
  • limit = 800, কারণ শুধু theirs base থেকে বদলেছে
  • দুটি লাইনই পাশাপাশি থাকবে

main-এ থাকা অবস্থায় git merge hotfix দেওয়ার পরে একটি ফাইলে এই অংশ দেখা গেল:
<<<<<<< HEAD / fee = 70 / ======= / fee = 50 / >>>>>>> hotfix.
fee = 70 লাইনটি কোথা থেকে এসেছে?

  • বর্তমান branch main (ours) থেকে
  • আনা branch hotfix (theirs) থেকে
  • Merge base থেকে
  • Git দুই মানের গড় হিসেবে বানিয়েছে

একটি merge commit-এর git cat-file -p আউটপুটে দুটি parent লাইন আছে। প্রথম parent লাইনটি সাধারণত কী নির্দেশ করে?

  • দুই branch-এর merge base
  • যে branch আনা হয়েছিল তার tip
  • দুই parent-এর মধ্যে যেটি আগে তৈরি হয়েছিল
  • Merge-এর সময় যে branch-এ ছিলেন, তার আগের tip

একটি branch-এ fee() function-এর নাম বদলে deliveryFee() করা হলো আর সব পুরোনো call বদলানো হলো। আরেকটি branch-এ ফাইলের অন্য জায়গায় fee() ডাকে এমন একটি নতুন function যোগ হলো। দুই branch merge করলে সবচেয়ে সম্ভাব্য ফল কী?

  • Git conflict marker দিয়ে থামবে
  • Merge পরিষ্কারভাবে হবে, কিন্তু প্রোগ্রাম চালালে বা test-এ ভাঙবে
  • Git নিজেই নতুন function-এ নাম বদলে deliveryFee() করে দেবে
  • Fast-forward হবে, কারণ conflict নেই

Commit না করা কোনো বদল ছাড়া একটি পরিষ্কার working tree থেকে git merge feature দেওয়ার পরে তিনটি ফাইলে conflict হলো। আপনি ঠিক করলেন আজ merge করবেন না। কোন command সবচেয়ে উপযুক্ত?

  • git reset --hard
  • git add .
  • git merge --abort
  • git merge --continue

শব্দকোষ

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

মনে রাখুন

  1. git merge X বর্তমান branch-কে বদলায়; ours = HEAD, theirs = X.
  2. ours যদি theirs-এর ancestor হয় → fast-forward (শুধু pointer সরে); --ff-only শুধু এটাই করে, --no-ff তবুও merge commit বানায়।
  3. দুই পথ আলাদা হলে three-way merge: merge base (best common ancestor) থেকে দুই দিকের বদল hunk ধরে মেলানো; ফল দুই-parent-ওয়ালা merge commit, প্রথম parent = আগের HEAD.
  4. Conflict = একই বা পাশাপাশি অংশ দুই পক্ষ ভিন্নভাবে বদলেছে (বা modify/delete, add/add). ফাইল ঠিক করুন → git add → git commit; ফিরতে git merge --abort; base দেখতে merge.conflictStyle zdiff3.
  5. পরিষ্কার merge মানেই ঠিক প্রোগ্রাম নয় — semantic conflict ধরে test আর CI. ছোট branch আর ঘন ঘন মেলানো conflict কমায়।

পরের পর্বে: মডিউল ২ এখানেই শেষ — নিজের কম্পিউটারে Git দিয়ে commit, history, ভুল শোধরানো, branch আর merge আপনার হাতে। পর্ব ১৪ থেকে শুরু দলের সাথে Git: remote repository, clone, fetch, pull আর push. সেখানে দেখবেন git pull আসলে fetch আর merge-এর জোড়া — আর আজকের fast-forward আর three-way merge প্রতিদিন সেখানেই ঘটে।

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

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