পাঠ ৩৬ · ৫৮-এর মধ্যে · মডিউল ৮
Home / Courses / Software Engineering Principles & Git / মার্জ কনফ্লিক্ট ও সমাধান

মার্জ কনফ্লিক্ট ও সমাধান

Merge conflicts and resolution
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • মার্জ কনফ্লিক্ট কেন ও ঠিক কখন ঘটে — L35-এর merge base তুলনার সাথে সরাসরি সম্পর্ক
  • real, standard কনফ্লিক্ট মার্কার ফরম্যাট এবং তা কীভাবে পড়তে হয়
  • কনফ্লিক্ট সমাধানের ওয়ার্কফ্লো — এডিট করা, মার্কার সরানো, git add, কমিট সম্পন্ন করা
  • কোড দিয়ে একদম শুরু থেকে একটি জেনুইন লাইন-বাই-লাইন three-way-merge অ্যালগরিদম যাচাই

১ · কনফ্লিক্ট কেন ঘটে

merge conflictMerge Conflictthree-way merge-এ ঘটে যখন একই লাইন merge base-এর তুলনায় দুই branch-ই ভিন্ন কন্টেন্টে বদলে ফেলেছে -- git স্বয়ংক্রিয়ভাবে সিদ্ধান্ত নিতে না পেরে থেমে যায়। ঘটে L35-এর three-way merge-এর সময়, যখন কোনো ফাইলের একই অংশ দুই বিচ্যুত branch-ই merge base-এর তুলনায় ভিন্নভাবে বদলে ফেলেছে। git প্রতিটি branch-এর পরিবর্তন merge base-এর সাথে তুলনা করে — যদি শুধু একপাশ কোনো নির্দিষ্ট লাইন বদলায়, git নির্দ্বিধায় সেই পাশের ভার্সন গ্রহণ করে, কোনো কনফ্লিক্ট ছাড়াই। কিন্তু যদি উভয় পাশই একই লাইনকে ভিন্ন ভিন্ন কন্টেন্টে বদলায়, কোনো দ্ব্যর্থহীন উপায়ে সেগুলো স্বয়ংক্রিয়ভাবে একত্রিত করার নেই — তাই git মার্জ প্রক্রিয়া থামিয়ে দেয় এবং ম্যানুয়াল সমাধান দাবি করে।

কেউ বদলায়নি
base-এর লাইনই থেকে যায়, কোনো পরিবর্তন নেই।
শুধু একপাশ বদলেছে
সেই পাশের ভার্সন স্বয়ংক্রিয়ভাবে গ্রহণ করা হয় — কোনো কনফ্লিক্ট নেই।
উভয়ই একই কন্টেন্টে বদলেছে
যদিও দুই পাশই বদলেছে, ফলাফল একই — নিরাপদে অটো-মার্জ হয়, কনফ্লিক্ট নেই।
দুইপাশ ভিন্নভাবে বদলেছে
দ্ব্যর্থহীন সমাধান নেই — কনফ্লিক্ট মার্কার বসিয়ে ম্যানুয়াল সিদ্ধান্তের জন্য অপেক্ষা।

২ · কনফ্লিক্ট মার্কার — real, standard ফরম্যাট

conflict markersConflict Markersgit কনফ্লিক্টিং লাইনের চারপাশে বসিয়ে দেওয়া আক্ষরিক টেক্সট মার্কার -- <<<<<<< HEAD, =======, >>>>>>> branch-name -- দুই ভার্সনকে পাশাপাশি দেখায়। কনফ্লিক্ট হলে git সরাসরি ফাইলের ভেতরেই আক্ষরিক টেক্সট বসিয়ে দেয়: <<<<<<< HEAD, তারপর বর্তমান branch-এর ভার্সন, তারপর =======, তারপর অন্য branch-এর ভার্সন, শেষে >>>>>>> branch-name। ডেভেলপারকে ম্যানুয়ালি ফাইলটি এডিট করে চূড়ান্ত, সঠিক কন্টেন্ট রাখতে হয় (এক পাশ, অন্য পাশ, দুটোর সমন্বয়, বা একেবারে নতুন কিছু — যেটাই সঠিক হয়), মার্কারগুলো সরিয়ে ফেলতে হয়, তারপর git add দিয়ে সমাধান-করা ফাইলটি স্টেজ করে merge commit সম্পন্ন করতে হয়।

