فهرست مقاله
در پروژهٔ مهندسی، دو تغییرِ جدا میتوانند هنگام ادغام به یک تنظیمِ ناسازگار برسند. ابزارِ ادغام، شباهت و اختلافِ متن را میبیند؛ برای فهمِ شرطهای باند فرکانسی یا واحدها، آزمونِ دیگری لازم است. این درس، Git نصبشده را روی فایلهای کاملاً ساختگی اجرا میکند و جوابِ بایتیِ ازپیشمعلوم دارد. مخزن، شاخه، commit، تاریخچه و شبکه در آزمایش ساخته یا تغییر داده نمیشوند؛ فقط جزءِ ادغامِ سهفایل را میسنجیم.
۱. سه فایل و نقشِ نسخهٔ مشترک

سه نقش داریم: base متنِ اولیه، current تغییرِ سمتِ اول و other تغییرِ سمتِ دوم است. مطابقِ توضیحِ git merge-file، تغییرِ مسیرِ base به other در current ترکیب میشود. در این مثال، base را خودمان بهصورت ثابت تعریف میکنیم؛ کشفِ جدِ مشترکِ commitها انجام نمیشود. به همین دلیل، آزمونِ این تابعِ فایل، معادلِ آزمونِ کاملِ ادغامِ شاخهها نیست.
تنظیمِ ساختگی چند سطرِ مستقل دارد: نرخِ نمونه ۸۰۰۰ هرتز، پنجرهٔ ۸۰ نمونه و دو مرزِ فرکانسی ۵۰۰ و ۳۰۰۰ هرتز. سطرهای دیگر، زمینهٔ ثابت را میسازند تا فاصلهٔ تغییرها روشن باشد. این نوشتهها فقط دادهٔ آزموناند؛ فیلتر ساخته نمیشود، سیگنال پردازش نمیشود و رفتارِ سختافزار یا کیفیتِ صوت از آنها نتیجه نمیگیریم. واحدها در نامِ کلیدها مشخصاند.
در سناریوی نخست، current نرخ را به ۱۶۰۰۰ و other پنجره را به ۱۶۰ تغییر میدهد. جوابِ دستی باید هر دو تغییر و همهٔ سطرهای دستنخورده را داشته باشد. مرجع با دو جایگزینیِ معلوم روی متنِ ثابت ساخته میشود؛ خروجیِ خودِ Git را برای نوشتنِ انتظار به کار نمیبریم. وجودِ عددهای مطلوب نیز کافی نیست؛ کلِ بایتهای نتیجه با انتظار مقایسه میشوند.
۲. نتیجه در خروجی، با ورودیهای دستنخورده

حالتِ عادیِ merge-file میتواند current را بازنویسی کند. اینجا گزینهٔ -p نتیجه را به stdout میفرستد و پس از هر ادغام، بایتهای هر سه ورودی دوباره خوانده میشوند. برچسبهای current، base و other با -L ثابتاند. سبکِ merge، گزینهٔ --no-diff3، الگوریتمِ myers و طولِ هفتِ نشانگر نیز صریح تعیین شدهاند؛ خروجیِ مرجع به تنظیمِ سلیقهایِ کاربر وابسته گذاشته نشده است.
فایلها در پوشهٔ تازه و خصوصیِ کنارِ اسکریپت نوشته میشوند. مسیرِ مطلق و پوشهٔ والد کنترل میشود؛ تنها سه نامِ معلوم وجود دارند. محیطِ فرایندِ فرزند، متغیرهای Git ارثرسیده را کنار میگذارد و تنظیماتِ عمومی و سیستمی را برای همین اجرا محدود میکند؛ محیطِ اصلیِ کاربر تغییر نمیکند. این کار، sandbox یا آزمونِ امنیتِ اجرای برنامهٔ ناشناس نیست؛ متن و نامِ فایلها را خودمان ساختهایم.
طبقِ مرجع subprocess پایتون، آرگومانها را فهرست میدهیم و shell=False است. خروجی و خطا جدا، بهصورت بایت گرفته میشوند؛ تبدیلِ خودکارِ پایانِ خط نداریم. خودِ فایلهای آزمون ASCII و LF هستند. timeout پانزدهثانیهای فقط نگهبانِ اجرای فرایند است؛ زمانِ واقعیِ ادغام اندازه گرفته نشده و وعدهٔ پایان در آن مدت نمیدهیم.
۳. تعارضِ یک سطر، کدِ بازگشتِ یک

