JSON Toolkit

JSON ফাইলে Git মার্জ কনফ্লিক্ট: merge=union কেন ভাঙা JSON লেখে

সংক্ষেপে: merge=union ভাঙা JSON ঢুকতে দেয় আর তবু সফল বলে। এখানে JSON-এর জন্য কি-সচেতন থ্রি-ওয়ে মার্জ ড্রাইভার, প্লাস কনফ্লিক্ট কমানোর তিন অভ্যাস।

মধ্যবর্তী~7 মিনিট পড়াপ্রকাশ 2026-10-30আপডেট 2026-10-30
JSON ফাইলে Git মার্জ কনফ্লিক্ট মেটানোর কভার

মূল কথা: মার্জ কৌশল নিয়ে তর্কের আগে সিরিয়ালাইজেশন ও কি-ক্রম প্রমিত করুন; merge=union যে প্রতিটি কনফ্লিক্ট বাঁচায় তা পরে ফিরে আসে অনুপস্থিত কমা, ডুপ্লিকেট কি, বা দুইটি স্তূপ করা ডকুমেন্ট হিসেবে, তাই JSON কনফ্লিক্টের জায়গা কি-সচেতন থ্রি-ওয়ে মার্জ ড্রাইভার।

একই সপ্তাহে দুইজন দুটি অনুবাদ কি যোগ করে: একজন tools.diff, অন্যজন tools.schema। পরিবর্তনগুলো অসংশ্লিষ্ট ও সেমান্টিকভাবে সামঞ্জস্যপূর্ণ। Git তবু কনফ্লিক্ট রিপোর্ট করে, আর কনফ্লিক্ট ব্লকে দুই লাইনই থাকে:

<<<<<<< HEAD
    "diff": "তুলনা"
=======
    "schema": "স্কিমা তৈরি"
>>>>>>> feat-b

কারণ সরল। দুই ব্র্যাঞ্চই ক্লোজিং ব্রেসের ঠিক আগে একটি নতুন লাইন ঢুকেছে, আর Git-এর লাইনভিত্তিক থ্রি-ওয়ে মার্জ কেবল অবস্থান দেখে, অর্থ নয়। এর "এই দুটি ভিন্ন কি" ধারণা নেই। তাই কেউ .gitattributes ডকুমেন্টেশন পড়ে একটি লাইন কপি করে:

i18n.json merge=union

কনফ্লিক্ট উধাও। এর দামই এই নিবন্ধের বিষয়।

১. union মার্জ করে না, দুই পক্ষের লাইনই রাখে

merge=union স্পষ্টভাবে ডকুমেন্টেড: টেক্সট ফাইলে থ্রি-ওয়ে ফাইল-স্তরের মার্জ চালাও, কিন্তু কনফ্লিক্ট মার্কার না রেখে দুই সংস্করণ থেকেই লাইন নাও। ডকুমেন্টেশনের পরেই সতর্কতা — পরিণতি না বুঝলে এটি ব্যবহার করবেন না। সেই সতর্কতা নিছক ভরাট লেখা নয়।

Union অবজেক্ট, কি বা অ্যারে বোঝে না। লাইন বোঝে। নিচের প্রতিটি ফল উপরের ঠিক i18n.json দিয়ে স্থানীয় রিপোজিটরিতে পুনরুৎপাদিত।

পরিস্থিতি ডিফল্ট ড্রাইভার merge=union কি-সচেতন ড্রাইভার
দুই পক্ষ আলাদা কি যোগ করে কনফ্লিক্ট, হাতে "মার্জড", কমা অনুপস্থিত, অবৈধ JSON মার্জড, দুই কি রাখা
দুই পক্ষ একই কি বদলায় কনফ্লিক্ট, হাতে "মার্জড", ডুপ্লিকেট কি, শেষটি জেতে এক্সিট কোড ১, কনফ্লিক্ট পাথ রিপোর্ট
এক-লাইন JSON, দুই পক্ষে একটি করে পরিবর্তন কনফ্লিক্ট, পুরো ফাইল বাছাই "মার্জড", দুই ডকুমেন্ট স্তূপ মার্জড, তবে পুনঃফরম্যাট
এক পক্ষ minify মোছে, অন্য পক্ষ parse সম্পাদনা করে কনফ্লিক্ট, হাতে "মার্জড", মোছা কি ফিরে আসে, কমা অনুপস্থিত মার্জড, মোছা থাকে, সম্পাদনা প্রয়োগ

কেবল মাঝের কলামই git merge-কে সফল ফেরত দেয়, আর এর কোনো আউটপুট পার্স হয় না। JSON-এ union-এর অদ্ভুত অংশ এটাই: যত নীরব, তত বিপজ্জনক।