মনে রাখার মতো একটি সূক্ষ্ম পয়েন্ট

দুই branch যদি একই লাইনকে একই নতুন কন্টেন্টে বদলায় (identical পরিবর্তন), সেটি কনফ্লিক্ট নয় — যদিও দুই পাশই বদলেছে, ফলাফল অভিন্ন বলে দ্ব্যর্থহীনভাবে অটো-মার্জ করা যায়। কনফ্লিক্টের আসল শর্ত হলো দুই পাশের ফলাফল ভিন্ন হওয়া, শুধু "দুই পাশই বদলেছে" যথেষ্ট নয়।

৩ · কোড দিয়ে যাচাই

নিচের কোড সেলটি বাস্তব git বাইনারি চালায় না — এই ব্রাউজার-স্যান্ডবক্সে কোনো real git ইনস্টল নেই — বরং এটি real git-এর লাইন-বাই-লাইন three-way merge অ্যালগরিদম যেভাবে কাজ করে ঠিক সেভাবেই একদম শুরু থেকে বিশ্বস্তভাবে সিমুলেট করছে। এটিই L35-এর git_merge-এর সরলীকৃত, ফাইল-লেভেল লজিকের অনুপস্থিত অংশ — যা প্রকৃত লাইন-লেভেল তুলনা করে জেনুইন কনফ্লিক্ট শনাক্ত করে।

Python
# লাইন-বাই-লাইন three-way merge -- real git-এর কনফ্লিক্ট-শনাক্তকরণ অ্যালগরিদমের একটি
# বিশ্বস্ত সিমুলেশন, একদম শুরু থেকে লেখা -- এই স্যান্ডবক্সে real git বাইনারি নেই।

def three_way_merge(base_lines, a_lines, b_lines, label_a="HEAD", label_b="feature-branch"):
    merged = []
    conflict_count = 0
    n = max(len(base_lines), len(a_lines), len(b_lines))
    for i in range(n):
        base_line = base_lines[i] if i < len(base_lines) else ""
        a_line = a_lines[i] if i < len(a_lines) else ""
        b_line = b_lines[i] if i < len(b_lines) else ""

        a_changed = (a_line != base_line)
        b_changed = (b_line != base_line)

        if a_changed and b_changed and a_line != b_line:
            # উভয় পাশ ভিন্নভাবে বদলেছে -- দ্ব্যর্থহীন সমাধান নেই, conflict marker বসানো হচ্ছে
            conflict_count += 1
            merged.append(f"<<<<<<< {label_a}")
            merged.append(a_line)
            merged.append("=======")
            merged.append(b_line)
            merged.append(f">>>>>>> {label_b}")
        elif a_changed:
            merged.append(a_line)   # শুধু A বদলেছে -- A-এর ভার্সন অটো-গ্রহণ
        elif b_changed:
            merged.append(b_line)   # শুধু B বদলেছে -- B-এর ভার্সন অটো-গ্রহণ
        else:
            merged.append(base_line)  # কেউ বদলায়নি (অথবা দুইপাশই একই বদল করেছে -> a_line == b_line)
    return merged, conflict_count


# ---- ডেমো: একটি README-এর ৫ লাইন, main ও feature-b দুটোই বিচ্যুতভাবে বদলেছে ----
base_lines = [
    "প্রজেক্টের নাম: Bookstore App",
    "ভূমিকা: এটি একটি অনলাইন বই বিক্রির সিস্টেম।",
    "স্ট্যাটাস: ডেভেলপমেন্ট চলছে",
    "লেখক তালিকা: এখনো যোগ করা হয়নি",
    "লাইসেন্স: MIT",
]

main_lines = [
    "প্রজেক্টের নাম: Bookstore App",                              # অপরিবর্তিত
    "ভূমিকা: এটি একটি অনলাইন বই বিক্রির সিস্টেম, মোবাইল অ্যাপসহ।",   # শুধু main বদলেছে
    "স্ট্যাটাস: টেস্টিং পর্যায়ে (main)",                            # main ও feature-b দুজনেই ভিন্নভাবে বদলেছে
    "লেখক তালিকা: এখনো যোগ করা হয়নি",                              # অপরিবর্তিত (main-এ)
    "লাইসেন্স: Apache-2.0 (main)",                                # main ও feature-b দুজনেই ভিন্নভাবে বদলেছে
]