در سناریوی دوم، هر دو طرف همان سطرِ نرخِ نمونه را تغییر میدهند: یک طرف ۱۶۰۰۰ و دیگری ۴۴۱۰۰ هرتز. Git در اجرای ثبتشده یک ناحیهٔ تعارض ساخت و کدِ بازگشتِ یک داد. بلوکِ دارای نشانگرها، با انتظارِ دستیِ کامل مقایسه میشود؛ هر دو مقدار باقیاند و هیچکدام بهطور خودکار برنده نشده است. گزینههای ترجیحِ یک طرف یا اتحادِ متن را به کار نمیبریم.
"""Actual Git text merge on original disposable offline DSP-config fixtures."""
import json
import os
import platform
import shutil
import subprocess
import tempfile
from pathlib import Path
BASE = b"""# synthetic DSP configuration; no hardware execution
sample_rate_hz=8000
channels=1
unit=V
dtype=float
source=offline
window_samples=80
hop_samples=40
boundary=zero
output=preview
low_cutoff_hz=500
filter_band=demo
order=2
normalization=none
trace=fixtures
high_cutoff_hz=3000
"""
def semantic_band_valid(data):
"""Only our fixed ASCII fixture's numerical band policy, not a general parser."""
if any(marker in data for marker in (b"<<<<<<<", b"=======", b">>>>>>>")):
return False
fields = {}
for line in data.decode("ascii").splitlines():
if line and not line.startswith("#"):
key, value = line.split("=", 1)
fields[key] = value
fs = int(fields["sample_rate_hz"])
low = int(fields["low_cutoff_hz"])
high = int(fields["high_cutoff_hz"])
return fs > 0 and 0 < low < high < fs / 2
def run_example():
git = shutil.which("git")
if git is None:
raise RuntimeError("git_not_installed")
git = str(Path(git).resolve())
parent = Path(__file__).resolve().parent
folder = Path(tempfile.mkdtemp(prefix="merge-fixture-", dir=parent)).resolve()
if folder.parent != parent or not folder.name.startswith("merge-fixture-"):
raise RuntimeError("unexpected_temp_location")
names = ("current.cfg", "base.cfg", "other.cfg")
paths = {name: folder / name for name in names}
env = os.environ.copy()
for key in tuple(env):
if key.startswith("GIT_"):
env.pop(key)
env["GIT_CONFIG_NOSYSTEM"] = "1"
env["GIT_CONFIG_GLOBAL"] = os.devnull
env["GIT_CEILING_DIRECTORIES"] = str(folder)
def command(args):
return subprocess.run(
[git] + args, cwd=folder, env=env, shell=False,
capture_output=True, check=False, timeout=15,
)
def merge(current, other):
originals = {"current.cfg": current, "base.cfg": BASE, "other.cfg": other}
for name, data in originals.items():
paths[name].write_bytes(data)
result = command([
"-c", "merge.conflictStyle=merge", "merge-file", "-p", "--no-diff3",
"--diff-algorithm=myers", "--marker-size=7",
"-L", "current", "-L", "base", "-L", "other",
"current.cfg", "base.cfg", "other.cfg",
])
unchanged = all(paths[name].read_bytes() == data for name, data in originals.items())
return result, unchanged
checks = {}
try:
version = command(["--version"])
if version.returncode != 0:
raise RuntimeError("git_version_failed")
current = BASE.replace(b"sample_rate_hz=8000", b"sample_rate_hz=16000")
other = BASE.replace(b"window_samples=80", b"window_samples=160")
clean, clean_unchanged = merge(current, other)
expected_clean = BASE.replace(b"sample_rate_hz=8000", b"sample_rate_hz=16000").replace(
b"window_samples=80", b"window_samples=160")
checks["nonoverlap_clean_status_and_stderr"] = clean.returncode == 0 and clean.stderr == b""
checks["nonoverlap_exact_independent_literal_result"] = clean.stdout == expected_clean
checks["stdout_mode_preserves_all_three_inputs"] = clean_unchanged
checks["clean_fixture_has_no_conflict_markers_and_valid_band"] = (
b"<<<<<<<" not in clean.stdout and semantic_band_valid(clean.stdout))
different = command(["diff", "--no-index", "--no-color", "--no-ext-diff", "--no-textconv",
"--", "base.cfg", "current.cfg"])
checks["no_index_diff_outside_repository_reports_expected_changed_line"] = (
different.returncode == 1 and b"-sample_rate_hz=8000\n" in different.stdout
and b"+sample_rate_hz=16000\n" in different.stdout and different.stderr == b"")
identical = command(["diff", "--no-index", "--no-color", "--no-ext-diff", "--no-textconv",
"--", "base.cfg", "base.cfg"])
checks["no_index_identical_diff_is_zero_and_empty"] = (
identical.returncode == 0 and identical.stdout == b"" and identical.stderr == b"")
conflict_other = BASE.replace(b"sample_rate_hz=8000", b"sample_rate_hz=44100")
conflict, conflict_unchanged = merge(current, conflict_other)
expected_conflict = BASE.replace(b"sample_rate_hz=8000\n", (
b"<<<<<<< current\nsample_rate_hz=16000\n=======\n"
b"sample_rate_hz=44100\n>>>>>>> other\n"))
checks["one_conflicting_region_returns_one_not_generic_tool_failure"] = (
conflict.returncode == 1 and conflict.stderr == b"")
checks["conflict_exact_handwritten_marker_result"] = conflict.stdout == expected_conflict
checks["three_explicit_labels_and_merge_style_no_base_marker"] = (
conflict.stdout.count(b"<<<<<<< current\n") == 1
and conflict.stdout.count(b"=======\n") == 1
and conflict.stdout.count(b">>>>>>> other\n") == 1
and b"|||||||" not in conflict.stdout)
checks["conflict_output_is_unresolved_and_originals_unchanged"] = (
not semantic_band_valid(conflict.stdout) and conflict_unchanged)
semantic_current = BASE.replace(b"low_cutoff_hz=500", b"low_cutoff_hz=2500")
semantic_other = BASE.replace(b"high_cutoff_hz=3000", b"high_cutoff_hz=2000")
semantic, semantic_unchanged = merge(semantic_current, semantic_other)
expected_semantic = BASE.replace(b"low_cutoff_hz=500", b"low_cutoff_hz=2500").replace(
b"high_cutoff_hz=3000", b"high_cutoff_hz=2000")
checks["textually_clean_semantically_wrong_exact_output"] = (
semantic.returncode == 0 and semantic.stdout == expected_semantic and semantic.stderr == b"")
checks["individually_valid_edits_can_combine_into_invalid_band"] = (
semantic_band_valid(BASE) and semantic_band_valid(semantic_current)
and semantic_band_valid(semantic_other) and not semantic_band_valid(semantic.stdout))
checks["semantic_case_also_preserves_originals"] = semantic_unchanged
identity, identity_unchanged = merge(BASE, BASE)
checks["no_changes_returns_exact_base"] = (
identity.returncode == 0 and identity.stdout == BASE and identity.stderr == b"" and identity_unchanged)
missing = command(["merge-file", "-p", "missing.cfg", "base.cfg", "other.cfg"])
checks["missing_fixture_is_distinct_recorded_error_not_one_conflict"] = (
missing.returncode not in (0, 1) and missing.stderr != b"" and missing.stdout == b"")
checks["only_three_named_files_and_no_repository_created"] = (
sorted(p.name for p in folder.iterdir()) == sorted(names) and not (folder / ".git").exists())
output = {
"runtime": {"python": platform.python_version(), "git": version.stdout.decode("ascii").strip(),
"platform": platform.system()},
"fixture": {"input_encoding": "ASCII bytes, LF newline", "units": "Hz and samples in synthetic config only",
"private_files": list(names), "repository_created": False, "network_used": False,
"merge_args": "merge-file -p --no-diff3 --diff-algorithm=myers --marker-size=7 -L current -L base -L other current.cfg base.cfg other.cfg"},
"results": {"clean_returncode": clean.returncode, "clean_text": clean.stdout.decode("ascii"),
"one_region_conflict_returncode": conflict.returncode,
"conflict_text": conflict.stdout.decode("ascii"),
"semantic_invalid_clean_returncode": semantic.returncode,
"semantic_invalid_clean_text": semantic.stdout.decode("ascii"),
"diff_changed_returncode": different.returncode, "diff_same_returncode": identical.returncode,
"missing_input_returncode": missing.returncode},
"checks": checks,
"limitations": ["Actual Git text merge only; no branch, commit, index or user repository is touched.",
"Clean text merge is not validation of filter semantics, hardware, safety or performance.",
"Exit 1 here is one conflict region; observed missing-input code is not a portable error taxonomy.",
"Timeout is only a subprocess test guard, not a measured merge deadline."],
}
finally:
if folder.parent != parent or not folder.name.startswith("merge-fixture-"):
raise RuntimeError("cleanup_location_mismatch")
for path in paths.values():
if path.parent != folder:
raise RuntimeError("cleanup_path_mismatch")
if path.exists():
path.unlink()
folder.rmdir() # Nonrecursive: unexpected files prevent cleanup instead of being deleted.
checks["private_named_files_cleaned_nonrecursively"] = not folder.exists()
if not all(type(value) is bool and value for value in checks.values()):
raise AssertionError({name: passed for name, passed in checks.items() if not passed})
return output
if __name__ == "__main__":
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
در این فرمان، کدِ یک را نباید فوراً «ابزار اجرا نشد» معنی کنیم؛ مستندات تعدادِ ناحیههای تعارض را از نتیجهٔ تمیزِ صفر جدا میکنند. برای دیدنِ خطای متفاوت، نامِ ورودیِ ناموجودِ ساختگی نیز داده میشود؛ این اجرا ۲۵۵ و پیامِ خطا داشت. آن عدد، مشاهدهٔ همین محیط است و جدولِ جامعِ خطاهای همهٔ سیستمها نیست. قراردادِ عددِ منفیِ سیگنال در subprocess هم ویژهٔ POSIX است.
به همین علت check=False انتخاب شده و هر وضعیت با انتظارِ همان سناریو سنجیده میشود. check=True برای هر کدِ غیرصفر استثنا میساخت و بدونِ رسیدگیِ آگاهانه، تعارضِ موردِ انتظار را هم خطا میدید. نبودِ استثنا، حلشدنِ تعارض را ثابت نمیکند؛ متنِ نشانگردار هنوز نیاز به تصمیمِ انسانی و آزمون دارد. تابعِ کوچکِ بررسیِ باندِ ما چنین خروجیای را معتبر نمیشمارد.
۴. ادغامِ تمیز با معنای نادرست