২. ফল এক: অনুপস্থিত কমা, পরিচ্ছন্ন মার্জ হিসেবে রিপোর্ট

একই দুই ব্র্যাঞ্চ, এবার merge=union বসানো:

$ git merge feat-b
Auto-merging i18n.json
Merge made by the 'ort' strategy.
 i18n.json | 1 +
 1 file changed, 1 insertion(+)

কনফ্লিক্ট নেই, মার্জ কমিট তৈরি। ফাইলটি এরকম:

{
  "tools": {
    "parse": "পার্স",
    "minify": "মিনিফাই",
    "diff": "তুলনা"
    "schema": "স্কিমা তৈরি"
  }
}

"diff": "তুলনা"-এর পরে কোনো কমা নেই। Union আমাদের পক্ষের লাইন ও তাদের পক্ষের লাইন রেখেছে, প্রতিটির নিজস্ব পিচ্ছে কমা নিয়ে, আর জোড়ার স্থানে একটি বিভাজক হারিয়েছে।

পার্স করলে:

$ python3 -c "import json;json.load(open('i18n.json'))"
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 10 column 5

এটাই সবচেয়ে খারাপ অংশ: Git মার্জকে সফল ধরে, CI মার্জ ধাপ পাস করে, আর কোড রিভিউ "১ ইনসার্শন"-এর সবুজ ডিফ দেখে। ব্যর্থতা অপেক্ষা করে রানটাইমের প্রথম JSON.parse পর্যন্ত, ততক্ষণে মার্জ অনেক পেছনে।

৩. ফল দুই: নীরবে ডেটা ফেলে দেওয়া ডুপ্লিকেট কি

দ্বিতীয় পরীক্ষায় ভিন্ন আকৃতি: দুই পক্ষ একই কি বদলায়, একটি "পার্স / ফরম্যাট", অন্যটি "পার্সার"। Union-এর পর:

{
  "tools": {
    "parse": "পার্স / ফরম্যাট",
    "parse": "পার্সার",
    "minify": "মিনিফাই"
  }
}

এবার JSON বৈধ আর JSON.parse কিছু তোলে না। কিন্তু parse দুইবার আসে। অবজেক্ট লিটেরাল সেমান্টিক্স অনুযায়ী পরের এন্ট্রি জেতে, তাই চূড়ান্ত মান "পার্সার" — অন্য সম্পাদনাটি কোনো লগ লাইন ও এরর ছাড়াই বাদ যায়।

কেবল ডুপ্লিকেট-কি চেক এটি প্রকাশ করে:

duplicate keys detected: ['parse']
JSON.parse succeeded, final parse value = 'পার্সার'

এটি অবৈধ JSON-এর চেয়ে খুঁজতে কঠিন: ফাইল লোড হয়, পেজ রেন্ডার হয়, আর একটি স্ট্রিং নীরবে ফিরে যায়। সপ্তাহ পরে কেউ জিজ্ঞাসা করে কেন একটি অনুবাদ কখনো প্রকাশ পেল না।

৪. ফল তিন: এক-লাইন JSON দুই ডকুমেন্ট হয়ে যায়

কিছু কনফিগ ফাইল এক লাইনে মিনিফাইড, যেমন বিল্ড আর্টিফ্যাক্ট। Union-এর মার্জ করার লাইন নেই, তাই দুই পক্ষই সম্পূর্ণ লাইন আর ফল সেগুলোকে স্তূপ করে:

{"tools":{"parse":"পার্স","minify":"মিনিফাই","diff":"তুলনা"},"version":1}
{"tools":{"parse":"পার্স","minify":"মিনিফাই"},"version":2}

JSON.parse রিপোর্ট করে Unexpected non-whitespace character after JSON at position 63 — প্রথম ডকুমেন্ট শেষ করে দ্বিতীয়টি পেয়েছে। Git আবার "Merge made" বলে।

মিনিফাইড JSON আদৌ ভার্সন কন্ট্রোলে থাকা উচিত নয়, কিন্তু একবার থাকলে union তাকে দুই অসম্পর্কিত ডকুমেন্টে বদলায়।

৫. ফল চার: মোছা কি ফিরে আসে

শেষ আকৃতিটি সবচেয়ে প্রতারক। এক পক্ষ minify মোছে; অন্য পক্ষ পাশের parse সম্পাদনা করে। ডিফল্ট ড্রাইভার কনফ্লিক্ট রিপোর্ট করে; union দেয়:

{
  "tools": {
    "parse": "পার্স"
    "parse": "পার্সার",
    "minify": "মিনিফাই"
  }
}

একসঙ্গে তিন ত্রুটি: minify পুনরুজ্জীবিত, parse দুইবার, আর প্রথম লাইনে কমা নেই। Git তবু "২ ইনসার্শন" ও সফল মার্জ রিপোর্ট করে।

সাধারণ শিক্ষা: মোছার ক্ষেত্রে "দুই পক্ষের লাইন রাখা" মানে মোছা বাতিল করা। যে দল কিছু সময় JSON-এ union ব্যবহার করেছে তাদের ইতিহাসে "আমরা ওটা মুছেছিলাম" সেটিংস চুপচাপ বেঁচে থাকে।

৬. সঠিক পথ: কি দিয়ে থ্রি-ওয়ে মার্জ

Git নির্দিষ্ট পাথে কাস্টম মার্জ ড্রাইভার দিতে দেয়। ড্রাইভার তিনটি ফাইল পাথ পায়: %O সাধারণ পূর্বপুরুষ, %A আমাদের (ফল এখানেই লেখা হয়), %B তাদের। কয়েক ডজন লাইন Node যথেষ্ট:

import { readFileSync, writeFileSync } from 'node:fs';

const [basePath, oursPath, theirsPath] = process.argv.slice(2);
const read = (filePath) => JSON.parse(readFileSync(filePath, 'utf8'));
const [base, ours, theirs] = [read(basePath), read(oursPath), read(theirsPath)];

const sortKeys = (value) => Array.isArray(value)
  ? value.map(sortKeys)
  : value && typeof value === 'object'
    ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]))
    : value;

const same = (left, right) => JSON.stringify(sortKeys(left)) === JSON.stringify(sortKeys(right));
const conflicts = [];

function merge3(baseValue, oursValue, theirsValue, path) {
  if (same(oursValue, theirsValue)) return oursValue;
  if (same(oursValue, baseValue)) return theirsValue;
  if (same(theirsValue, baseValue)) return oursValue;

  const allObjects = [oursValue, theirsValue, baseValue].every(
    (value) => value && typeof value === 'object' && !Array.isArray(value),
  );

  if (allObjects) {
    const keys = new Set([...Object.keys(baseValue ?? {}), ...Object.keys(oursValue), ...Object.keys(theirsValue)]);
    const merged = {};
    const has = (source, key) => Boolean(source) && Object.hasOwn(source, key);
    for (const key of keys) {
      const child = merge3(
        has(baseValue, key) ? baseValue[key] : undefined,
        has(oursValue, key) ? oursValue[key] : undefined,
        has(theirsValue, key) ? theirsValue[key] : undefined,
        path ? `${path}.${key}` : key,
      );
      if (child !== undefined) merged[key] = child;
    }
    return merged;
  }

  conflicts.push({ path: path || '(root)', ours: oursValue, theirs: theirsValue });
  return oursValue;
}

const merged = merge3(base, ours, theirs);

if (conflicts.length > 0) {
  writeFileSync('merge-conflicts.json', `${JSON.stringify({ file: oursPath, conflicts }, null, 2)}\n`, 'utf8');
  console.error(`${conflicts.length} real conflict(s), see merge-conflicts.json`);
  process.exit(1);
}

