<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Debugging on DevOps Way - Практические гайды</title>
    <link>https://devopsway.ru/tags/debugging/</link>
    <description>Recent content in Debugging on DevOps Way - Практические гайды</description>
    <image>
      <title>DevOps Way - Практические гайды</title>
      <url>https://devopsway.ru/images/devopsway-og.png</url>
      <link>https://devopsway.ru/images/devopsway-og.png</link>
    </image>
    <generator>Hugo -- 0.160.1</generator>
    <language>ru</language>
    <lastBuildDate>Thu, 16 Apr 2026 13:12:41 -0400</lastBuildDate>
    <atom:link href="https://devopsway.ru/tags/debugging/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>День 6: bisect — Git находит сломавший коммит за вас</title>
      <link>https://devopsway.ru/posts/git-bisect/</link>
      <pubDate>Thu, 16 Apr 2026 10:00:00 +0300</pubDate>
      <guid>https://devopsway.ru/posts/git-bisect/</guid>
      <description>Бинарный поиск по истории коммитов. За 10 шагов найти виновника среди 1024 коммитов. Ручной bisect, автоматический через bisect run, и когда использовать skip.</description>
      <content:encoded><![CDATA[<h2 id="цель-урока">Цель урока</h2>
<p>После урока вы <strong>умеете</strong> найти коммит, внёсший регрессию, за <code>log₂(N)</code> шагов через <code>git bisect</code>; различаете <code>bad</code>, <code>good</code>, <code>skip</code>; умеете автоматизировать поиск через <code>git bisect run</code> с любым тест-скриптом.</p>
<table>
  <thead>
      <tr>
          <th>Параметр</th>
          <th>Значение</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bloom</td>
          <td>Применение, Анализ</td>
      </tr>
      <tr>
          <td>SFIA</td>
          <td>Уровень 2–3</td>
      </tr>
      <tr>
          <td>Время</td>
          <td>30–40 минут</td>
      </tr>
      <tr>
          <td>Артефакт</td>
          <td><code>test-bisect.sh</code> — тест-детектор под <code>bisect run</code></td>
      </tr>
      <tr>
          <td>Проверка</td>
          <td>Мини-тест + сценарий «найди виновника за 4 шага»</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="теория-за-3-минуты">Теория за 3 минуты</h2>
<p>Git-история — это <strong>DAG коммитов</strong>. Каждый коммит знает, от какого родителя он произошёл.</p>
<p>Когда тест сломался «где-то за последние две недели», ручной перебор по 50+ коммитам — час. <code>bisect</code> делает бинарный поиск по этому DAG: на каждом шаге берёт коммит ровно посередине между «точно работает» и «точно сломано», вы его проверяете, Git сужает диапазон вдвое.</p>
<p><strong>Математика простая:</strong></p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Коммитов между good и bad</th>
          <th style="text-align: center">Шагов bisect</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">16</td>
          <td style="text-align: center">4</td>
      </tr>
      <tr>
          <td style="text-align: right">64</td>
          <td style="text-align: center">6</td>
      </tr>
      <tr>
          <td style="text-align: right">1024</td>
          <td style="text-align: center">10</td>
      </tr>
      <tr>
          <td style="text-align: right">1 000 000</td>
          <td style="text-align: center">20</td>
      </tr>
  </tbody>
</table>
<p>Каждый шаг уменьшает область поиска в 2 раза. Это единственный инструмент Git с настоящей «магией» — вы поймёте почему, когда пройдёте практику.</p>
<hr>
<h2 id="практика-1-ручной-bisect-на-16-коммитах">Практика 1: ручной bisect на 16 коммитах</h2>
<h3 id="шаг-1-собираем-репо-с-регрессией">Шаг 1. Собираем репо с регрессией</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p demo-bisect <span class="o">&amp;&amp;</span> <span class="nb">cd</span> demo-bisect
</span></span><span class="line"><span class="cl">git init -q
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Пишем простой калькулятор процентов</span>
</span></span><span class="line"><span class="cl">cat &gt; calc.js <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">function percent(value, total) {
</span></span></span><span class="line"><span class="cl"><span class="s">  return (value / total) * 100;
</span></span></span><span class="line"><span class="cl"><span class="s">}
</span></span></span><span class="line"><span class="cl"><span class="s">module.exports = percent;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat &gt; test.js <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">const percent = require(&#39;./calc&#39;);
</span></span></span><span class="line"><span class="cl"><span class="s">const r = percent(25, 100);
</span></span></span><span class="line"><span class="cl"><span class="s">if (r !== 25) { console.error(&#39;FAIL: got&#39;, r); process.exit(1); }
</span></span></span><span class="line"><span class="cl"><span class="s">console.log(&#39;PASS&#39;);
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git add . <span class="o">&amp;&amp;</span> git commit -q -m <span class="s2">&#34;feat: percent calculator&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 15 коммитов с невинными правками, один из них ломает тест</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> i in <span class="k">$(</span>seq <span class="m">1</span> 15<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;// iteration </span><span class="nv">$i</span><span class="s2">&#34;</span> &gt;&gt; calc.js
</span></span><span class="line"><span class="cl">  <span class="c1"># В коммит 9 вносим регрессию</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$i</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;9&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    sed -i <span class="s1">&#39;s|(value / total) \* 100|(value / total) + 100|&#39;</span> calc.js
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  git add . <span class="o">&amp;&amp;</span> git commit -q -m <span class="s2">&#34;chore: minor change </span><span class="nv">$i</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git log --oneline <span class="p">|</span> head -5
</span></span></code></pre></div><h3 id="шаг-2-проверяем-что-сейчас-сломано">Шаг 2. Проверяем, что сейчас сломано</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">node test.js
</span></span><span class="line"><span class="cl"><span class="c1"># FAIL: got 100.25</span>
</span></span></code></pre></div><h3 id="шаг-3-запускаем-bisect">Шаг 3. Запускаем bisect</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bisect start
</span></span><span class="line"><span class="cl">git bisect bad HEAD           <span class="c1"># текущее состояние — плохое</span>
</span></span><span class="line"><span class="cl">git bisect good HEAD~15       <span class="c1"># самый первый коммит — хороший</span>
</span></span></code></pre></div><p>Git переключится на середину и скажет:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Bisecting: 7 revisions left to test after this (roughly 3 steps)
</span></span></code></pre></div><h3 id="шаг-4-тестируем-и-отвечаем">Шаг 4. Тестируем и отвечаем</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">node test.js <span class="o">&amp;&amp;</span> git bisect good <span class="o">||</span> git bisect bad
</span></span></code></pre></div><p>Git возьмёт следующую середину. Повторяете команду выше ещё 2–3 раза.</p>
<p>После последнего шага увидите:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">&lt;sha&gt; is the first bad commit
</span></span><span class="line"><span class="cl">commit &lt;sha&gt;
</span></span><span class="line"><span class="cl">    chore: minor change 9
</span></span></code></pre></div><h3 id="шаг-5-выход-из-bisect">Шаг 5. Выход из bisect</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bisect reset
</span></span></code></pre></div><p><strong>Итог</strong>: нашли виновника из 15 коммитов за <strong>4 шага</strong> (log₂(16) = 4). Если бы коммитов было 1024, ушло бы 10 шагов. Руками было бы 1024 запуска теста.</p>
<hr>
<h2 id="практика-2-автоматический-bisect-через-run">Практика 2: автоматический bisect через <code>run</code></h2>
<p>Проверка «запусти тест и ответь good/bad» — это цикл. Git умеет его крутить сам.</p>
<h3 id="шаг-1-тест-скрипт-с-правильными-exit-кодами">Шаг 1. Тест-скрипт с правильными exit-кодами</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; test-bisect.sh <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="s"># Bisect ожидает: exit 0 = good, exit 1 = bad, exit 125 = skip (не могу протестировать)
</span></span></span><span class="line"><span class="cl"><span class="s">node test.js &gt; /dev/null 2&gt;&amp;1
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">chmod +x test-bisect.sh
</span></span></code></pre></div><h3 id="шаг-2-полностью-автоматический-поиск">Шаг 2. Полностью автоматический поиск</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bisect start HEAD HEAD~15
</span></span><span class="line"><span class="cl">git bisect run ./test-bisect.sh
</span></span></code></pre></div><p>Git сам пробегает все шаги, печатает итог, оставляет вас на виновнике:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">running &#39;./test-bisect.sh&#39;
</span></span><span class="line"><span class="cl">Bisecting: 7 revisions left...
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">&lt;sha&gt; is the first bad commit
</span></span><span class="line"><span class="cl">bisect found first bad commit
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bisect reset
</span></span></code></pre></div><p><strong>Это ключевой навык</strong>: если у вас есть воспроизводимый тест (любой скрипт, возвращающий 0/1), регрессию можно находить без участия человека. В CI это превращается в job «найди-виновника-и-открой-issue».</p>
<hr>
<h2 id="практика-3-когда-тест-нестабилен--bisect-skip">Практика 3: когда тест нестабилен — <code>bisect skip</code></h2>
<p>Не все коммиты пригодны для тестирования. Пример: в коммите 5 сломалась сборка (не тест), <code>npm install</code> падает — вы ни good, ни bad сказать не можете.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bisect start HEAD HEAD~15
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># допустим, текущий коммит не собирается</span>
</span></span><span class="line"><span class="cl">git bisect skip
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Git возьмёт соседний коммит и попытается снова</span>
</span></span></code></pre></div><p>Для автоматизации — <code>exit 125</code> в тест-скрипте:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; test-bisect-safe.sh <span class="s">&lt;&lt; &#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="s"># Сначала проверяем, что коммит вообще работоспособен
</span></span></span><span class="line"><span class="cl"><span class="s">if [ ! -f calc.js ]; then
</span></span></span><span class="line"><span class="cl"><span class="s">  exit 125   # этот коммит не годится для теста
</span></span></span><span class="line"><span class="cl"><span class="s">fi
</span></span></span><span class="line"><span class="cl"><span class="s">node test.js &gt; /dev/null 2&gt;&amp;1
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">chmod +x test-bisect-safe.sh
</span></span></code></pre></div><p>Правило: <code>bad</code>/<code>good</code> — «я могу сказать про поведение», <code>skip</code> — «я не могу сказать».</p>
<hr>
<h2 id="артефакт-универсальный-шаблон-тест-скрипта">Артефакт: универсальный шаблон тест-скрипта</h2>
<p>Сохраните в корне своего проекта как <code>.bisect-run.sh</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># .bisect-run.sh — безопасный тест-детектор для git bisect run</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Правила: exit 0 = good, 1 = bad, 125 = skip, 128 = abort</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -u
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Сначала защита от нерабочих коммитов (не могу даже запустить)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> ! -f package.json <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">125</span>
</span></span><span class="line"><span class="cl">npm install --silent &gt; /dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">125</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Запускаем настоящий тест</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> npm <span class="nb">test</span> &gt; /tmp/bisect-test.log 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">0</span>   <span class="c1"># good</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Различаем «тест упал» vs «ошибка инфры»</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> grep -q <span class="s2">&#34;Test failed\|assert&#34;</span> /tmp/bisect-test.log<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>   <span class="c1"># bad — наша регрессия</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">125</span> <span class="c1"># skip — проблема не в нашем коде</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>Плюс алиас в <code>~/.gitconfig</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[alias]</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># bisect с автостартом от текущего HEAD</span>
</span></span><span class="line"><span class="cl">    <span class="na">bi</span> <span class="o">=</span> <span class="s">&#34;!f() { git bisect start HEAD \&#34;$1\&#34; &amp;&amp; git bisect run \&#34;${2:-./.bisect-run.sh}\&#34;; }; f&#34;</span>
</span></span></code></pre></div><p>Использование:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git bi HEAD~50                  <span class="c1"># искать среди последних 50 коммитов</span>
</span></span><span class="line"><span class="cl">git bi v1.2.0                   <span class="c1"># искать между тегом v1.2.0 и HEAD</span>
</span></span><span class="line"><span class="cl">git bi HEAD~50 ./my-test.sh     <span class="c1"># свой тест-скрипт</span>
</span></span></code></pre></div><hr>
<h2 id="-документирование">📝 Документирование</h2>
<p>Напишите в <code>NOTES.md</code> своего учебного репозитория:</p>
<ol>
<li><strong>Вашими словами</strong>: как bisect уменьшает 1000 коммитов до 10 шагов.</li>
<li><strong>Разницу <code>bad</code> / <code>good</code> / <code>skip</code></strong> — по одному предложению на каждый.</li>
<li><strong>Какой тест-скрипт вы напишете для своего проекта</strong>: набросайте 5–10 строк.</li>
<li><strong>Ситуация из прошлого</strong>: был ли случай, когда bisect помог бы? Сколько времени потратили руками?</li>
</ol>
<hr>
<h2 id="мини-тест">Мини-тест</h2>
<ol>
<li>В вашем проекте 512 коммитов между последним работающим тегом и сломанным HEAD. Сколько шагов bisect?</li>
<li>Какой exit-код должен вернуть тест-скрипт, если в текущем коммите не собирается зависимость (не от нашего кода)?</li>
<li>Почему плохая идея ставить <code>bad</code> в начале истории и <code>good</code> в HEAD, если тест сейчас падает?</li>
<li>Чем отличается <code>git bisect reset</code> от <code>git bisect end</code>? (подсказка: вторая команда не существует)</li>
</ol>
<p>Ответы — в конце поста.</p>
<hr>
<h2 id="что-дальше">Что дальше</h2>
<ul>
<li><strong>День 7</strong> → cherry-pick + rerere: точечный перенос коммитов между ветками, и как Git запоминает, как вы решили конфликт, чтобы не решать его второй раз</li>
<li><strong><a href="/posts/git-master-final-challenge/">Challenge</a></strong> → сломанный репозиторий с 10 проблемами. Задача P7 — регрессия в 20 коммитах, bisect найдёт её за 5 шагов</li>
<li><strong>Системно с нуля</strong> → «Курс молодого бойца» DevIT Academy</li>
</ul>
<hr>
<h2 id="ответы-на-мини-тест">Ответы на мини-тест</h2>
<ol>
<li><code>log₂(512) = 9</code> шагов.</li>
<li><code>exit 125</code> — «не могу протестировать». <code>bad</code> было бы ложным — регрессии в нашем коде нет.</li>
<li><code>bad</code> и <code>good</code> — это про <strong>поведение теста</strong>, а не про «свежесть» коммита. <code>good</code> значит «тест проходит», <code>bad</code> — «тест падает». В начале истории, скорее всего, теста ещё нет или он проходит — это <code>good</code>. Путают направление, когда думают, что <code>good</code> = «старый коммит». Git не знает времени — он знает тест.</li>
<li><code>git bisect reset</code> возвращает HEAD к исходной ветке и чистит состояние. Команды <code>git bisect end</code> нет — это типичная ошибка памяти. Если видите в чужом скрипте — это баг.</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