اکنون current فقط مرزِ پایین را از ۵۰۰ به ۲۵۰۰ و other فقط مرزِ بالا را از ۳۰۰۰ به ۲۰۰۰ هرتز تغییر میدهد. هر تغییر نسبت به base، بهتنهایی شرطِ قراردادیِ مثبتبودنِ پایین، کوچکبودنِ آن از بالا و کوچکبودنِ بالا از نصفِ نرخ را دارد. اما نتیجهٔ ترکیبی، پایینِ ۲۵۰۰ و بالای ۲۰۰۰ دارد؛ نامرتب است. چون تغییرها در سطرهای جدا هستند، Git نتیجهٔ متنیِ تمیز و کدِ صفر میدهد.
{
"runtime": {
"python": "3.14.5",
"git": "git version 2.55.0.windows.3",
"platform": "Windows"
},
"fixture": {
"input_encoding": "ASCII bytes, LF newline",
"units": "Hz and samples in synthetic config only",
"private_files": [
"current.cfg",
"base.cfg",
"other.cfg"
],
"repository_created": false,
"network_used": false,
"merge_args": "merge-file -p --no-diff3 --diff-algorithm=myers --marker-size=7 -L current -L base -L other current.cfg base.cfg other.cfg"
},
"results": {
"clean_returncode": 0,
"clean_text": "# synthetic DSP configuration; no hardware execution\nsample_rate_hz=16000\nchannels=1\nunit=V\ndtype=float\nsource=offline\nwindow_samples=160\nhop_samples=40\nboundary=zero\noutput=preview\nlow_cutoff_hz=500\nfilter_band=demo\norder=2\nnormalization=none\ntrace=fixtures\nhigh_cutoff_hz=3000\n",
"one_region_conflict_returncode": 1,
"conflict_text": "# synthetic DSP configuration; no hardware execution\n<<<<<<< current\nsample_rate_hz=16000\n=======\nsample_rate_hz=44100\n>>>>>>> other\nchannels=1\nunit=V\ndtype=float\nsource=offline\nwindow_samples=80\nhop_samples=40\nboundary=zero\noutput=preview\nlow_cutoff_hz=500\nfilter_band=demo\norder=2\nnormalization=none\ntrace=fixtures\nhigh_cutoff_hz=3000\n",
"semantic_invalid_clean_returncode": 0,
"semantic_invalid_clean_text": "# synthetic DSP configuration; no hardware execution\nsample_rate_hz=8000\nchannels=1\nunit=V\ndtype=float\nsource=offline\nwindow_samples=80\nhop_samples=40\nboundary=zero\noutput=preview\nlow_cutoff_hz=2500\nfilter_band=demo\norder=2\nnormalization=none\ntrace=fixtures\nhigh_cutoff_hz=2000\n",
"diff_changed_returncode": 1,
"diff_same_returncode": 0,
"missing_input_returncode": 255
},
"checks": {
"nonoverlap_clean_status_and_stderr": true,
"nonoverlap_exact_independent_literal_result": true,
"stdout_mode_preserves_all_three_inputs": true,
"clean_fixture_has_no_conflict_markers_and_valid_band": true,
"no_index_diff_outside_repository_reports_expected_changed_line": true,
"no_index_identical_diff_is_zero_and_empty": true,
"one_conflicting_region_returns_one_not_generic_tool_failure": true,
"conflict_exact_handwritten_marker_result": true,
"three_explicit_labels_and_merge_style_no_base_marker": true,
"conflict_output_is_unresolved_and_originals_unchanged": true,
"textually_clean_semantically_wrong_exact_output": true,
"individually_valid_edits_can_combine_into_invalid_band": true,
"semantic_case_also_preserves_originals": true,
"no_changes_returns_exact_base": true,
"missing_fixture_is_distinct_recorded_error_not_one_conflict": true,
"only_three_named_files_and_no_repository_created": true,
"private_named_files_cleaned_nonrecursively": true
},
"limitations": [
"Actual Git text merge only; no branch, commit, index or user repository is touched.",
"Clean text merge is not validation of filter semantics, hardware, safety or performance.",
"Exit 1 here is one conflict region; observed missing-input code is not a portable error taxonomy.",
"Timeout is only a subprocess test guard, not a measured merge deadline."
]
}
این شاهد، مسئولیتِ دو آزمون را روشن میکند: یک آزمون، ادغامِ دقیقِ متن را تأیید میکند؛ دیگری شرطِ عددیِ خودِ تنظیم را. شرطِ نمونه، مشخصاتِ کاملِ فیلتر نیست و دربارهٔ پایداری، پاسخِ فرکانسی، تأخیر یا قابلیتِ اجرای دستگاه حرفی نمیزند. خوانندهٔ فیلدها هم فقط قالبِ ثابتِ همین fixture را میشناسد؛ اعتبارسنجِ عمومیِ فایلِ پیکربندی یا API نیست.
git diff --no-index دو فایلِ مستقل را بدونِ مخزن مقایسه میکند. در کد، اختلافِ base و current وضعیتِ یک و سطرهای منفی و مثبتِ موردِ انتظار دارد؛ مقایسهٔ base با خودش، صفر و خروجیِ تهی میدهد. معنای یک در این فرمان، «اختلاف» است؛ در merge-file «یک تعارض» بود. نامِ فرمان و قراردادِ آن، پیش از تفسیرِ عدد لازماند.
هفده گروهِ واقعی با Python 3.14.5 و Git 2.55.0.windows.3 در ویندوز اجرا شدهاند. نسخهٔ مرجعِ subprocess برابر 3.14.8 و مستندِ جاریِ diff برابر 2.56.0 است؛ آن نسخهها اجراشده معرفی نمیشوند. پس از پایان، فقط سه فایلِ معلوم پاک و پوشه با rmdir غیربازگشتی حذف میشود؛ فایلِ غیرمنتظره مانعِ حذف خواهد شد. قطعِ ناگهانیِ فرایند و خرابیِ دیسک، در این آزمایش سنجیده نشدهاند.
بازبینیِ مرزِ برشِ تصویر نمونهای از مرجعِ مستقل برای معنای تغییر است. نتیجهٔ خطا در جایگزینیِ فایل نیز اثرِ محلی را از پیامِ خطا جدا میکند. این درس، موضوعِ دیگری دارد: تفسیرِ خروجیِ ادغام. منابع در ۲۵ مهر ۱۴۰۵ خوانده شدهاند؛ هیچ ادعای سرعتِ بهتر یا آزمونِ مخزنِ واقعی مطرح نیست.
۵. چهار پرسش با پاسخِ کامل