writeFileSync(oursPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
process.exit(0);

তিন নিয়ম এটিকে টিকিয়ে রাখে: দুই পক্ষে অভিন্ন, সেটাই নিন; এক পক্ষ পূর্বপুরুষের সমান, অন্যটি নিন; দুই পক্ষে আলাদা মানে প্রকৃত কনফ্লিক্ট। বাকিসব সাধারণ অবজেক্টে পুনরাবৃত্তি।

নিবন্ধন দুই ধাপে — .gitattributes পাথ অনুযায়ী ড্রাইভার বাছে, git config ড্রাইভার সংজ্ঞায়িত করে:

git config merge.json-merge.name "JSON key merge"
git config merge.json-merge.driver "node merge-json.mjs %O %A %B"
echo "i18n.json merge=json-merge" >> .gitattributes

প্রথমটি কমিট করা যায় (কোডের সঙ্গে যায় ও CI-তে প্রযোজ্য)। দ্বিতীয়টি লোকাল কনফিগ, README-তে থাকা দরকার — কাস্টম ড্রাইভার সাধারণত এখানেই ভেঙে পড়ে।

"প্রতি পক্ষ আলাদা কি যোগ করে" ক্ষেত্রে ড্রাইভার উৎপন্ন করে:

{
  "tools": {
    "parse": "পার্স",
    "minify": "মিনিফাই",
    "diff": "তুলনা",
    "schema": "স্কিমা তৈরি"
  }
}

এক্সিট কোড ০, বৈধ JSON, দুই কি উপস্থিত।

একটি পার্শ্বপ্রতিক্রিয়া জানা দরকার: ড্রাইভার JSON.stringify(merged, null, 2) লেখে, তাই এটি পুরো ফাইলের ফরম্যাটিং আবার লেখে। ৬৩ বাইটের মিনিফাইড কনফিগ ৭ লাইন ও ১০৫ বাইট হয়ে ফিরে আসে — একই কন্টেন্ট, ভিন্ন আকৃতি। এতে ড্রাইভার কেবল হাতে রক্ষণাবেক্ষণ করা সোর্স ফাইলের জন্য মানায়। মিনিফাইড বা জেনারেট করা JSON রিপোজিটরিতে থাকা উচিত নয়, এই ড্রাইভারও ব্যবহার করা উচিত নয়, নাহলে প্রতিটি মার্জ ফরম্যাটিং শব্দ টেনে আনবে।

৭. কনফ্লিক্টে ফিরে লিখবেন না — এই ফাঁদ দেখতে যত ছোট, তত বড়

দুই পক্ষ একই কি বদলালে ড্রাইভারের কী করা উচিত? স্বাভাবিক পদক্ষেপ "আমাদেরটা ফিরে লেখো, এক্সিট ১, Git কনফ্লিক্ট রিপোর্ট করুক"। পরীক্ষা করে দেখা এটি বিপজ্জনক:

$ git merge conf-b
CONFLICT (content): Merge conflict in i18n.json
Automatic merge failed; fix conflicts and then commit the result.

$ grep -c '^<<<<<<<' i18n.json
0

Git কনফ্লিক্ট রিপোর্ট করে, কিন্তু ফাইলে একটিও কনফ্লিক্ট মার্কার নেই — ড্রাইভার ইতিমধ্যে তার ফল লিখে ফেলেছে। কন্টেন্ট সম্পূর্ণ স্বাভাবিক দেখায়, আমাদের পক্ষের মান নিয়ে। একটি তাড়াহুড়ো git add i18n.json && git commit আর তাদের পরিবর্তন উধাও, ইতিহাসে কেবল "Merge conflict resolved"।

তাই প্রকৃত কনফ্লিক্টে ড্রাইভার %A লেখা উচিত নয়। অমীমাংসিত এন্ট্রি রিপোর্ট ফাইলে রেকর্ড করে এক্সিট ১:

{
  "file": "i18n.json",
  "conflicts": [
    { "path": "tools.parse", "ours": "পার্স / ফরম্যাট", "theirs": "পার্সার" }
  ]
}

মূল্য হলো git status UU-তে থামে, তাই মানুষকে কাজ করতে হয় — আর ডজন ডজন JSON লাইনের ভেতরে <<<<<<< খুঁজতে হয় না। রিপোর্ট ঠিক বলে দেয় কোন পাথ সংঘর্ষ করেছে।

আরও একটি পরীক্ষিত আবিষ্কার: কাস্টম ড্রাইভার নিবন্ধিত হলে git checkout --conflict=merge -- i18n.json কনফ্লিক্ট মার্কার ফিরিয়ে আনতে পারে না, কারণ এটি আবার ড্রাইভার আহ্বান করে আর তবু শূন্য মার্কার দেয়। মার্কার ফিরিয়ে আনতে সাময়িকভাবে git config --unset merge.json-merge.driver করতে হবে, বা git show :1:file, :2:, :3: দিয়ে তিন স্তর টানতে হবে। "আমি সবসময় মার্কার পুনরুৎপাদন করতে পারি" ভরসা করা কেউ ঠিক যখন দরকার তখনই পথ বন্ধ দেখে।

৮. আগেই কনফ্লিক্ট কমাতে তিন অভ্যাস

ড্রাইভার ব্যাকস্টপ, মূল টুল নয়। কনফ্লিক্টের সংখ্যা নিজেই কমানো যায়।

সিরিয়ালাইজেশন প্রমিত করুন। একজন indent=2, আরেকজন indent=4, তৃতীয়জন পিচ্ছে নিউলাইন বাদ দেয়। প্রতিটি সেভ পুরো ফাইল আবার লেখে আর মার্জের কাজ করার কিছু থাকে না। ফরম্যাটার কনফিগ পিন করুন, বা প্রি-কমিট হুকে চালান।

কি-ক্রম স্থিতিশীল রাখুন। কি-ক্রম JSON-এর কাছে অর্থহীন কিন্তু Git-এর কাছে খুব বাস্তব। লেখার আগে কি সাজান (JSON.stringify-এর আগে sort, বা jq -S দিয়ে পাইপ) করলে এক সম্পাদনা এক হাঙ্কে থাকে। এটি দুটি JSON ডকুমেন্ট কীভাবে আলাদা-এ তুলনার আগে নরমালাইজের একই ধারণা — সেখানে ডিফের জন্য, এখানে কমিটের জন্য।

জেনারেট করা ফাইল রিপোজিটরির বাইরে রাখুন। মিনিফাইড JSON, টাইমস্ট্যাম্পযুক্ত বিল্ড ম্যানিফেস্ট, লকফাইল — প্রতিটি রানে বদলায়। সোর্স ভার্সন করুন, আর্টিফ্যাক্ট জেনারেট করুন।

৯. চেকলিস্ট

  • .gitattributes JSON-কে merge=union-এ ম্যাপ করলে git log -p দিয়ে ইতিহাসে ডুপ্লিকেট কি বা সিনট্যাক্স এরর নমুনা দেখুন, তারপর প্রতিস্থাপন করুন;
  • কাস্টম ড্রাইভার নিবন্ধনের পর সবাইকে git config ধাপ চালাতে দিন, বা make setup-এ রাখুন;
  • ড্রাইভারের কনফ্লিক্ট শাখা %A লিখবে না; রিপোর্ট দেবে;
  • CI-তে node -e "JSON.parse(...)" বা jq empty গেট যোগ করুন, যাতে "বৈধ কিন্তু ভুল" JSON মার্জের পরে ও ডিপ্লয়ের আগে ধরা পড়ে;
  • মিনিফাইড JSON ও জেনারেট করা ফাইল .gitignore-এ যোগ করে সেই শ্রেণির কনফ্লিক্ট পুরো সরান।

১০. সাধারণ প্রশ্ন

merge=union কখন নিরাপদ?

কেবল append-only ফাইলে যেখানে প্রতিটি লাইন সম্পূর্ণ রেকর্ড আর ক্রমের কোনো অর্থ নেই, যেমন JSON Lines লগ। যে মুহূর্তে লাইন জায়গায় সম্পাদনা হতে পারে — বিদ্যমান রেকর্ডে ফিল্ড যোগ — union দুই পরস্পরবিরোধী রেকর্ড তৈরি করে। এটি নকশার সীমানা, সাবধানে ব্যবহারের বিষয় নয়।

কাস্টম ড্রাইভার অ্যারে সামলাতে পারে?

উপরের সংস্করণ অ্যারেকে পারমাণবিক ধরে: দুই পক্ষ অ্যারে বদলালে কনফ্লিক্ট রিপোর্ট করে। ইনডেক্স মিলানো পুরোনো টেক্সট-ডিফ সমস্যা পুনরুৎপাদন করে (একটি আইটেম ঢোকালে সব সরে), আর id মিলানোর জন্য সম্মত প্রাইমারি কি দরকার। অ্যারে মার্জ মানুষের হাতে ছাড়াই ভালো, বা "অ্যারে append-only" প্রথা দিয়ে আটকান।

ড্রাইভারে বাগ থাকলে কী হয়?

এটি নীরবে ফাইল নষ্ট করে। রোলআউটের আগে স্ক্র্যাচ রিপোজিটরিতে তিন আকৃতিতে প্রথম সংস্করণ পরীক্ষা করুন (প্রতি পক্ষ কি যোগ, দুই পক্ষ একই কি বদল, এক মোছে আর অন্য সম্পাদনা)। ভাঙা ড্রাইভার নীরবে ফাইল আবার লেখে, যা কনফ্লিক্ট মার্কারের চেয়ে ধরতে অনেক কঠিন।

শুধু এক লাইন jq দিয়ে মার্জ করবেন না কেন?

jq -s '.[0] * .[1]' অগভীর মার্জ, এর কি-ক্রম ও নেস্টেড আচরণ কেস ধরে যাচাই করতে হয়, আর এর পূর্বপুরুষ নেই — base ছাড়া "মোছা" ও "অপরিবর্তিত" আলাদা করা যায় না। থ্রি-ওয়ে মার্জে তিন ইনপুট দরকার।

কনফ্লিক্ট রিপোর্ট কমিট করা উচিত?

না। .gitignore-এ যোগ করুন; এটি এককালীন আর্টিফ্যাক্ট যা মার্জ শেষে মেয়াদোত্তীর্ণ।