وقتی دیروز درست کار می‌کرد

بعد از چند هفته توسعه، خروجی یک گزارش اشتباه شده و کسی نمی‌داند کدام تغییر مسئول است. مرور تصادفی commitها یا خواندن همه diffها می‌تواند ساعت‌ها طول بکشد. اگر یک نسخه سالم و یک نسخه خراب داریم و خطا قابل بازتولید است، git bisect با تقسیم محدوده تاریخچه کمک می‌کند اولین تغییر نامطلوب را محدود کنیم. پیش‌نیاز اصلی، آزمون قابل اعتماد است؛ ابزار نمی‌تواند به جای تیم تعریف کند «خراب» دقیقاً چه معنی دارد.

ابتدا یک ورودی کوچک بسازید که اختلاف رفتار را نشان دهد. همان ورودی را روی دو سر محدوده اجرا کنید و خروجی مورد انتظار را از قانون محصول به دست آورید. به شبکه زنده، ساعت فعلی یا فایل خارج مخزن تکیه نکنید؛ این وابستگی‌ها ممکن است نسخه سالم را هم خراب نشان دهند. برای خطای ناپایدار، اول شرایط بازتولید را پایدار کنید؛ یک تصمیم غلط در میانه جست‌وجو مسیر بعدی را منحرف می‌کند.

ساختن آزمونی که تاریخچه را قضاوت کند

پیش از شروع، کار جاری را commit یا به روش مناسب نگه دارید و در checkout آزمایشی کار کنید؛ bisect میان نسخه‌ها جابه‌جا می‌شود. در مثال، نام good-release یک برچسب فرضی است که باید با commit سالم واقعی جایگزین شود. HEAD خراب فعلی را مشخص می‌کند. هر دو نسخه باید قبل از شروع بررسی شوند؛ حدس‌زدن سالم‌بودن نسخه قدیمی نتیجه را بی‌اعتبار می‌کند.

در اجرای خودکار، کد خروج صفر یعنی سالم، کد غیرصفر مناسب یعنی خراب و 125 یعنی این نسخه قابل آزمون نیست. همه خطاهای build را به خراب تبدیل نکنید؛ شاید نسخه قدیمی به وابستگی یا runtime دیگری نیاز داشته باشد. برعکس، استفاده زیاد از skip می‌تواند چند commit احتمالی باقی بگذارد. گزارش bisect log را همراه نسخه ابزار و فایل ورودی نگه دارید تا نتیجه قابل بررسی باشد.

commit پیدا‌شده نقطه تغییر رفتار است، نه لزوماً تنها علت معماری. ممکن است یک تنظیم، تغییر داده یا ترتیب اجرای پیش‌فرض، ایراد قدیمی را آشکار کرده باشد. diff و قرارداد وابستگی‌ها را بخوانید و تست بازتولید را به آزمون رگرسیون تبدیل کنید. بازگرداندن فوری commit بدون بررسی می‌تواند اصلاح‌های مستقل داخل همان تغییر را نیز حذف کند؛ تصمیم اصلاح باید آگاهانه باشد.

نمونه کد و روش بررسی

پیش‌نیاز و خروجی مورد انتظار در توضیح مثال آمده است. ابتدا نمونه را در محیط آزمایشی اجرا کنید و سپس آن را با داده و محدودیت‌های پروژه خود تطبیق دهید.

1. دستورات ترمینال؛ در checkout آزمایشی و تمیز

# Replace good-release with a verified good tag or commit.
# tools/check_regression.py must exist at every tested revision,
# or be supplied from a fixed path outside the checkout.
git bisect start
git bisect bad HEAD
git bisect good good-release
git bisect run python tools/check_regression.py
git bisect log
git bisect reset

دستورها را خط‌به‌خط و فقط پس از جایگزینی نسخه سالم اجرا کنید. اسکریپت باید برای رفتار سالم exit 0، برای خطای مورد نظر exit 1 و برای نسخه غیرقابل آزمون exit 125 بدهد. این دستورها وابسته به مخزن شما هستند و نمونه مستقل قابل اجرا نیستند.

2. ساخت آزمایشگاه مستقل با یک رگرسیون معلوم

این فایل یک مخزن تازه در پوشه موقت می‌سازد و تنظیم نام نویسنده را فقط در همان مخزن ثبت می‌کند. سه commit اول درست‌اند و از commit چهارم مقدار اشتباه می‌شود. اسکریپت بررسی بیرون checkout قرار می‌گیرد تا جابه‌جایی تاریخچه آن را حذف نکند.

# make_bisect_demo.py; requires git on PATH.
from pathlib import Path
import subprocess
import tempfile

repo = Path(tempfile.mkdtemp(prefix="liyan-bisect-demo-"))
def git(*args):
    return subprocess.check_output(["git", "-C", str(repo), *args], text=True).strip()