پرسش اول: تغییرِ نرخ به ۱۶۰۰۰ و پنجره به ۱۶۰ چه نتیجهای دارد؟ پاسخ: هر دو تغییر در جوابِ دقیقِ دستی حاضرند و ادغامِ ثبتشده کدِ صفر میدهد؛ سطرهای دیگر باید عیناً حفظ شوند، نه فقط دو عددِ تازه.
پرسش دوم: کدِ یکِ سناریوی نرخِ مشترک یعنی چه؟ پاسخ: یک ناحیهٔ تعارضِ متنی ساخته شده است. خروجی هر دو نرخ را با نشانگر دارد و حلِ خودکار انجام نشده؛ این حالت با ورودیِ ناموجودِ مشاهدهشدهٔ ۲۵۵ فرق دارد.
پرسش سوم: چرا نتیجهٔ ۲۵۰۰ و ۲۰۰۰ با کدِ صفر رد میشود؟ پاسخ: ادغام از نظرِ متن تمیز است، اما شرطِ مستقلِ پایینِ کوچکتر از بالا نقض میشود. موفقیتِ ابزارِ ادغام، تأییدِ معنای تنظیمات نیست.
پرسش چهارم: آیا این اجرا شاخهٔ پروژه را تغییر میدهد؟ پاسخ: خیر؛ هیچ مخزن یا شاخهای ساخته نشده است. سه فایلِ خصوصی، با -p و حفظِ بایتِ ورودی، آزموده میشوند؛ نتیجه را نباید گزارشِ ادغامِ واقعیِ تاریخچه معرفی کرد.
منابع و مطالعهٔ بیشتر
- Git؛ قرارداد merge-file و ادغام سهطرفهٔ فایل
- Python؛ subprocess.run، خروجی باینری و کد بازگشت
- Git؛ diff --no-index برای دو فایل مستقل
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