feature_b_lines = [
    "প্রজেক্টের নাম: Bookstore App",                              # অপরিবর্তিত
    "ভূমিকা: এটি একটি অনলাইন বই বিক্রির সিস্টেম।",                  # অপরিবর্তিত (feature-b-তে)
    "স্ট্যাটাস: প্রোডাকশনে রিলিজের প্রস্তুতি (feature-b)",           # main ও feature-b দুজনেই ভিন্নভাবে বদলেছে
    "লেখক তালিকা: রাহুল, সুমাইয়া, করিম যোগ করা হয়েছে",              # শুধু feature-b বদলেছে
    "লাইসেন্স: GPL-3.0 (feature-b)",                              # main ও feature-b দুজনেই ভিন্নভাবে বদলেছে
]

merged_result, conflicts = three_way_merge(
    base_lines, main_lines, feature_b_lines, label_a="main", label_b="feature-b"
)

print(f"মোট conflict সংখ্যা: {conflicts}\n")
print("=== মার্জ করা ফাইলের কনটেন্ট (conflict marker-সহ) ===")
for line in merged_result:
    print(line)

    
হাতে যাচাই করলে দেখা যায়: লাইন ০ কেউ বদলায়নি (base থেকেই থাকে)। লাইন ১ শুধু main বদলেছে, তাই স্বয়ংক্রিয়ভাবে main-এর ভার্সন গ্রহণ হয় (কনফ্লিক্ট ছাড়াই)। লাইন ২ ও লাইন ৪ — দুটোতেই main ও feature-b দুজনেই ভিন্ন কন্টেন্টে বদলেছে, তাই এই দুটো লাইনেই genuine <<<<<<< main / ======= / >>>>>>> feature-b মার্কার বসে — conflicts-এর মান হয় ঠিক ২। লাইন ৩ শুধু feature-b বদলেছে, তাই সেই ভার্সন স্বয়ংক্রিয়ভাবে গ্রহণ হয়, কোনো মার্কার ছাড়াই।
মূল কথা · Key takeaway

মার্জ কনফ্লিক্ট মানে git "ভেঙে গেছে" নয় — এটি git-এর একটি সৎ স্বীকারোক্তি যে এই নির্দিষ্ট সিদ্ধান্তটি নেওয়ার জন্য মানুষের বিচার-বুদ্ধি দরকার, যা কোনো অ্যালগরিদম নিরাপদে অনুমান করতে পারে না। বাকি সবটুকু — একই লাইনে দুইপাশের ভিন্ন পরিবর্তনের বাইরের প্রতিটি লাইন — git ইতিমধ্যেই নির্ভুলভাবে নিজে থেকে মিলিয়ে দিয়েছে। M8-এর পরবর্তী পাঠগুলোতে (rebase, merge-vs-rebase) এই একই মৌলিক তুলনা-লজিকই বারবার ফিরে আসবে।

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

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

প্র ০১ কেন "শুধু একপাশ বদলেছে" হলে কনফ্লিক্ট হয় না, কিন্তু "দুইপাশই ভিন্নভাবে বদলেছে" হলে হয় — এই পার্থক্যের পেছনের যুক্তি কী?

যদি শুধু একপাশ বদলায়, অন্য পাশ base-এর সাথেই একমত (কিছু বদলায়নি) — তাই যে পাশ বদলেছে, সেই পাশের ইচ্ছাটাই একমাত্র "নতুন তথ্য", কোনো দ্বন্দ্ব নেই সেখানে গ্রহণ করার জন্য। কিন্তু দুইপাশই যদি ভিন্নভাবে বদলায়, তার মানে দুই ডেভেলপারই স্বাধীনভাবে সিদ্ধান্ত নিয়েছেন এই লাইনটা কেমন হওয়া উচিত, আর তাদের সিদ্ধান্ত পরস্পরবিরোধী — কোনো একটাকে "ডিফল্ট" হিসেবে বেছে নেওয়া ভুল হতে পারে, তাই একজন মানুষকেই সিদ্ধান্ত নিতে হয়।

