<?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>workflow · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/workflow/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Fri, 03 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/workflow/feed.xml" rel="self" type="application/rss+xml"/><item><title>Makefiles Are Not Just for C</title><link>https://articles.wleeaf.dev/posts/makefiles-beyond-c/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/makefiles-beyond-c/</guid><description>Using Make as a project task runner when npm scripts and shell aliases fall short.</description><category>make</category><category>automation</category><category>workflow</category><content:encoded><![CDATA[<p>Every project accumulates a collection of commands. Build this, test that, deploy here, clean up there. Most people throw these into npm scripts, a shell script, or a README that nobody reads. I put them in a Makefile.</p>
<p>Not because I write C. Because Make is a task runner that is already installed on every machine.</p>
<h2 id="the-basics">The Basics<a class="anchor" href="#the-basics" aria-label="Link to “The Basics”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">build</span> <span class="n">test</span> <span class="n">deploy</span> <span class="n">clean</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose run --rm app npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">	rsync -a --delete ./dist/ server:/opt/app/
</span></span><span class="line"><span class="cl">	ssh server <span class="s2">&#34;cd /opt/app &amp;&amp; docker compose up -d&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">clean</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose down -v
</span></span><span class="line"><span class="cl">	rm -rf dist node_modules
</span></span></code></pre></div>
</div>
<p>Now:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make build
</span></span><span class="line"><span class="cl">make <span class="nb">test</span>
</span></span><span class="line"><span class="cl">make deploy
</span></span><span class="line"><span class="cl">make clean</span></span></code></pre></div>
</div>
<p><code>.PHONY</code> tells Make these targets are commands, not files. Without it, if you have a directory called <code>build</code>, Make would say &ldquo;build is up to date&rdquo; and do nothing.</p>
<h2 id="dependencies">Dependencies<a class="anchor" href="#dependencies" aria-label="Link to “Dependencies”">#</a></h2>
<p>The power of Make is in the dependency graph. <code>deploy</code> depends on <code>build</code>:</p>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">	rsync -a --delete ./dist/ server:/opt/app/
</span></span></code></pre></div>
</div>
<p>When you run <code>make deploy</code>, it runs <code>build</code> first. If <code>build</code> fails, <code>deploy</code> never runs. This is what npm scripts cannot do cleanly.</p>
<h2 id="variables">Variables<a class="anchor" href="#variables" aria-label="Link to “Variables”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">SERVER</span> <span class="o">:=</span> myserver
</span></span><span class="line"><span class="cl"><span class="nv">REMOTE_DIR</span> <span class="o">:=</span> /opt/app
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span> <span class="o">:=</span> myapp:latest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker build -t <span class="k">$(</span>IMAGE<span class="k">)</span> .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">push</span><span class="o">:</span> <span class="n">build</span>
</span></span><span class="line"><span class="cl">	docker push <span class="k">$(</span>IMAGE<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">push</span>
</span></span><span class="line"><span class="cl">	ssh <span class="k">$(</span>SERVER<span class="k">)</span> <span class="s2">&#34;cd </span><span class="k">$(</span>REMOTE_DIR<span class="k">)</span><span class="s2"> &amp;&amp; docker pull </span><span class="k">$(</span>IMAGE<span class="k">)</span><span class="s2"> &amp;&amp; docker compose up -d&#34;</span>
</span></span></code></pre></div>
</div>
<p>Override from the command line:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make deploy <span class="nv">SERVER</span><span class="o">=</span>staging-server</span></span></code></pre></div>
</div>
<h2 id="multi-stage-builds-with-file-targets">Multi-Stage Builds With File Targets<a class="anchor" href="#multi-stage-builds-with-file-targets" aria-label="Link to “Multi-Stage Builds With File Targets”">#</a></h2>
<p>Make tracks file timestamps. This is useful for caching:</p>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">node_modules</span><span class="o">:</span> <span class="n">package</span>.<span class="n">json</span> <span class="n">package</span>-<span class="n">lock</span>.<span class="n">json</span>
</span></span><span class="line"><span class="cl">	npm ci
</span></span><span class="line"><span class="cl">	touch node_modules
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dist</span><span class="o">:</span> <span class="n">node_modules</span> <span class="k">$(</span><span class="nv">shell</span> <span class="nv">find</span> <span class="nv">src</span> -<span class="nv">type</span> <span class="nv">f</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">	npm run build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">serve</span>
</span></span><span class="line"><span class="cl"><span class="nf">serve</span><span class="o">:</span> <span class="n">dist</span>
</span></span><span class="line"><span class="cl">	python3 -m http.server <span class="m">8080</span> -d dist
</span></span></code></pre></div>
</div>
<p><code>node_modules</code> only rebuilds if <code>package.json</code> or <code>package-lock.json</code> changed. <code>dist</code> only rebuilds if source files changed. Make compares file timestamps automatically.</p>
<h2 id="help-target">Help Target<a class="anchor" href="#help-target" aria-label="Link to “Help Target”">#</a></h2>
<p>Add a self-documenting help target:</p>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">.DEFAULT_GOAL</span> <span class="o">:=</span> <span class="nb">help</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">help</span><span class="o">:</span> <span class="c">## Show this help
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	@grep -E <span class="s1">&#39;^[a-zA-Z_-]+:.*?## .*$$&#39;</span> <span class="k">$(</span>MAKEFILE_LIST<span class="k">)</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>		awk <span class="s1">&#39;BEGIN {FS = &#34;:.*?## &#34;}; {printf &#34;  \033[36m%-15s\033[0m %s\n&#34;, $$1, $$2}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span> <span class="c">## Build the Docker image
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose build
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span> <span class="c">## Run the test suite
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose run --rm app npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="c">## Deploy to production
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	./deploy.sh
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">clean</span><span class="o">:</span> <span class="c">## Remove all build artifacts
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	rm -rf dist node_modules
</span></span></code></pre></div>
</div>
<p>Now <code>make</code> with no arguments prints:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>  build           Build the Docker image
  test            Run the test suite
  deploy          Deploy to production
  clean           Remove all build artifacts</code></pre>
</div>
<h2 id="environment-specific-configs">Environment-Specific Configs<a class="anchor" href="#environment-specific-configs" aria-label="Link to “Environment-Specific Configs”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">ENV</span> <span class="o">?=</span> development
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="err">ifeq</span> <span class="err">(</span><span class="k">$(</span><span class="nv">ENV</span><span class="k">)</span><span class="err">,production)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">COMPOSE_FILE</span> <span class="o">:=</span> docker-compose.yml -f docker-compose.prod.yml
</span></span><span class="line"><span class="cl"><span class="err">else</span>
</span></span><span class="line"><span class="cl">  <span class="nv">COMPOSE_FILE</span> <span class="o">:=</span> docker-compose.yml
</span></span><span class="line"><span class="cl"><span class="err">endif</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">up</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	docker compose <span class="k">$(</span>addprefix -f ,<span class="k">$(</span>COMPOSE_FILE<span class="k">))</span> up -d
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Usage:
</span></span></span><span class="line"><span class="cl"><span class="c"># make up           → development
</span></span></span><span class="line"><span class="cl"><span class="c"># make up ENV=production  → production
</span></span></span></code></pre></div>
</div>
<h2 id="parallel-execution">Parallel Execution<a class="anchor" href="#parallel-execution" aria-label="Link to “Parallel Execution”">#</a></h2>
<p>Make can run independent targets in parallel:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make -j4 lint <span class="nb">test</span> typecheck</span></span></code></pre></div>
</div>
<p>As long as these targets do not depend on each other, Make runs all four simultaneously. This is free parallelism with no extra scripting.</p>
<h2 id="a-real-project-makefile">A Real Project Makefile<a class="anchor" href="#a-real-project-makefile" aria-label="Link to “A Real Project Makefile”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">makefile</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">help</span> <span class="n">dev</span> <span class="n">build</span> <span class="n">test</span> <span class="n">lint</span> <span class="n">deploy</span> <span class="n">clean</span>
</span></span><span class="line"><span class="cl"><span class="nv">.DEFAULT_GOAL</span> <span class="o">:=</span> <span class="nb">help</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span> <span class="o">:=</span> myapp
</span></span><span class="line"><span class="cl"><span class="nv">TAG</span> <span class="o">:=</span> <span class="k">$(</span>shell git rev-parse --short HEAD<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">help</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">	@grep -E <span class="s1">&#39;^[a-zA-Z_-]+:.*?## .*$$&#39;</span> <span class="k">$(</span>MAKEFILE_LIST<span class="k">)</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>		awk <span class="s1">&#39;BEGIN {FS = &#34;:.*?## &#34;}; {printf &#34;  \033[36m%-15s\033[0m %s\n&#34;, $$1, $$2}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dev</span><span class="o">:</span> <span class="c">## Start development server
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose up -d
</span></span><span class="line"><span class="cl">	docker compose logs -f app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">build</span><span class="o">:</span> <span class="c">## Build production image
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker build -t <span class="k">$(</span>IMAGE<span class="k">)</span>:<span class="k">$(</span>TAG<span class="k">)</span> -t <span class="k">$(</span>IMAGE<span class="k">)</span>:latest .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span> <span class="c">## Run tests
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose run --rm app npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">lint</span><span class="o">:</span> <span class="c">## Run linter
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose run --rm app npm run lint
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">deploy</span><span class="o">:</span> <span class="n">build</span> <span class="c">## Deploy to production
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker push <span class="k">$(</span>IMAGE<span class="k">)</span>:<span class="k">$(</span>TAG<span class="k">)</span>
</span></span><span class="line"><span class="cl">	ssh server <span class="s2">&#34;docker pull </span><span class="k">$(</span>IMAGE<span class="k">)</span><span class="s2">:</span><span class="k">$(</span>TAG<span class="k">)</span><span class="s2"> &amp;&amp; docker compose up -d&#34;</span>
</span></span><span class="line"><span class="cl">	@echo <span class="s2">&#34;Deployed </span><span class="k">$(</span>TAG<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">clean</span><span class="o">:</span> <span class="c">## Clean up everything
</span></span></span><span class="line"><span class="cl"><span class="c"></span>	docker compose down -v
</span></span><span class="line"><span class="cl">	docker image prune -f
</span></span></code></pre></div>
</div>
<p>The Makefile is not trendy. It does not have a package ecosystem or a plugin system. But it works everywhere, it handles dependencies, it supports parallelism, and it does not require installing anything. For project automation, that is hard to beat.</p>
]]></content:encoded></item><item><title>Git Worktrees Changed How I Handle Branches</title><link>https://articles.wleeaf.dev/posts/git-worktrees/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/git-worktrees/</guid><description>Switching branches without stashing, losing context, or waiting for node_modules to reinstall.</description><category>git</category><category>workflow</category><category>productivity</category><content:encoded><![CDATA[<p>The worst part of working on multiple branches is the context switching. You are deep in a feature branch, someone asks you to review a hotfix, and now you have to stash your changes, switch branches, wait for your build tools to catch up, do the review, switch back, pop the stash, and hope nothing got lost.</p>
<p>Git worktrees fix this entirely: they let you check out several branches at once, each in its own directory, all sharing one repository.</p>
<h2 id="what-is-a-worktree">What Is a Worktree?<a class="anchor" href="#what-is-a-worktree" aria-label="Link to “What Is a Worktree?”">#</a></h2>
<p>A worktree is a separate working directory that shares the same <code>.git</code> data. Each worktree can have a different branch checked out simultaneously:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># You are on main in /home/user/project</span>
</span></span><span class="line"><span class="cl">git worktree add ../project-hotfix hotfix/urgent-fix</span></span></code></pre></div>
</div>
<p>Now you have two directories:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>/home/user/project          → main branch
/home/user/project-hotfix   → hotfix/urgent-fix branch</code></pre>
</div>
<p>Both share the same git history. Commits in one are visible in the other. But they have completely independent working directories.</p>
<h2 id="the-workflow">The Workflow<a class="anchor" href="#the-workflow" aria-label="Link to “The Workflow”">#</a></h2>
<p>My typical day looks like this:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Main work</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/projects/app
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Someone needs a review - create a worktree</span>
</span></span><span class="line"><span class="cl">git worktree add ~/reviews/pr-142 origin/feature/new-api
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Review in a completely separate directory</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/reviews/pr-142
</span></span><span class="line"><span class="cl">npm install  <span class="c1"># only needed first time</span>
</span></span><span class="line"><span class="cl">npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Done reviewing, remove it</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/projects/app
</span></span><span class="line"><span class="cl">git worktree remove ~/reviews/pr-142</span></span></code></pre></div>
</div>
<p>The key advantage: my main working directory is completely untouched. No stashing, no branch switching, no reinstalling dependencies.</p>
<h2 id="practical-patterns">Practical Patterns<a class="anchor" href="#practical-patterns" aria-label="Link to “Practical Patterns”">#</a></h2>
<h3 id="parallel-testing">Parallel testing<a class="anchor" href="#parallel-testing" aria-label="Link to “Parallel testing”">#</a></h3>
<p>Run tests on two branches at the same time:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add /tmp/test-main main
</span></span><span class="line"><span class="cl">git worktree add /tmp/test-feature feature/new-thing
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># In two terminals</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp/test-main <span class="o">&amp;&amp;</span> npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp/test-feature <span class="o">&amp;&amp;</span> npm test</span></span></code></pre></div>
</div>
<h3 id="long-running-branches">Long-running branches<a class="anchor" href="#long-running-branches" aria-label="Link to “Long-running branches”">#</a></h3>
<p>If you maintain a staging branch alongside main:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add ~/staging staging</span></span></code></pre></div>
</div>
<p>Keep it around permanently. Push to staging by working in that directory. No branch switching ever.</p>
<h3 id="ci-simulation">CI simulation<a class="anchor" href="#ci-simulation" aria-label="Link to “CI simulation”">#</a></h3>
<p>Test your CI pipeline locally against a clean checkout:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add /tmp/ci-test HEAD
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /tmp/ci-test
</span></span><span class="line"><span class="cl">npm ci
</span></span><span class="line"><span class="cl">npm run lint
</span></span><span class="line"><span class="cl">npm <span class="nb">test</span>
</span></span><span class="line"><span class="cl">npm run build</span></span></code></pre></div>
</div>
<p>This catches the classic &ldquo;works on my machine&rdquo; issues where you have uncommitted files or stale build artifacts that mask problems.</p>
<h2 id="managing-worktrees">Managing Worktrees<a class="anchor" href="#managing-worktrees" aria-label="Link to “Managing Worktrees”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># List all worktrees</span>
</span></span><span class="line"><span class="cl">git worktree list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Output:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /home/user/project         abc1234 [main]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /home/user/project-hotfix  def5678 [hotfix/urgent-fix]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /tmp/ci-test               abc1234 (detached HEAD)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Remove a worktree</span>
</span></span><span class="line"><span class="cl">git worktree remove /home/user/project-hotfix
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clean up stale worktree references</span>
</span></span><span class="line"><span class="cl">git worktree prune</span></span></code></pre></div>
</div>
<h2 id="gotchas">Gotchas<a class="anchor" href="#gotchas" aria-label="Link to “Gotchas”">#</a></h2>
<p>You cannot check out the same branch in two worktrees. Git prevents this because having the same branch in two places would make it ambiguous which working directory a commit applies to.</p>
<p>If you need to work on the same branch in two places, create a local tracking branch:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git worktree add ../experiment experiment-copy
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ../experiment
</span></span><span class="line"><span class="cl">git checkout -b experiment-local origin/experiment</span></span></code></pre></div>
</div>
<p>The other thing to watch out for is node_modules. Each worktree needs its own <code>node_modules</code> because paths are different. The first <code>npm install</code> in a new worktree takes time. After that, it is fast because the npm cache is shared.</p>
<h2 id="vs-git-stash">vs. git stash<a class="anchor" href="#vs-git-stash" aria-label="Link to “vs. git stash”">#</a></h2>
<p>Stash is fine for quick interruptions. If someone asks you a question and you need to check something on another branch for thirty seconds, stash works.</p>
<p>But for anything that takes more than a minute, worktrees are better. They do not mess with your working directory, they let you have multiple contexts open in different editor windows, and they do not have the failure mode where you forget to pop the stash and lose track of what you were doing.</p>
<p>I keep a <code>~/worktrees/</code> directory for temporary ones and let them accumulate during the week. Friday afternoon I run <code>git worktree list</code> and clean up anything I am done with.</p>
]]></content:encoded></item></channel></rss>