<?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>Parallel on DevOps Way - Практические гайды</title>
    <link>https://devopsway.ru/tags/parallel/</link>
    <description>Recent content in Parallel 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/parallel/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>День 8: worktree — две ветки в двух каталогах, один репозиторий</title>
      <link>https://devopsway.ru/posts/day-08-worktree/</link>
      <pubDate>Thu, 16 Apr 2026 11:00:00 +0300</pubDate>
      <guid>https://devopsway.ru/posts/day-08-worktree/</guid>
      <description>Как держать открытыми одновременно feature и hotfix ветку в разных каталогах, не трогая git stash и git checkout. Одно хранилище .git, несколько рабочих деревьев.</description>
      <content:encoded><![CDATA[<h2 id="цель-урока">Цель урока</h2>
<p>После урока вы <strong>умеете</strong> создавать параллельные рабочие деревья через <code>git worktree</code>, держать в них разные ветки одновременно, убирать их и чистить мёртвые ссылки через <code>prune</code>. Понимаете, что объекты и refs лежат в одном <code>.git/</code>, а <code>HEAD</code> и <code>index</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>25–35 минут</td>
      </tr>
      <tr>
          <td>Артефакт</td>
          <td>Алиасы <code>wta</code>/<code>wtl</code>/<code>wtr</code> + конвенция <code>~/code/&lt;repo&gt;/</code> + <code>&lt;repo&gt;-&lt;ветка&gt;/</code></td>
      </tr>
      <tr>
          <td>Проверка</td>
          <td>Мини-тест + параллельная работа feature + hotfix без <code>stash</code></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="теория-за-3-минуты">Теория за 3 минуты</h2>
<p>Обычный репозиторий выглядит так: один каталог <code>my-repo/</code>, внутри <code>.git/</code> с объектами и refs, рядом ваши файлы. В каждый момент времени checked out <strong>одна ветка</strong> — это ваш <code>HEAD</code>.</p>
<p><code>git worktree</code> снимает это ограничение. Вы можете дополнительно выдать <strong>отдельный каталог</strong> (рабочее дерево), который использует <strong>тот же <code>.git/</code></strong>, но держит <strong>свой HEAD</strong> и <strong>свой index</strong>. В этом каталоге — другая ветка.</p>
<p>Что общее: объекты (<code>.git/objects/</code>), refs (<code>.git/refs/</code>), конфиг. Что раздельное: <code>HEAD</code>, <code>index</code>, stage, working copy.</p>
<p>Физически служебные файлы дополнительных worktrees лежат в <code>.git/worktrees/&lt;name&gt;/</code> главной репы. Поэтому <code>worktree add</code> дешёвый — не клонирует объекты заново.</p>
<p><strong>Главный случай применения:</strong> срочный hotfix в <code>main</code>, когда у вас в основном каталоге грязная <code>feature/X</code> с 20 правками. Без worktree — <code>git stash</code>, <code>checkout main</code>, пишешь hotfix, <code>checkout feature</code>, <code>stash pop</code>. С worktree — соседний каталог, отдельное IDE-окно, оба состояния живы параллельно.</p>
<p><strong>Ограничение:</strong> одна ветка не может быть checked out сразу в двух worktree (Git защищается). Нужна либо другая ветка, либо <code>--force</code>, либо <code>--detach</code>.</p>
<hr>
<h2 id="практика-1-параллельный-hotfix-без-stash">Практика 1: параллельный hotfix без stash</h2>
<h3 id="шаг-1-собираем-репо-с-начатой-feature">Шаг 1. Собираем репо с начатой feature</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-wt <span class="o">&amp;&amp;</span> <span class="nb">cd</span> demo-wt
</span></span><span class="line"><span class="cl">git init -q
</span></span><span class="line"><span class="cl">git config user.email s@e.com
</span></span><span class="line"><span class="cl">git config user.name Student
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;v1&#34;</span> &gt; app.txt <span class="o">&amp;&amp;</span> git add . <span class="o">&amp;&amp;</span> git commit -q -m <span class="s2">&#34;feat: initial&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git checkout -q -b feature/profile
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;// profile WIP&#34;</span> &gt; profile.js
</span></span><span class="line"><span class="cl">git add profile.js
</span></span><span class="line"><span class="cl"><span class="c1"># СТАРТОВАЛИ feature, но не дошли до коммита — index грязный</span>
</span></span><span class="line"><span class="cl">git status
</span></span></code></pre></div><p>В основном каталоге висит незакоммиченный <code>profile.js</code>. В классической схеме вы бы сейчас делали <code>git stash</code>, чтобы пойти чинить <code>main</code>.</p>
<h3 id="шаг-2-добавляем-worktree-под-hotfix">Шаг 2. Добавляем worktree под hotfix</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># вернуться в parent, потому что worktree add требует path вне текущего</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">git -C demo-wt worktree add ./demo-wt-hotfix -b hotfix/login main
</span></span></code></pre></div><p>Команда:</p>
<ul>
<li>создала соседний каталог <code>demo-wt-hotfix/</code></li>
<li>в нём HEAD указывает на новую ветку <code>hotfix/login</code>, которая отходит от <code>main</code></li>
<li>в основном <code>demo-wt/</code> ничего не поменялось — там по-прежнему <code>feature/profile</code> + грязный index</li>
</ul>
<p>Проверьте:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> demo-wt-hotfix
</span></span><span class="line"><span class="cl">git status            <span class="c1"># on branch hotfix/login, clean</span>
</span></span><span class="line"><span class="cl">ls                    <span class="c1"># app.txt — копия файлов main, без profile.js</span>
</span></span></code></pre></div><h3 id="шаг-3-делаем-hotfix-параллельно">Шаг 3. Делаем hotfix параллельно</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># пишем hotfix в соседнем каталоге</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;v1 + hotfix&#34;</span> &gt; app.txt
</span></span><span class="line"><span class="cl">git add . <span class="o">&amp;&amp;</span> git commit -q -m <span class="s2">&#34;fix: urgent login&#34;</span>
</span></span><span class="line"><span class="cl">git push -q 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span>   <span class="c1"># если бы был remote</span>
</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"><span class="nb">cd</span> ../demo-wt
</span></span><span class="line"><span class="cl">git status            <span class="c1"># всё ещё feature/profile с грязным profile.js</span>
</span></span><span class="line"><span class="cl">cat profile.js        <span class="c1"># содержимое сохранилось</span>
</span></span></code></pre></div><p>Мы только что сделали hotfix, не трогая feature-ветку и не теряя WIP-правки. Оба каталога — валидные git-репозитории на один <code>.git/</code>.</p>
<h3 id="шаг-4-убираем-worktree-когда-закончили">Шаг 4. Убираем worktree, когда закончили</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">git -C demo-wt worktree list
</span></span><span class="line"><span class="cl"><span class="c1"># /path/demo-wt          &lt;sha&gt; [feature/profile]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /path/demo-wt-hotfix   &lt;sha&gt; [hotfix/login]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">git -C demo-wt worktree remove ./demo-wt-hotfix
</span></span><span class="line"><span class="cl">git -C demo-wt worktree list
</span></span><span class="line"><span class="cl"><span class="c1"># остался только главный</span>
</span></span></code></pre></div><p><code>worktree remove</code> требует <strong>чистое дерево</strong> (как и любой безопасный git). Если там грязь — закоммитьте или <code>remove --force</code>.</p>
<hr>
<h2 id="практика-2-review-чужого-pr-в-отдельном-каталоге">Практика 2: review чужого PR в отдельном каталоге</h2>
<p>Типичная задача — быстро прокликать чей-то PR, запустить его тесты, сходить в код, и вернуться к своей работе. Без worktree — опять stash-дэнс.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> demo-wt
</span></span><span class="line"><span class="cl"><span class="c1"># создаём worktree, привязанный к ветке review (имитация чужого PR)</span>
</span></span><span class="line"><span class="cl">git checkout -q -b other-pr main
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;// PR content&#34;</span> &gt; pr.txt <span class="o">&amp;&amp;</span> git add . <span class="o">&amp;&amp;</span> git commit -q -m <span class="s2">&#34;feat: pr content&#34;</span>
</span></span><span class="line"><span class="cl">git checkout -q feature/profile
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">git -C demo-wt worktree add ./demo-wt-review other-pr
</span></span></code></pre></div><p>Теперь:</p>
<ul>
<li>в <code>demo-wt/</code> вы на <code>feature/profile</code></li>
<li>в <code>demo-wt-review/</code> — код ветки <code>other-pr</code>, можно открыть в отдельной IDE-вкладке, запустить тесты, посмотреть diff</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> demo-wt-review
</span></span><span class="line"><span class="cl">ls                    <span class="c1"># виден pr.txt</span>
</span></span><span class="line"><span class="cl">git log --oneline -1
</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"><span class="nb">cd</span> ..
</span></span><span class="line"><span class="cl">git -C demo-wt worktree remove ./demo-wt-review
</span></span></code></pre></div><p><strong>Частая ошибка:</strong> попытка добавить worktree с уже checked-out веткой:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git -C demo-wt worktree add ./another feature/profile
</span></span><span class="line"><span class="cl"><span class="c1"># fatal: &#39;feature/profile&#39; is already checked out at &#39;/path/demo-wt&#39;</span>
</span></span></code></pre></div><p>Решения: новая ветка (<code>-b name</code>), <code>--detach</code> (анонимный HEAD на том же коммите), либо <code>--force</code> (рискованно — index начнёт расходиться, Git не гарантирует согласованность).</p>
<hr>
<h2 id="практика-3-worktree-prune-и-зачем-он-нужен">Практика 3: <code>worktree prune</code> и зачем он нужен</h2>
<p>Если вы удалите каталог worktree <strong>руками</strong> (без <code>git worktree remove</code>), в <code>.git/worktrees/</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="nb">cd</span> demo-wt
</span></span><span class="line"><span class="cl">git worktree add ../tmp-wt -b throwaway
</span></span><span class="line"><span class="cl">rm -rf ../tmp-wt                       <span class="c1"># удалили каталог напрямую</span>
</span></span><span class="line"><span class="cl">git worktree list                      <span class="c1"># запись всё ещё показывается, но &#34;prunable&#34;</span>
</span></span><span class="line"><span class="cl">git worktree prune --verbose           <span class="c1"># чистит осиротевшие</span>
</span></span><span class="line"><span class="cl">git worktree list                      <span class="c1"># остался только main</span>
</span></span></code></pre></div><p><code>worktree prune</code> — garbage collector для worktree-ссылок. Запускается автоматически при <code>git gc</code>, вручную нужен когда вы хотите немедленно освободить имена веток и слоты.</p>
<h3 id="когда-нужен-worktree-lock">Когда нужен <code>worktree lock</code></h3>
<p>Если ваше дерево живёт на съёмном диске или в сетевом каталоге, который иногда отключается, Git может посчитать его «prunable» в момент недоступности. Защита:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add ../external-wt -b research
</span></span><span class="line"><span class="cl">git worktree lock ../external-wt --reason <span class="s2">&#34;research на внешнем HDD&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># теперь prune его не тронет, даже если каталог временно пропал</span>
</span></span><span class="line"><span class="cl">git worktree unlock ../external-wt     <span class="c1"># когда хотите снять защиту</span>
</span></span></code></pre></div><hr>
<h2 id="артефакт-алиасы-и-конвенция-каталогов">Артефакт: алиасы и конвенция каталогов</h2>
<p>Конвенция, которую удобно держать во всех репозиториях:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">~/code/
</span></span><span class="line"><span class="cl">├── myrepo/                  ← главный каталог, в нём обычно main или текущая feature
</span></span><span class="line"><span class="cl">├── myrepo-hotfix/           ← worktree для срочных фиксов
</span></span><span class="line"><span class="cl">└── myrepo-review/           ← worktree для чужих PR
</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"># короткие worktree-операции</span>
</span></span><span class="line"><span class="cl">    <span class="na">wt</span>  <span class="o">=</span> <span class="s">worktree
</span></span></span><span class="line"><span class="cl"><span class="s">    wta = worktree add
</span></span></span><span class="line"><span class="cl"><span class="s">    wtl = worktree list
</span></span></span><span class="line"><span class="cl"><span class="s">    wtr = worktree remove</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># добавить worktree рядом с текущим репо: git wth hotfix/login main</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># создаст ../$(basename pwd)-&lt;ветка&gt;/</span>
</span></span><span class="line"><span class="cl">    <span class="na">wth</span> <span class="o">=</span> <span class="s">&#34;!f() { \
</span></span></span><span class="line"><span class="cl"><span class="s">      repo=$(basename $(pwd)); \
</span></span></span><span class="line"><span class="cl"><span class="s">      branch=$1; base=${2:-main}; \
</span></span></span><span class="line"><span class="cl"><span class="s">      short=$(echo $branch | tr &#39;/&#39; &#39;-&#39;); \
</span></span></span><span class="line"><span class="cl"><span class="s">      git worktree add \&#34;../${repo}-${short}\&#34; -b \&#34;$branch\&#34; \&#34;$base\&#34;; \
</span></span></span><span class="line"><span class="cl"><span class="s">    }; 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"><span class="nb">cd</span> ~/code/myrepo
</span></span><span class="line"><span class="cl">git wth hotfix/login             <span class="c1"># создаст ~/code/myrepo-hotfix-login с веткой hotfix/login от main</span>
</span></span><span class="line"><span class="cl">git wth review/pr-42 origin/main <span class="c1"># review чужого PR от текущего main на remote</span>
</span></span><span class="line"><span class="cl">git wtl                          <span class="c1"># посмотреть все активные</span>
</span></span><span class="line"><span class="cl">git wtr ../myrepo-hotfix-login   <span class="c1"># убрать когда закончили</span>
</span></span></code></pre></div><hr>
<h2 id="когда-не-нужен-worktree">Когда НЕ нужен worktree</h2>
<ul>
<li>Проект маленький, ветки короткие, <code>stash</code> справляется → не плодите каталоги.</li>
<li>Сборка тяжёлая и требует каталог-специфичного <code>node_modules</code> / <code>target/</code> на каждый worktree — диск съест. В таком случае иногда выгоднее один каталог + <code>stash</code>.</li>
<li>CI-контейнер, который клонирует репо в чистый каталог на каждый запуск — worktree там не нужен.</li>
</ul>
<p>Правило: worktree хорош для <strong>долгоживущих параллельных контекстов локально</strong>. Если контекст short-lived и лёгкий — <code>stash</code> проще.</p>
<hr>
<h2 id="-документирование">📝 Документирование</h2>
<p>Напишите в <code>NOTES.md</code> учебного репозитория:</p>
<ol>
<li><strong>Своими словами</strong>: что общего у двух worktree и что своё у каждого.</li>
<li><strong>Пример из вашей работы</strong> — случай, где worktree сэкономил бы переключение: какая была грязная ветка, куда пришлось бы stash&rsquo;ить, сколько времени ушло.</li>
<li><strong>Когда НЕ стоит</strong> — одна причина против worktree из вашего контекста.</li>
<li><strong>Ваша конвенция каталогов</strong> — как вы назовёте hotfix / review worktrees в своих проектах.</li>
</ol>
<hr>
<h2 id="мини-тест">Мини-тест</h2>
<ol>
<li>Вы на <code>feature/X</code>, index грязный. Что произойдёт, если сделать <code>git worktree add ../hot feature/X</code>? Какой флаг позволит обойти это?</li>
<li>Где физически живут <code>HEAD</code> и <code>index</code> второго worktree?</li>
<li>Что такое «prunable» worktree и когда нужен <code>worktree prune</code>?</li>
<li>Зачем <code>worktree lock</code> и в каком сценарии без него вы потеряете ссылку?</li>
</ol>
<p>Ответы — в конце.</p>
<hr>
<h2 id="что-дальше">Что дальше</h2>
<ul>
<li><strong><a href="/posts/git-master-final-challenge/">Challenge</a></strong> → сломанный репозиторий с 10 проблемами. Задачи P9 и P10 — параллельный hotfix + feature и prune осиротевших worktree-записей.</li>
<li><strong>Системно с нуля</strong> → «Курс молодого бойца» DevIT Academy — git в рабочем потоке, не как набор команд.</li>
</ul>
<hr>
<h2 id="ответы-на-мини-тест">Ответы на мини-тест</h2>
<ol>
<li>Git откажется: <code>'feature/X' is already checked out at &lt;path&gt;</code>. Обойти — новой веткой (<code>-b feature/X-parallel</code>), <code>--detach</code> (анонимный HEAD на том же коммите без ветки) или <code>--force</code> (не рекомендуется — index обоих деревьев может разойтись).</li>
<li>В <code>.git/worktrees/&lt;name&gt;/</code> главной репы. Там лежат <code>HEAD</code>, <code>index</code>, <code>gitdir</code> (указатель на путь к каталогу дерева). Объекты и refs — общие, в главном <code>.git/</code>.</li>
<li>«Prunable» — worktree, каталог которого уже удалён на диске, но запись в <code>.git/worktrees/</code> осталась. <code>worktree prune</code> удаляет такие записи, освобождая имена веток и слоты. Запускается вручную или автоматически при <code>git gc</code>.</li>
<li>Если worktree на съёмном диске или сетевом каталоге — в момент отключения Git сочтёт его prunable и может подчистить. <code>worktree lock</code> говорит «этот worktree не мёртв, не трогать», даже если сейчас недоступен.</li>
</ol>
]]></content:encoded>
    </item>
  </channel>
</rss>