প্র ০২ যদি দুই branch-ই ঠিক একই লাইনকে ঠিক একই নতুন কন্টেন্টে বদলায় (identical পরিবর্তন), আমাদের ফাংশন কি কনফ্লিক্ট মার্ক করবে? কেন বা কেন না?

না, মার্ক করবে না। শর্তটি হলো a_changed and b_changed and a_line != b_line — যদি দুই পাশের নতুন কন্টেন্ট identical হয়, a_line != b_line মিথ্যা হয়, তাই প্রথম if ব্লকে ঢোকে না। এরপর elif a_changed সত্য হওয়ায় a_line যোগ হয় — যা b_line-এর সমানই, তাই ফলাফল সঠিক থাকে, কোনো মার্কার ছাড়াই। এটি precisely সেই সূক্ষ্ম পয়েন্টটি যাচাই করে যা এই পাঠের ২য় সেকশনে উল্লেখ করা হয়েছিল।

প্র ০৩ বাস্তব git-এ কনফ্লিক্ট রেজলিউশনের পর ঠিক কোন কমান্ডগুলো (git add + merge commit সম্পন্ন) চালাতে হয়, এবং কেন প্রতিটি ধাপ দরকার?

প্রথমে ফাইলটি ম্যানুয়ালি এডিট করে সব <<<<<<</=======/>>>>>>> মার্কার সরিয়ে চূড়ান্ত, সঠিক কন্টেন্ট রাখতে হয়। তারপর git add <file> চালিয়ে সমাধান-করা ফাইলটি Staging Area-তে নিয়ে আসতে হয় — এটিই git-কে জানায় "এই ফাইলের কনফ্লিক্ট সমাধান হয়ে গেছে"। শেষে git commit চালালে (আগে থেকেই প্রস্তুত থাকা merge commit মেসেজ দিয়ে) দুই-parent-ওয়ালা merge commit-টি সম্পন্ন হয় — L35-এর git_merge-এ যা সরাসরি একধাপেই সম্পন্ন হতো, বাস্তব কনফ্লিক্ট থাকলে এই তিন ধাপে ভাগ হয়ে যায়।

অনুশীলন

  1. পরীক্ষা করুন: কোড সেলে feature_b_lines-এর index ২-এর লাইনটিকে main_lines-এর index ২-এর লাইনের মতোই identical করে বদলে Run চাপুন — conflicts-এর মান কী হয়?

    conflicts এখন ১ হবে (২ থেকে কমে), কারণ index ২-এর লাইনে এখন a_line == b_line — প্র ০২-এর ব্যাখ্যা অনুযায়ী এটি আর কনফ্লিক্ট হিসেবে গণ্য হবে না, বরং স্বয়ংক্রিয়ভাবে সেই identical ভার্সনে মিলে যাবে। index ৪-এর কনফ্লিক্টটি (লাইসেন্স) অপরিবর্তিত থাকবে, যেহেতু সেটি স্পর্শ করা হয়নি।

  2. চিন্তা করুন: বাস্তব জীবনে কনফ্লিক্ট সমাধান করার সময় "শুধু একপাশ বেছে নেওয়া" ছাড়াও তৃতীয় একটি বাস্তবসম্মত সম্ভাবনা কী থাকতে পারে?

    অনেক সময় সঠিক সমাধান দুই পাশের কোনোটাই হুবহু নয়, বরং দুটোর একটি সমন্বয় বা সম্পূর্ণ নতুন একটি ভার্সন — যেমন আমাদের ডেমোর স্ট্যাটাস-লাইনে main বলছে "টেস্টিং পর্যায়ে" আর feature-b বলছে "প্রোডাকশনে রিলিজের প্রস্তুতি" — বাস্তব সিদ্ধান্ত হয়তো হবে "টেস্টিং সম্পন্ন, প্রোডাকশন রিলিজের প্রস্তুতি চলছে" — দুই দিকের তথ্যই ধরে রেখে একটি নতুন, আরও নির্ভুল বাক্য লেখা, যা কোনো স্বয়ংক্রিয় অ্যালগরিদম নিজে থেকে বানাতে পারে না।

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

আগের পাঠ
L35 · মার্জিং — fast-forward বনাম three-way merge