git("init", "--quiet")
git("config", "user.name", "Bisect Demo")
git("config", "user.email", "demo@example.test")
for index in range(6):
    result = 100 if index < 3 else 99
    (repo / "calc.py").write_text(
        f"# revision {index + 1}\ndef total():\n    return {result}\n", encoding="utf-8"
    )
    git("add", "calc.py")
    git("commit", "--quiet", "-m", f"step-{index + 1}")
    if index == 0:
        git("tag", "demo-good")
    if index == 3:
        git("tag", "demo-first-bad")

check = repo.parent / (repo.name + "-check.py")
check.write_text(
    "import pathlib, runpy, sys\n"
    "if not pathlib.Path('calc.py').exists(): sys.exit(125)\n"
    "module = runpy.run_path('calc.py')\n"
    "sys.exit(0 if module['total']() == 100 else 1)\n",
    encoding="utf-8",
)
print("REPOSITORY:", repo)
print("CHECK SCRIPT:", check)
print("EXPECTED BAD:", git("rev-parse", "demo-first-bad"))

سه مسیر/شناسه چاپ‌شده را نگه دارید. کد خروج checker برای فایل موجودِ سالم صفر، برای خروجی اشتباه یک و برای نسخه فاقد فایل 125 است. این آزمایشگاه نیازی به اینترنت یا remote ندارد و در مخزن پروژه اصلی تغییری ایجاد نمی‌کند.

3. اجرای جست‌وجو و ثبت شواهد

مسیرهای placeholder را با خروجی مرحله قبل عوض کنید و دستورها را خط‌به‌خط اجرا کنید. -C اجازه می‌دهد بدون عوض‌کردن پوشه جاری، فقط همان مخزن آزمایشگاهی هدف باشد.

# Substitute both paths printed by make_bisect_demo.py.
git -C "/path/to/liyan-bisect-demo-..." bisect start HEAD demo-good
git -C "/path/to/liyan-bisect-demo-..." bisect run python "/path/to/liyan-bisect-demo-...-check.py"
git -C "/path/to/liyan-bisect-demo-..." rev-parse refs/bisect/bad
git -C "/path/to/liyan-bisect-demo-..." bisect log
git -C "/path/to/liyan-bisect-demo-..." bisect reset

شناسه refs/bisect/bad باید با EXPECTED BAD یکسان باشد. bisect log مجموعه تصمیم‌ها را نشان می‌دهد و reset شما را به HEAD اولیه برمی‌گرداند. اگر در کار واقعی چند commit به دلیل skip باقی ماند، آن را یک نتیجه قطعی واحد گزارش نکنید.

4. بررسی خودکار پیدا‌شدن commit چهارم

این فایل را بیرون مخزن آزمایشگاهی ذخیره کنید و دو مسیر چاپ‌شده را به آن بدهید. پیشوند نام مخزن کنترل می‌شود تا دستورهای جابه‌جایی روی پوشه اشتباه اجرا نشوند. در تمام مسیرها، finally حالت bisect را می‌بندد.

# verify_bisect_demo.py REPOSITORY CHECK_SCRIPT
from pathlib import Path
import subprocess
import sys

repo = Path(sys.argv[1]).resolve()
check = Path(sys.argv[2]).resolve()
if not repo.name.startswith("liyan-bisect-demo-"):
    raise SystemExit("Use only the disposable repository from this article")

def git(*args):
    return subprocess.check_output(["git", "-C", str(repo), *args], text=True).strip()

expected = git("rev-parse", "demo-first-bad")
try:
    git("bisect", "start", "HEAD", "demo-good")
    git("bisect", "run", sys.executable, str(check))
    assert git("rev-parse", "refs/bisect/bad") == expected
    print("Found the expected regression:", expected)
finally:
    git("bisect", "reset")

خروجی باید Found the expected regression را همراه commit چهارم چاپ کند. پس از ثبت نتیجه، پوشه آزمایشگاهی و checker آن صرفاً artifact تمرین‌اند. در پروژه واقعی، به‌جای total() یک تست کوچک و پایدار از رفتار خراب بسازید و آن را به مجموعه رگرسیون اضافه کنید.

تفسیر نتیجه و اصلاح علت

برای تمرین دانشجویی، یک مخزن کوچک با چند تغییر بسازید و عمداً قانون محاسبه را در یک commit تغییر دهید. یک اسکریپت بررسی قطعی بنویسید و با bisect همان commit را پیدا کنید. در پایان bisect reset شما را از حالت جست‌وجو خارج می‌کند. ارزش این تمرین، یادگیری رابطه تاریخچه، آزمایش و شواهد است؛ اشکال‌زدایی از حدس شخصی به فرایندی قابل تکرار تبدیل می‌شود.

چک‌لیست اجرای عملی

  • هر دو سر سالم و خراب با همان تست تأیید شوند.
  • checkout آزمایشی تمیز باشد.
  • exit code خطا با نسخه غیرقابل آزمون اشتباه نشود.
  • پس از ثبت log، bisect reset اجرا شود.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.