<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://haacked.com/atom.xml" rel="self" type="application/atom+xml" /><link href="https://haacked.com/" rel="alternate" type="text/html" /><updated>2026-03-26T18:25:05+00:00</updated><id>https://haacked.com/atom.xml</id><title type="html">You’ve Been Haacked</title><subtitle>You&apos;ve been Haacked is a blog about Technology, Software, Management, and Open Source. It&apos;s full of good stuff.
</subtitle><author><name>Phil Haack</name></author><entry><title type="html">Resolve Merge Conflicts the Easy Way</title><link href="https://haacked.com/archive/2026/03/25/resolve-merge-conflicts/" rel="alternate" type="text/html" title="Resolve Merge Conflicts the Easy Way" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://haacked.com/archive/2026/03/25/resolve-merge-conflicts</id><content type="html" xml:base="https://haacked.com/archive/2026/03/25/resolve-merge-conflicts/"><![CDATA[<p>Git is great at merging until it isn’t. Most of the time, when I rebase my feature branch against the main branch, it all goes to plan. Nothing to do for me. But when it doesn’t go to plan, it can be a big mess. Git dumps a wall of conflict markers on you. You resolve those, continue the rebase, and the next commit has conflicts too. Depending on the scope of changes, resolving merge conflicts can be a very tedious chore. The temptation to <code class="language-plaintext highlighter-rouge">git rebase --abort</code> and pretend this never happened is overwhelming.</p>

<p>It turns out, we have some great tools now for dealing with tedious chores. In particular, I’ve set up two tools that turned merge conflicts from a dreaded chore into a minor speed bump. Most of the time, they resolve themselves before I even see them. For the ones that don’t, automation handles the tedious parts so I only deal with the genuinely ambiguous cases.</p>

<p><img src="https://i.haacked.com/blog/2026-03-25-resolve-merge-conflicts/merge-conflict-resolution.png" alt="A friendly robot referee untangling two git branches" /></p>

<h2 id="the-problem-with-textual-merging">The Problem with Textual Merging</h2>

<p>Git’s built-in merge is purely textual. It compares lines of text and looks for overlapping changes. It doesn’t understand your code. It doesn’t know what a function is, or an import statement, or a class definition. It just sees lines.</p>

<p>This means Git reports conflicts that aren’t actually conflicts. Two developers add different imports to the same file, near the same spot. Git sees overlapping line changes and panics:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
=======
import { useState } from 'react';
import { useEffect } from 'react';
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature/dashboard
</code></pre></div></div>

<p>A human can see instantly that both changes are independent additions. One added <code class="language-plaintext highlighter-rouge">useQuery</code>, the other added <code class="language-plaintext highlighter-rouge">useEffect</code>. The correct resolution is to keep all three imports. But Git can’t see that because Git doesn’t understand syntax. It only sees text.</p>

<p>These false conflicts add up. On a large rebase, they can turn a five-minute task into a thirty-minute slog.</p>

<h2 id="layer-1-mergiraf">Layer 1: Mergiraf</h2>

<p><a href="https://mergiraf.org/">Mergiraf</a> is a structural merge driver for Git. Instead of comparing lines of text, it parses your files using language grammars and merges at the syntax tree level. If two changes touch different parts of the syntax tree, it merges them cleanly. If they genuinely conflict at the structural level, it falls back to standard conflict markers.</p>

<p>That import example above? Mergiraf resolves it automatically. It understands that those are independent additions to an import list and combines them.</p>

<p>Mergiraf supports over 25 languages: Java, Rust, Go, Python, JavaScript, TypeScript, C, C++, C#, Ruby, Elixir, PHP, Dart, Scala, Haskell, OCaml, Lua, Nix, YAML, TOML, HTML, XML, and more. For file types it doesn’t support, it returns a non-zero exit code and Git falls back to its default textual merge. No harm done.</p>

<h3 id="setup">Setup</h3>

<p>Three steps.</p>

<p><strong>1. Install mergiraf:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>mergiraf
</code></pre></div></div>

<p><strong>2. Register the merge driver in your <code class="language-plaintext highlighter-rouge">~/.gitconfig</code>:</strong></p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[merge "mergiraf"]</span>
    <span class="py">name</span> <span class="p">=</span> <span class="s">mergiraf</span>
    <span class="py">driver</span> <span class="p">=</span> <span class="s">mergiraf merge --git %O %A %B -s %S -x %X -y %Y -p %P -l %L</span>
</code></pre></div></div>

<p><strong>3. Apply it globally in <code class="language-plaintext highlighter-rouge">~/.config/git/attributes</code>:</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* merge=mergiraf
</code></pre></div></div>

<p>The wildcard (<code class="language-plaintext highlighter-rouge">*</code>) tells Git to run every file through mergiraf. This might sound aggressive, but it’s fine. If mergiraf doesn’t recognize the file type, it steps aside and Git handles it normally.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Note</p><p>If you use my <a href="https://github.com/haacked/dotfiles">dotfiles</a>, the <code class="language-plaintext highlighter-rouge">git/install.sh</code> script creates the attributes file for you. Run it once and you’re done.</p>
</div>

<h3 id="companion-settings">Companion Settings</h3>

<p>Two additional Git settings complement mergiraf nicely.</p>

<p><strong>diff3 conflict style</strong>: By default, Git’s conflict markers only show your version and their version. With <code class="language-plaintext highlighter-rouge">diff3</code>, you also see the common ancestor (the “base”). This gives both mergiraf and humans more context to resolve conflicts correctly.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[merge]</span>
    <span class="py">conflictStyle</span> <span class="p">=</span> <span class="s">diff3</span>
</code></pre></div></div>

<p>Here’s the difference. Standard conflict markers:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
const timeout = 5000;
=======
const timeout = 10000;
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature
</code></pre></div></div>

<p>With diff3:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
const timeout = 5000;
||||||| base
const timeout = 3000;
=======
const timeout = 10000;
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature
</code></pre></div></div>

<p>The base section tells you the original value was 3000. Now you can see that HEAD changed it to 5000 and the feature branch changed it to 10000. Without the base, you’re guessing.</p>

<p><strong>rerere</strong> (reuse recorded resolution): Rerere records how you resolve conflicts and automatically replays those resolutions if the same conflict comes up again. This is useful during rebases where you might encounter the same conflict multiple times.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[rerere]</span>
    <span class="py">enabled</span> <span class="p">=</span> <span class="s">true</span>
</code></pre></div></div>

<h2 id="layer-2-automating-the-rest">Layer 2: Automating the Rest</h2>

<p>Mergiraf handles the structural conflicts, but some conflicts are genuinely ambiguous. And some aren’t ambiguous at all, they’re just tedious. Lock files, database migrations, stacked PR duplicates. Each of these has a clear resolution strategy, but you still have to do the work manually.</p>

<p>This is drudgery. Drudgery that follows clear rules. Perfect for automation.</p>

<p>I built a <a href="https://github.com/haacked/dotfiles/tree/main/ai/skills/resolve-conflicts">Claude Code skill</a> called <code class="language-plaintext highlighter-rouge">/resolve-conflicts</code> that handles the entire conflict resolution workflow. Type <code class="language-plaintext highlighter-rouge">/resolve-conflicts</code> and it takes over.</p>

<h3 id="how-it-works">How It Works</h3>

<p>The skill follows a three-step loop:</p>

<ol>
  <li>
    <p><strong>Detect context.</strong> It reads Git’s internal state files to determine whether you’re in a rebase, merge, cherry-pick, or revert, and how far along you are (e.g., step 3 of 12 in a rebase).</p>
  </li>
  <li>
    <p><strong>Categorize and resolve.</strong> It runs a categorization script on every conflicted file, sorting them into buckets: lock file, migration, mergiraf-supported, or other. Then it resolves each bucket with the appropriate strategy.</p>
  </li>
  <li>
    <p><strong>Continue.</strong> It regenerates any lock files, runs the appropriate continue command (<code class="language-plaintext highlighter-rouge">git rebase --continue</code>, <code class="language-plaintext highlighter-rouge">git commit --no-edit</code>, etc.), and loops back to step 1 if more conflicts appear.</p>
  </li>
</ol>

<h3 id="resolution-strategies">Resolution Strategies</h3>

<p>Each category gets its own treatment:</p>

<p><strong>Lock files</strong>: Accept theirs to clear the conflict markers, then regenerate. The content of a lock file is derived from the dependency manifest, so there’s no point resolving individual lines. The skill runs the appropriate package manager (<code class="language-plaintext highlighter-rouge">npm install</code>, <code class="language-plaintext highlighter-rouge">cargo generate-lockfile</code>, <code class="language-plaintext highlighter-rouge">poetry lock --no-update</code>, etc.) to produce a correct lock file from the resolved manifest.</p>

<p><strong>Migrations</strong>: Ask the human. Migration files represent sequential schema changes where order matters. Getting this wrong can break your database. The skill flags these and asks you how to proceed.</p>

<p><strong>Mergiraf files</strong>: Run mergiraf as a second pass. Even though mergiraf already ran as a merge driver during the git operation, sometimes conflicts remain (partial resolutions, complex restructuring). The skill runs <code class="language-plaintext highlighter-rouge">mergiraf solve</code> on the file. If conflict markers remain after that, it falls through to AI analysis.</p>

<p><strong>Everything else</strong>: Read the conflict, analyze both sides, and resolve it. This is where the skill earns its keep on the genuinely tricky ones.</p>

<h3 id="stacked-pr-duplicate-detection">Stacked PR Duplicate Detection</h3>

<p>If you work with stacked PRs, you’ve probably hit this one. You have a feature branch with sub-PRs stacked on top of each other. You merge a sub-PR into main. Now when you rebase the parent branch, Git produces conflicts where both sides have nearly identical code and the base section is empty.</p>

<p>Here’s what that looks like with diff3:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
function calculateTotal(items) {
  return items.reduce((sum, item) =&gt; sum + item.price, 0);
}
||||||| base
=======
function calculateTotal(items) {
  return items.reduce((sum, item) =&gt; sum + item.price, 0);
}
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature/add-checkout
</code></pre></div></div>

<p>Empty base. Both sides identical. This isn’t a real conflict. It’s just Git seeing that code appeared on both sides independently (once from the merged sub-PR, once from the feature branch that originally authored it).</p>

<p>The skill detects this pattern. When the base is empty but HEAD and the incoming side are more than 95% similar (after normalizing whitespace), it auto-resolves by keeping the HEAD version and tells you what it did. For 70-95% similarity, it shows both versions and asks you to confirm. Below 70%, it’s a genuine divergence and presents both options for you to decide.</p>

<h3 id="a-realistic-session">A Realistic Session</h3>

<p>Here’s what it looks like in practice. You’re rebasing and hit conflicts:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; /resolve-conflicts

Conflicts (rebase step 3/12):
- 1 lockfile: package-lock.json
- 2 mergiraf: src/app.ts, src/utils.ts
- 1 other: README.md

Resolving lockfile: package-lock.json... accepted theirs (will regenerate)
Resolving mergiraf: src/app.ts... resolved structurally
Resolving mergiraf: src/utils.ts... 1 conflict remains, analyzing...
  Auto-resolved src/utils.ts hunk at line 42: stacked PR duplicate
  (HEAD and incoming 98% similar with empty base). Kept HEAD version.
Resolving other: README.md... [presents conflict for user review]

Regenerating package-lock.json... done
Continuing rebase (step 4/12)...
</code></pre></div></div>

<p>If step 4 has conflicts, it resolves those too. All the way through step 12. You just watch it work and only chime in when it needs a human decision.</p>

<h3 id="setup-1">Setup</h3>

<p>The skill lives in my <a href="https://github.com/haacked/dotfiles/tree/main/ai/skills/resolve-conflicts">dotfiles repo</a>. If you use my dotfiles, it’s already available via symlink. Otherwise, grab the files and drop them into <code class="language-plaintext highlighter-rouge">~/.claude/skills/resolve-conflicts/</code>.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Note</p><p>The skill works best with <a href="https://mergiraf.org/">mergiraf</a> installed for the structural merging step. Without it, those files fall through to AI analysis, which still works but is less precise for structural changes.</p>
</div>

<h2 id="putting-it-together">Putting It Together</h2>

<p>The two layers complement each other:</p>

<ol>
  <li>During any git merge, rebase, or cherry-pick, mergiraf runs automatically as the merge driver. It silently resolves structural conflicts before you ever see them. You don’t have to do anything.</li>
  <li>For the conflicts that remain, <code class="language-plaintext highlighter-rouge">/resolve-conflicts</code> categorizes them, applies the right strategy for each type, and continues the operation.</li>
</ol>

<p>The result: most rebases that used to require manual intervention now complete with zero or minimal human input. The conflicts that do need your attention are the genuinely ambiguous ones that deserve it.</p>

<h2 id="try-it">Try It</h2>

<p>Both tools are open source and available in my <a href="https://github.com/haacked/dotfiles">dotfiles repo</a>. Mergiraf is available at <a href="https://mergiraf.org/">mergiraf.org</a> and installs in minutes. The resolve-conflicts skill requires <a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code</a>.</p>

<p>Merge conflicts are an inevitable part of collaborative development. The suffering is optional.</p>]]></content><author><name>Phil Haack</name></author><category term="git" /><category term="productivity" /><category term="open-source" /><summary type="html"><![CDATA[Merge conflicts don't have to ruin your day. I use mergiraf for structural merging and a Claude Code skill for everything else. Here's how to set it up.]]></summary></entry><entry><title type="html">One Year at PostHog</title><link href="https://haacked.com/archive/2026/01/06/one-year-at-posthog/" rel="alternate" type="text/html" title="One Year at PostHog" /><published>2026-01-06T00:00:00+00:00</published><updated>2026-01-06T00:00:00+00:00</updated><id>https://haacked.com/archive/2026/01/06/one-year-at-posthog</id><content type="html" xml:base="https://haacked.com/archive/2026/01/06/one-year-at-posthog/"><![CDATA[<p>The last thing an engineer wants to see is their GitHub avatar next to the pull request that caused an outage.</p>

<p>Yet there it was. My smiling face. On <em>that</em> PR.</p>

<p>This was my first year at PostHog, and it felt like a real test of the culture. Would they blame me? Let it quietly color opinions of my work? Fire me over a mistake?</p>

<p><img src="https://i.haacked.com/blog/2026-01-06-one-year-at-posthog/hogzilla.png" alt="Hogzilla" title="Hogzilla" /></p>

<p>Nah.</p>

<p>My colleagues were supportive and understanding. The question wasn’t “who do we blame?” but “what allowed this to happen and how do we prevent it?” This is a company that looks at failure through the lens of systems and incentives, not individual fault. It embraces the ideas of <a href="https://www.etsy.com/codeascraft/blameless-postmortems">blameless post-mortems</a>.</p>

<blockquote>
  <p>If we go with “blame” as the predominant approach, then we’re implicitly accepting that deterrence is how organizations become safer. This is founded in the belief that individuals, not situations, cause errors.</p>
</blockquote>

<p>That moment is just one of many reasons why I love working here. This is a company that backs up its principles with action.</p>

<h2 id="a-year-of-posthog">A Year of PostHog</h2>

<p>A year ago today, I <a href="https://haacked.com/archive/2025/01/07/new-year-new-job/">started at PostHog</a>, and it has been even better than I hoped. When I joined, I wrote:</p>

<blockquote>
  <p><a href="https://posthog.com/handbook">Their company handbook</a> really impressed me. What it communicates to me is that this is a remote-friendly company that values transparency, autonomy, and trust. It’s a company that treats its employees like adults and tries to minimize overhead.</p>
</blockquote>

<p>It is easy to be cynical about company handbooks and the values they claim to uphold. Enron famously promoted Integrity, Respect, Communication, and Excellence (RICE) as its values, but we all know how that turned out.</p>

<p>What has stood out to me at PostHog is that the handbook reflects reality. The company thinks deeply about how it wants to operate and is willing to adjust when things drift. At our most recent all-company off-site in Tulum, we even revisited and changed some of our values to better reflect how we actually work.</p>

<p>That kind of self-awareness is rare. It’s one big reason why I love working here. Also, Tulum.</p>

<h2 id="retirement-job">Retirement Job?</h2>

<p>When Microsoft acquired GitHub, it gave me a lot of options. I took time off. I started a YC company. That company did not work out, so I took more time off. I was not in a hurry to get back to work, but I did miss the camaraderie of building things with other people. That is what led me to interview at PostHog.</p>

<p>In my interview, one of the founders asked an interesting question. Given that it seemed like I did not <em>need</em> to work, would I be motivated to work hard at PostHog? In other words, was this a “retirement job”?</p>

<p>Of course I said I would work hard. What else could I say?</p>

<p>But here’s what I didn’t say: writing code and building product doesn’t feel like hard work to me, at least not compared to what I did before. As a Director of Engineering, so much of my energy went into bureaucracy, politics, and navigating bad incentives.</p>

<p>That was exhausting.</p>

<p>Writing code is how I unwind.</p>

<p>So in a way, yes, this <em>is</em> a retirement job. Because I love doing it.</p>

<h2 id="polyglot">Polyglot</h2>

<p>I also still have a lot to learn, and this has been a great place to do it.</p>

<p>My team builds the Feature Flags product, which spans the backend service, the frontend UI, and a large collection of SDKs. Over the past year, I have written production code in Python, TypeScript, Rust, Go, Ruby, Elixir, C#, and even PHP.</p>

<p>Yes, PHP. I held out for thirty years. The streak is over.</p>

<p>At one point, I shipped the same ETag caching feature to seven different SDKs in seven different languages in three days.</p>

<p>I am not an expert in all of these languages. But with a solid foundation in programming principles, and a lot of help from LLMs, I have been able to ramp up quickly.</p>

<p>Working across so many languages and paradigms has stretched my thinking and deepened my understanding of how software systems are built.</p>

<h2 id="team-lead">Team Lead</h2>

<p>A while back I wrote <a href="https://haacked.com/archive/2024/12/10/chutes-and-ladder/">Chutes and Ladder career path</a> about how careers do not have to follow a neat, linear ladder. True to that idea, after a year as an individual contributor, I am stepping into a team lead role.</p>

<p>This was not something I actively sought out. I genuinely love being an IC. But, as has often happened in my career, this change grew out of a real need. We are carving out a new <a href="https://posthog.com/teams/flags-platform">Flags Platform team</a> from the Feature Flags team. The good news is that a <a href="https://posthog.com/handbook/company/management">team lead role</a> at PostHog is not a traditional management role. It’s effectively an IC with a bit more responsibilities for the team such as hiring, mentoring, and planning.</p>

<p>What excites me most about this split is the chance to focus deeply on the platform itself.</p>

<p>I am excited to see what year two brings. I will be spending a lot more time in Rust, pushing our flags platform to be faster and more resilient, and working my butt off to ensure my face stays out of future root cause analysis reports.</p>]]></content><author><name>Phil Haack</name></author><category term="posthog" /><category term="work" /><category term="career" /><summary type="html"><![CDATA[Reflections on my first year at PostHog.]]></summary></entry><entry><title type="html">tree-me: Because git worktrees shouldn’t be a chore</title><link href="https://haacked.com/archive/2025/11/21/tree-me/" rel="alternate" type="text/html" title="tree-me: Because git worktrees shouldn’t be a chore" /><published>2025-11-21T00:00:00+00:00</published><updated>2025-11-21T00:00:00+00:00</updated><id>https://haacked.com/archive/2025/11/21/tree-me</id><content type="html" xml:base="https://haacked.com/archive/2025/11/21/tree-me/"><![CDATA[<p>I firmly believe that Git worktrees are one of the most underrated features of Git. I ignored them for years because I didn’t understand them or how to adapt them to my workflow.</p>

<p><img src="https://i.haacked.com/blog/2025-11-21-tree-me/git-worktrees.png" alt="Tree with multiple branches representing git worktrees" /></p>

<p>But that changed as I began using LLM coding tools to do more work in parallel. Being able to work on multiple branches simultaneously is a game changer.</p>

<p>Without git worktrees, working on multiple branches at the same time in the same repository is a pain. There’s the serial approach where you stash your changes, context switch, and pray you didn’t break anything. Or worse, just commit half-finished work with “WIP” messages (looking at you, past me). Or you can have multiple clones of the same repository, but that’s a pain to manage and can take up a lot of disk space.</p>

<p>Git worktrees solve this. They let you have multiple branches checked out simultaneously in different directories that all share the same git database (aka the .git directory). For me, this means I can work on a feature in one terminal, review a PR in another, have Claude Code work on another feature in another terminal, and have all of them share the same git history.</p>

<p>But here’s the thing: creating worktrees manually is tedious. You need to remember where you put them, what to name them, and clean them up later. Also, by default the git worktree is created in the root of the repository unless you specify a different directory.</p>

<p>I wanted something simpler. I wanted something that would work across any repository. No setup, no configuration files, just sensible defaults.</p>

<h2 id="enter-tree-me">Enter tree-me</h2>

<p>I built <a href="https://github.com/haacked/dotfiles/blob/main/bin/tree-me">tree-me</a>, a minimal wrapper around git’s native worktree commands. It adds organizational convention while letting git handle all the complexity.</p>

<p>Instead of this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create worktree manually</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> ~/worktrees/my-project
git worktree add ~/worktrees/my-project/fix-bug <span class="nt">-b</span> haacked/fix-bug main
<span class="nb">cd</span> ~/worktrees/my-project/fix-bug
<span class="c"># Now repeat for every repo...</span>
</code></pre></div></div>

<p>You do this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tree-me create haacked/fix-bug
<span class="c"># Creates: ~/dev/worktrees/my-project/haacked/fix-bug</span>
<span class="c"># And automatically cds into it</span>
</code></pre></div></div>

<h2 id="how-it-works">How it works</h2>

<p>tree-me uses git-like subcommands and follows conventions so you don’t have to think:</p>

<ul>
  <li><strong>Auto-detects repository name</strong> from your git remote</li>
  <li><strong>Auto-detects default branch</strong> (checks for origin/HEAD, falls back to main)</li>
  <li><strong>Organizes by repo</strong>: <code class="language-plaintext highlighter-rouge">$WORKTREE_ROOT/&lt;repo-name&gt;/&lt;branch-name&gt;</code></li>
  <li><strong>Delegates to git</strong> for all validation, errors, and edge cases</li>
  <li><strong>PR support</strong>: Fetches GitHub PRs using git’s native PR refs (requires <code class="language-plaintext highlighter-rouge">gh</code> CLI)</li>
  <li><strong>Auto-CD</strong>: Automatically changes to the worktree directory after creation</li>
  <li><strong>Tab completion</strong>: Complete commands and branch names in bash/zsh</li>
</ul>

<p>Commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tree-me create &lt;branch&gt; <span class="o">[</span>base]        <span class="c"># Create new branch in worktree</span>
tree-me checkout &lt;branch&gt;             <span class="c"># Checkout existing branch (alias: co)</span>
tree-me <span class="nb">pr</span> &lt;number|url&gt;               <span class="c"># Checkout GitHub PR (uses gh CLI)</span>
tree-me list                          <span class="c"># List all worktrees (alias: ls)</span>
tree-me remove &lt;branch&gt;               <span class="c"># Remove a worktree (alias: rm)</span>
tree-me prune                         <span class="c"># Prune stale worktree files</span>
tree-me shellenv                      <span class="c"># Output shell function for auto-cd</span>
</code></pre></div></div>

<p>Examples:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tree-me create haacked/fix-bug              <span class="c"># Create from main/master</span>
tree-me create haacked/fix-bug develop      <span class="c"># Create from develop</span>
tree-me co existing-feature                 <span class="c"># Checkout existing branch</span>
tree-me <span class="nb">pr </span>123                              <span class="c"># Checkout PR #123</span>
tree-me <span class="nb">pr </span>https://github.com/org/repo/pull/456
tree-me <span class="nb">ls</span>                                  <span class="c"># Show all worktrees</span>
tree-me <span class="nb">rm </span>haacked/fix-bug                  <span class="c"># Clean up (supports tab completion)</span>
</code></pre></div></div>

<h2 id="conventions">Conventions</h2>

<p>tree-me is a minimal wrapper around git’s native commands. Works with any repo, any language, any setup. The only convention is where worktrees live and how they’re named.</p>

<p>Want worktrees in a different location? Set <code class="language-plaintext highlighter-rouge">WORKTREE_ROOT</code>. Need to branch from develop instead of main? Pass it as an argument: <code class="language-plaintext highlighter-rouge">tree-me create my-feature develop</code>. Conventions with escape hatches.</p>

<h2 id="setup">Setup</h2>

<p>To enable auto-cd and tab completion, add this to your <code class="language-plaintext highlighter-rouge">~/.bashrc</code> or <code class="language-plaintext highlighter-rouge">~/.zshrc</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> &lt;<span class="o">(</span>tree-me shellenv<span class="o">)</span>
</code></pre></div></div>

<p>This makes <code class="language-plaintext highlighter-rouge">tree-me create</code>, <code class="language-plaintext highlighter-rouge">tree-me checkout</code>, and <code class="language-plaintext highlighter-rouge">tree-me pr</code> automatically cd into the worktree directory. It also enables tab completion for commands and branch names (try <code class="language-plaintext highlighter-rouge">tree-me rm &lt;TAB&gt;</code>).</p>

<p>View the full implementation at <a href="https://github.com/haacked/dotfiles/blob/main/bin/tree-me">github.com/haacked/dotfiles/blob/main/bin/tree-me</a>.</p>

<h2 id="the-pr-workflow">The PR workflow</h2>

<p>Here’s where it shines. Someone asks you to review a PR while you’re deep in a feature:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tree-me <span class="nb">pr </span>123                  <span class="c"># Fetches, checks out PR, and cds into it</span>
<span class="c"># You're now in: ~/dev/worktrees/dotfiles/pr-123</span>
<span class="c"># Review the code, test it, leave comments</span>
<span class="c"># When done, switch back</span>
tree-me co haacked/my-feature   <span class="c"># Checks out and cds back to your feature</span>
<span class="c"># Back to your work, no stash needed</span>
</code></pre></div></div>

<p>When you’re done reviewing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tree-me <span class="nb">rm </span>pr-123               <span class="c"># Tab complete to see available branches</span>
</code></pre></div></div>

<p>Gone. Clean. No accidentally committing review changes to your feature branch.</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Note</p><p>tree-me uses the <code class="language-plaintext highlighter-rouge">gh</code> CLI to fetch PRs. If you don’t have it installed, you can install it with <code class="language-plaintext highlighter-rouge">brew install gh</code>.</p>
</div>

<h2 id="installation">Installation</h2>

<p>Download <a href="https://github.com/haacked/dotfiles/blob/main/bin/tree-me">tree-me</a> and put it somewhere in your <code class="language-plaintext highlighter-rouge">PATH</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Example: copy to ~/bin or ~/.local/bin</span>
curl <span class="nt">-o</span> ~/bin/tree-me https://raw.githubusercontent.com/haacked/dotfiles/main/bin/tree-me
<span class="nb">chmod</span> +x ~/bin/tree-me
</code></pre></div></div>

<p>Then enable auto-cd and tab completion (see Setup section above).</p>

<p>That’s it. No dependencies beyond git, bash, and optionally the <code class="language-plaintext highlighter-rouge">gh</code> CLI for PR checkout.</p>

<h2 id="why-i-built-this">Why I built this</h2>

<p>I work on multiple repos daily—PostHog, my blog, various open source projects. I was tired of remembering project-specific worktree scripts and hunting for that worktree I created last week.</p>

<p>The philosophy: <strong>Don’t recreate what git does well. Add only the minimal convention needed.</strong></p>

<p>Git already handles worktrees perfectly. I just needed organized paths, sensible defaults, and a consistent interface across all my projects.</p>

<h2 id="directory-structure">Directory structure</h2>

<p>Everything is organized predictably based on the repository name and branch name:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/dev/worktrees/&lt;repo-name&gt;/&lt;branch-name&gt;
</code></pre></div></div>

<p>For example:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/dev/worktrees/
├── dotfiles/
│   ├── haacked/vim-improvements/
│   ├── haacked/git-tools/
│   └── main/
├── posthog/
│   ├── haacked/feature-flags/
│   ├── pr-789-contributor/
│   └── main/
└── spelungit/
    └── haacked/performance/
</code></pre></div></div>

<p>One glance and you know where everything is.</p>

<h2 id="what-it-doesnt-do">What it doesn’t do</h2>

<p>tree-me doesn’t copy environment files, install dependencies, or set up project-specific tools. That’s deliberate. Those concerns belong in your project’s setup scripts, not in a generic git tool.</p>

<p>Want to automate environment setup? Add a script to your repo that runs after checkout. Want to copy <code class="language-plaintext highlighter-rouge">.env</code> files? Put it in your project’s onboarding docs. tree-me just handles the git worktree ceremony.</p>

<h2 id="try-it">Try it</h2>

<p>If you work with multiple branches regularly, give worktrees a try. If you work with multiple repos, give tree-me a try. If you hate it, at least you learned about git worktrees (and that’s probably worth more than the script).</p>

<p>Find tree-me at <a href="https://github.com/haacked/dotfiles/blob/main/bin/tree-me">github.com/haacked/dotfiles/blob/main/bin/tree-me</a>. It’s MIT licensed—copy it, modify it, improve it.</p>]]></content><author><name>Phil Haack</name></author><category term="git" /><category term="open-source" /><category term="productivity" /><category term="shell" /><summary type="html"><![CDATA[Working on multiple branches simultaneously? I built tree-me to manage git worktrees without the ceremony. Convention over configuration for the win.]]></summary></entry><entry><title type="html">Spelungit: When `git log –grep` isn’t enough</title><link href="https://haacked.com/archive/2025/09/29/announcing-spelungit/" rel="alternate" type="text/html" title="Spelungit: When `git log –grep` isn’t enough" /><published>2025-09-29T00:00:00+00:00</published><updated>2025-09-29T00:00:00+00:00</updated><id>https://haacked.com/archive/2025/09/29/announcing-spelungit</id><content type="html" xml:base="https://haacked.com/archive/2025/09/29/announcing-spelungit/"><![CDATA[<p>Supporting a large codebase is challenging. Sometimes, the questions you have can’t be answered by the code at hand. For example, “When did we switch from class components to hooks and what discussion led to that decision?” or “Did we used to have logging here for invalid keys?” or “Did we ever have code to handle Zstd compression?”</p>

<p>Git’s search is challenging for these sorts of queries. <code class="language-plaintext highlighter-rouge">git log --grep="auth"</code> assumes you remember exact words from commit messages. Want to find “commits where we improved error handling in API endpoints”? You’ll need multiple greps with regex gymnastics, and still miss commits that used different terminology. We need a better tool.</p>

<p><img src="https://i.haacked.com/blog/2025-09-29-announcing-spelungit/robot-spelunking-git.png" alt="Robot spelunking Git" /></p>

<h2 id="enter-spelungit">Enter Spelungit</h2>

<p>I built <a href="https://github.com/haacked/spelungit">Spelungit</a>, a semantic search engine for Git commit history. Instead of keyword roulette, you search using natural language through Claude Code’s MCP interface.</p>

<p>Want commits where you refactored authentication? Ask for “authentication flow refactoring.” Looking for race conditions in background processing? Search for “race conditions in job processing.” It understands intent instead of making you guess exact words from three months ago.</p>

<h2 id="how-it-works">How it works</h2>

<p>Instead of this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># How do you even grep for "race condition fixes"?</span>
git log <span class="nt">--grep</span><span class="o">=</span><span class="s2">"race</span><span class="se">\|</span><span class="s2">concurrent</span><span class="se">\|</span><span class="s2">thread</span><span class="se">\|</span><span class="s2">lock</span><span class="se">\|</span><span class="s2">mutex"</span> <span class="nt">--all</span>
<span class="c"># Now manually read through 50 commits...</span>
</code></pre></div></div>

<p>You do this in your AI development tool of choice:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Search git <span class="nb">history </span><span class="k">for</span> <span class="s2">"race condition fixes in background jobs"</span>
</code></pre></div></div>

<p>Queries that work:</p>

<ul>
  <li>“When did we switch from REST to GraphQL?”</li>
  <li>“Commits where we improved error handling in API endpoints”</li>
  <li>“Show me refactoring of the authentication flow”</li>
  <li>“Security improvements in file upload handling”</li>
  <li>“Where did we fix that memory leak in the worker process?”</li>
</ul>

<p>It creates embeddings for both commit messages and code changes, which makes semantic search possible. It knows the difference between new features and bug fixes, even when commit messages are vague (looking at you, past me).</p>

<h2 id="installation">Installation</h2>

<p>Spelungit uses SQLite and local embeddings to make installation simple. Everything runs on your machine. No need for an API key, a database, or Docker.</p>

<p>One line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSL</span> https://raw.githubusercontent.com/haacked/spelungit/main/install-remote.sh | bash
</code></pre></div></div>

<p>Downloads everything, sets up Python venv, installs dependencies, configures Claude Code. Suspicious of pipe-to-bash? Clone the repo and run <code class="language-plaintext highlighter-rouge">./install.sh</code>.</p>

<h2 id="using-it">Using it</h2>

<p>Shows up in Claude Code (or your AI tool of choice) as an MCP server. The first time you ask a question in a repository that <code class="language-plaintext highlighter-rouge">spelungit</code> responds to, spelungit will index the repository. Once it’s done, it’ll be able to answer questions.For large repos, this can take a few minutes while it analyzes commits and creates embeddings.</p>

<p>Then search naturally:</p>

<ul>
  <li>“Show me commits about error handling improvements”</li>
  <li>“Find where we refactored the authentication”</li>
  <li>“What changed in the API layer recently?”</li>
</ul>

<p>Claude calls the appropriate MCP tools (<code class="language-plaintext highlighter-rouge">index_repository</code>, <code class="language-plaintext highlighter-rouge">search_commits</code>, <code class="language-plaintext highlighter-rouge">repository_status</code>, <code class="language-plaintext highlighter-rouge">get_database_info</code>) behind the scenes.</p>

<h2 id="under-the-hood">Under the hood</h2>

<p>Analyzes commit messages and code changes. For each commit:</p>

<ul>
  <li>Semantic content from messages and diffs</li>
  <li>Code patterns like function names and classes</li>
  <li>File types and directory structure</li>
  <li>Feature vs. bug fix vs. refactoring</li>
</ul>

<p>Uses Microsoft’s <code class="language-plaintext highlighter-rouge">all-MiniLM-L6-v2</code> (384 dimensions) with local sentence-transformers to create embeddings. Embeddings are stored in SQLite. Searching for git history uses cosine similarity searches.</p>

<p>The sqlite database only stores commit SHAs and embeddings, leaving the full commit message in git. Thousands of commits = few megabytes. Handles Git worktrees if you’re into that masochism.</p>

<h2 id="why-i-built-this">Why I built this</h2>

<p>I work across multiple repositories and constantly need to understand why changes were made. Traditional Git tools are limited. Too much detective work scrolling through commits.</p>

<p>I wanted to ask Git history questions in plain English and get the right commits back. Semantic search of Git history is something I always wished GitHub would add. I got tired of waiting so I built this.</p>

<p>Also, I’ve been experimenting with MCP servers and wanted to build something useful instead of another todo app.</p>

<h2 id="future-ideas-for-improvement">Future ideas for improvement</h2>

<p><strong>Configurable models</strong>: Local embeddings work pretty well, but OpenAI’s models would be better for teams willing to trade privacy for better accuracy.
<strong>PostgreSQL support</strong>: For massive repos or shared team search.
<strong>Smarter code understanding</strong>: Better refactoring vs. feature detection, architectural changes over time.</p>

<h2 id="try-it">Try it</h2>

<p><a href="https://github.com/haacked/spelungit">Spelungit</a> is MIT licensed. The project scratched a real itch for me. If it doesn’t work perfectly, that’s what GitHub issues are for.</p>

<p>Find it at <a href="https://github.com/haacked/spelungit">https://github.com/haacked/spelungit</a>.</p>]]></content><author><name>Phil Haack</name></author><category term="git" /><category term="open-source" /><category term="mcp" /><category term="claude" /><category term="ai" /><summary type="html"><![CDATA[Ever tried to remember why you made a change six months ago? I built Spelungit so you can search Git commit history with natural language instead of praying to the regex gods.]]></summary></entry><entry><title type="html">Cleaning up gone branches</title><link href="https://haacked.com/archive/2025/04/17/git-gone/" rel="alternate" type="text/html" title="Cleaning up gone branches" /><published>2025-04-17T00:00:00+00:00</published><updated>2025-04-17T00:00:00+00:00</updated><id>https://haacked.com/archive/2025/04/17/git-gone</id><content type="html" xml:base="https://haacked.com/archive/2025/04/17/git-gone/"><![CDATA[<p>A long time ago, I wrote a <a href="https://haacked.com/archive/2014/07/28/github-flow-aliases/">useful set of git aliases</a> to support the GitHub flow. My favorite alias was <code class="language-plaintext highlighter-rouge">bdone</code> which would:</p>

<ol>
  <li>Checkout the default branch.</li>
  <li>Run <code class="language-plaintext highlighter-rouge">git up</code> to make sure you’re up to date.</li>
  <li>Run <code class="language-plaintext highlighter-rouge">git bclean</code> to delete all the branches that have been merged into the default branch.</li>
</ol>

<p>And this worked great for a long time. The way it worked was it would list all the branches that have been merged into the default branch and then delete them. In my case, I didn’t use <code class="language-plaintext highlighter-rouge">git branch --merged</code> to list merged branches because I didn’t know about it at the time.</p>

<p>However, my aliases stopped working for me recently after joining PostHog. The main reason is on pretty much all of their repositories, they use Squash and Merge when merging PRs.</p>

<p><img src="https://i.haacked.com/blog/2025-04-17-git-gone/crushed-cars.png" alt="Image of a set of cars being squashed together" /></p>

<p>When you use <code class="language-plaintext highlighter-rouge">git merge --squash</code> or GitHub’s “Squash and merge” feature, Git creates a new commit on the target branch that combines all the changes from the source branch into a single commit. This new commit doesn’t retain any reference to the original commits from the source branch. As a result, Git doesn’t consider the source branch as merged, and commands like <code class="language-plaintext highlighter-rouge">git branch --merged</code> won’t show it.</p>

<p>But here’s the thing about Git. There’s almost always a way.</p>

<h2 id="solution">Solution</h2>

<p>When you merge a PR on GitHub, it shows you a “Delete branch” button:</p>

<p><img src="https://i.haacked.com/blog/2025-04-17-git-gone/delete-repo.png" alt="image of a delete branch button" /></p>

<p>This is a great feature. It’s a good way to clean up branches that have been merged into the default branch. In fact, you can configure GitHub to “Automatically delete head branches” when merged:</p>

<p><img src="https://i.haacked.com/blog/2025-04-17-git-gone/automatically-delete.png" alt="image showing configuring a repository to automatically delete head branches" /></p>

<p>I highly recommend you do the same. When the remote branch is deleted, Git will track it as “gone”. For example, if you run <code class="language-plaintext highlighter-rouge">git branch -vv</code> you’ll see something like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> git branch <span class="nt">-vv</span>

  haacked/decide-v4      ba39408 <span class="o">[</span>origin/haacked/decide-v4: gone] Fix demo to handle variants
  haacked/fix-sample-app ec15751 <span class="o">[</span>origin/haacked/fix-sample-app] Handle variants
<span class="k">*</span> haacked/local-only     e78d2f6 Do important stuff
  main                   ab885d0 <span class="o">[</span>origin/main] chore: Bump to v1.0.2
</code></pre></div></div>

<p>Notice that <code class="language-plaintext highlighter-rouge">haacked/decide-v4</code> is marked as <code class="language-plaintext highlighter-rouge">gone</code>.</p>

<p>We can use git’s porcelain to list branches and their tracking information in a more easily parseable format.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> git <span class="k">for</span><span class="nt">-each-ref</span> <span class="nt">--format</span><span class="o">=</span><span class="s1">'%(refname:short) %(upstream:track)'</span> refs/heads/

haacked/decide-v4 <span class="o">[</span>gone]
haacked/fix-sample-app
haacked/local-only
master
</code></pre></div></div>

<p>Let’s make this an alias to list these gone branches:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> git config <span class="nt">--global</span> alias.gone <span class="s2">"!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '</span><span class="se">\$</span><span class="s2">2 == </span><span class="se">\"</span><span class="s2">[gone]</span><span class="se">\"</span><span class="s2"> { print </span><span class="se">\$</span><span class="s2">1 }'"</span>
</code></pre></div></div>

<p>Now we can use the alias to list gone branches:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> git gone

haacked/decide-v4
</code></pre></div></div>

<p>Next step is to update my old <code class="language-plaintext highlighter-rouge">bclean</code> alias to use the new <code class="language-plaintext highlighter-rouge">gone</code> alias.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> git config <span class="nt">--global</span> alias.bclean <span class="s2">"!git gone | xargs -r git branch -D"</span>
</code></pre></div></div>

<p>Unfortunately, since git doesn’t know it’s been merged, we have to do a force delete. That’s a bit scary, but this won’t touch local branches or any branches that are still tracking a remote branch. With this alias, you can run <code class="language-plaintext highlighter-rouge">git bclean</code> to delete all the branches that have been merged into the default branch. Finally, we have the old <code class="language-plaintext highlighter-rouge">bdone</code> alias to switch to the default branch, run <code class="language-plaintext highlighter-rouge">git up</code>, and then run <code class="language-plaintext highlighter-rouge">bclean</code>.</p>

<p>Here’s the complete set of aliases mentioned in this post you can cut and paste into your <code class="language-plaintext highlighter-rouge">.gitconfig</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[alias]</span>
    <span class="py">default</span> <span class="p">=</span> <span class="s">"!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"</span>
    <span class="py">bclean</span> <span class="p">=</span> <span class="s">"!git gone | xargs -r git branch -D"</span>
    <span class="c"># Switches to specified branch (or the default branch if no branch is specified), runs git up, then runs bclean.
</span>    <span class="py">bdone</span> <span class="p">=</span> <span class="s">"!f() { DEFAULT=$(git default); git checkout ${1-$DEFAULT} &amp;&amp; git up &amp;&amp; git bclean; }; f"</span>
    <span class="py">gone</span> <span class="p">=</span> <span class="s">"!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '$2 == </span><span class="se">\"</span><span class="s">[gone]</span><span class="se">\"</span><span class="s"> { print $1 }'"</span>
</code></pre></div></div>

<p>Or, if you want to use the <code class="language-plaintext highlighter-rouge">git</code> command line, you can use the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git config <span class="nt">--global</span> alias.default <span class="s2">"!git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'"</span>
git config <span class="nt">--global</span> alias.gone <span class="s2">"!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ | awk '</span><span class="se">\$</span><span class="s2">2 == </span><span class="se">\"</span><span class="s2">[gone]</span><span class="se">\"</span><span class="s2"> { print </span><span class="se">\$</span><span class="s2">1 }'"</span>
git config <span class="nt">--global</span> alias.bclean <span class="s2">"!git gone | xargs -r git branch -D"</span>
git config <span class="nt">--global</span> alias.bdone <span class="s2">"!f() { DEFAULT=</span><span class="se">\$</span><span class="s2">(git default); git checkout </span><span class="se">\$</span><span class="s2">{1:-</span><span class="se">\$</span><span class="s2">DEFAULT} &amp;&amp; git up &amp;&amp; git bclean; }; f"</span>
</code></pre></div></div>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Note</p><p>When using the <code class="language-plaintext highlighter-rouge">git default</code> alias, it’s possible you’ll encounter the following error:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fatal: ref refs/remotes/origin/HEAD is not a symbolic ref
</code></pre></div>

</div>


<p>This alias relies on the presence of the origin/HEAD symbolic reference. In some cases, especially with newly cloned repositories or certain configurations, this reference might not be set. To fix it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git remote set-head origin <span class="nt">--auto</span>
</code></pre></div>

</div>


</div>

<p>So now, my old workflow is back. When I merge a PR, I can run <code class="language-plaintext highlighter-rouge">git bdone</code> from the branch I merged to clean up the branches. All is right with the world again.</p>

<p>And as always, you can find these aliases and more in my <a href="https://github.com/haacked/dotfiles">dotfiles repo</a>. See <a href="https://haacked.com/archive/2019/02/14/including-git-aliases/">this blog post</a> for more context on how you can include them in your dotfiles.</p>]]></content><author><name>Phil Haack</name></author><category term="git" /><summary type="html"><![CDATA[A git alias to clean up gone branches. Even ones that have been squashed and merged.]]></summary></entry><entry><title type="html">Using PostHog in your .NET applications</title><link href="https://haacked.com/archive/2025/02/25/posthog-dotnet-1.0/" rel="alternate" type="text/html" title="Using PostHog in your .NET applications" /><published>2025-02-25T00:00:00+00:00</published><updated>2025-02-25T00:00:00+00:00</updated><id>https://haacked.com/archive/2025/02/25/posthog-dotnet-1.0</id><content type="html" xml:base="https://haacked.com/archive/2025/02/25/posthog-dotnet-1.0/"><![CDATA[<p>PostHog helps you build better products. It tracks what users do. It controls features in production. And now it works with .NET!</p>

<p><a href="https://haacked.com/archive/2025/01/07/new-year-new-job/">I joined PostHog at the beginning of the year</a> as a Product Engineer on <a href="https://posthog.com/teams/feature-flags">the Feature Flags team</a>. Feature flags are just one of the many tools PostHog offers to help product engineers build better products.</p>

<p>Much of my job will consist of writing Python and React with TypeScript. But when I started, I noticed they didn’t have a .NET SDK. It turns out, I know a thing or two about .NET!</p>

<p><img src="https://github.com/user-attachments/assets/b7c64431-0232-4707-b016-c0161ea6b0eb" alt="Hedgehog giving two thumbs up in front of a computer showing the .NET logo" title="Who has two thumbs and loves .NET?" /></p>

<p>So if you’ve been wanting to use PostHog in your ASP.NET Core applications, yesterday is your lucky day! The 1.0 version of the PostHog .NET SDK for ASP.NET Core is <a href="https://www.nuget.org/packages/PostHog.AspNetCore">available on NuGet</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package PostHog.AspNetCore
</code></pre></div></div>

<p>You can find documentation for the library on <a href="https://posthog.com/docs/libraries/dotnet">the PostHog docs site</a>, but I’ll cover some of the basics here. I’ll also cover non-ASP.NET Core usage later in this post.</p>

<h2 id="configuration">Configuration</h2>

<p>To configure the client SDK, you’ll need:</p>

<ol>
  <li>Project API Key - <em>from the <a href="https://us.posthog.com/settings/project">PostHog dashboard</a></em></li>
  <li>Personal API Key - <em>for local evaluation (Optional, but recommended)</em></li>
</ol>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Note</p><p>For better performance, enable local feature flag evaluation by adding a personal API key (found in Settings). This avoids making API calls for each flag check.</p>
</div>

<p>By default, the PostHog client looks for settings in the <code class="language-plaintext highlighter-rouge">PostHog</code> section of the configuration system such as in the <code class="language-plaintext highlighter-rouge">appSettings.json</code> file:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"PostHog"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ProjectApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"phc_..."</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Treat your personal API key as a secret by using a secrets manager to store it. For example, for local development, use the <code class="language-plaintext highlighter-rouge">dotnet user-secrets</code> command to store your personal API key:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet user-secrets init
dotnet user-secrets <span class="nb">set</span> <span class="s2">"PostHog:PersonalApiKey"</span> <span class="s2">"phx_..."</span>
</code></pre></div></div>

<p>In production, you might use Azure Key Vault or a similar service to provide the personal API key.</p>

<h2 id="register-the-client">Register the client</h2>

<p>Once you set up configuration, register the client with the dependency injection container.</p>

<p>In your <code class="language-plaintext highlighter-rouge">Program.cs</code> file, call the <code class="language-plaintext highlighter-rouge">AddPostHog</code> extension method on the <code class="language-plaintext highlighter-rouge">WebApplicationBuilder</code> instance. It’ll look something like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">PostHog</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="nf">AddPostHog</span><span class="p">();</span>
</code></pre></div></div>

<p>Calling <code class="language-plaintext highlighter-rouge">builder.AddPostHog()</code> adds a singleton implementation of <code class="language-plaintext highlighter-rouge">IPostHogClient</code> to the dependency injection container. Inject it into your controllers or pages like so:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">MyController</span><span class="p">(</span><span class="n">IPostHogClient</span> <span class="n">posthog</span><span class="p">)</span> <span class="p">:</span> <span class="n">Controller</span>
<span class="p">{</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">MyPage</span><span class="p">(</span><span class="n">IPostHogClient</span> <span class="n">posthog</span><span class="p">)</span> <span class="p">:</span> <span class="n">PageModel</span>
<span class="p">{</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="usage">Usage</h2>

<p>Use the <code class="language-plaintext highlighter-rouge">IPostHogClient</code> service to identify users, capture analytics, and evaluate feature flags.</p>

<p>Use the <code class="language-plaintext highlighter-rouge">IdentifyAsync</code> method to identify users:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// This stores information about the user in PostHog.</span>
<span class="k">await</span> <span class="n">posthog</span><span class="p">.</span><span class="nf">IdentifyAsync</span><span class="p">(</span>
    <span class="n">distinctId</span><span class="p">,</span>
    <span class="n">user</span><span class="p">.</span><span class="n">Email</span><span class="p">,</span>
    <span class="n">user</span><span class="p">.</span><span class="n">UserName</span><span class="p">,</span>
    <span class="c1">// Properties to set on the person. If they're already</span>
    <span class="c1">// set, they will be overwritten.</span>
    <span class="n">personPropertiesToSet</span><span class="p">:</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="s">"phone"</span><span class="p">]</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">PhoneNumber</span> <span class="p">??</span> <span class="s">"unknown"</span><span class="p">,</span>
        <span class="p">[</span><span class="s">"email_confirmed"</span><span class="p">]</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">EmailConfirmed</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="c1">// Properties to set once. If they're already set</span>
    <span class="c1">// on the person, they won't be overwritten.</span>
    <span class="n">personPropertiesToSetOnce</span><span class="p">:</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="s">"joined"</span><span class="p">]</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span> 
    <span class="p">});</span>
</code></pre></div></div>
<p>Some things to note about the <code class="language-plaintext highlighter-rouge">IdentifyAsync</code> method:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">distinctId</code> is the identifier for the user. This could be an email, a username, or some other identifier such as the database Id. The important thing is that it’s a consistent and unique identifier for the user. If you use PostHog on the client, use the same <code class="language-plaintext highlighter-rouge">distinctId</code> here as you do on the client.</li>
  <li>The <code class="language-plaintext highlighter-rouge">personPropertiesToSet</code> and <code class="language-plaintext highlighter-rouge">personPropertiesToSetOnce</code> are optional. You can use them to set properties about the user.</li>
  <li>If you choose a <code class="language-plaintext highlighter-rouge">distinctId</code> that can change (such as username or email), you can use the <code class="language-plaintext highlighter-rouge">AliasAsync</code> method to alias the old <code class="language-plaintext highlighter-rouge">distinctId</code> with the new one so that the user can be tracked across different <code class="language-plaintext highlighter-rouge">distinctIds</code>.</li>
</ul>

<p>To capture an event, call the <code class="language-plaintext highlighter-rouge">Capture</code> method:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">posthog</span><span class="p">.</span><span class="nf">Capture</span><span class="p">(</span><span class="s">"some-distinct-id"</span><span class="p">,</span> <span class="s">"my-event"</span><span class="p">);</span>
</code></pre></div></div>

<p>This will capture an event with the distinct id, the event name, and the current timestamp. You can also include properties:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">posthog</span><span class="p">.</span><span class="nf">Capture</span><span class="p">(</span>
    <span class="s">"some-distinct-id"</span><span class="p">,</span>
    <span class="s">"user signed up"</span><span class="p">,</span>
    <span class="k">new</span><span class="p">()</span> <span class="p">{</span> <span class="p">[</span><span class="s">"plan"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"pro"</span> <span class="p">});</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Capture</code> method is synchronous and returns immediately. The actual batching and sending of events is done in the background.</p>

<h2 id="feature-flags">Feature flags</h2>

<p>To evaluate a feature flag, call the <code class="language-plaintext highlighter-rouge">IsFeatureEnabledAsync</code> method:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="k">await</span> <span class="n">posthog</span><span class="p">.</span><span class="nf">IsFeatureEnabledAsync</span><span class="p">(</span>
    <span class="s">"new_user_feature"</span><span class="p">,</span>
    <span class="s">"some-distinct-id"</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// The feature flag is enabled.</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This will evaluate the feature flag and return <code class="language-plaintext highlighter-rouge">true</code> if the feature flag is enabled. If the feature flag is not enabled or not found, it will return <code class="language-plaintext highlighter-rouge">false</code>.</p>

<p>Feature Flags can contain filter conditions that might depend on properties of the user. For example, you might have a feature flag that is enabled for users on the pro plan.</p>

<p>If you’ve previously identified the user and are NOT using local evaluation, the feature flag is evaluated on the server against the user properties set on the person via the <code class="language-plaintext highlighter-rouge">IdentifyAsync</code> method.</p>

<p>But if you’re using local evaluation, the feature flag is evaluated on the client, so you have to pass in the properties of the user:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">posthog</span><span class="p">.</span><span class="nf">IsFeatureEnabledAsync</span><span class="p">(</span>
    <span class="n">featureKey</span><span class="p">:</span> <span class="s">"person-flag"</span><span class="p">,</span>
    <span class="n">distinctId</span><span class="p">:</span> <span class="s">"some-distinct-id"</span><span class="p">,</span>
    <span class="n">personProperties</span><span class="p">:</span> <span class="k">new</span><span class="p">()</span> <span class="p">{</span> <span class="p">[</span><span class="s">"plan"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"pro"</span> <span class="p">});</span>
</code></pre></div></div>

<p>This will evaluate the feature flag and return <code class="language-plaintext highlighter-rouge">true</code> if the feature flag is enabled and the user’s plan is “pro”.</p>

<h2 id="net-feature-management">.NET Feature Management</h2>

<p><a href="https://learn.microsoft.com/en-us/azure/azure-app-configuration/feature-management-dotnet-reference">.NET Feature Management</a> is an abstraction over feature flags that is supported by ASP.NET Core. With it enabled, you can use the <code class="language-plaintext highlighter-rouge">&lt;feature /&gt;</code> tag helper to conditionally render UI based on the state of a feature flag.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="n">feature</span> <span class="n">name</span><span class="p">=</span><span class="s">"my-feature"</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="n">p</span><span class="p">&gt;</span><span class="n">This</span> <span class="k">is</span> <span class="n">a</span> <span class="n">feature</span> <span class="n">flag</span><span class="p">.&lt;/</span><span class="n">p</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="n">feature</span><span class="p">&gt;</span>
</code></pre></div></div>

<p>You can also use the <code class="language-plaintext highlighter-rouge">FeatureGateAttribute</code> in your controllers and pages to conditionally execute code based on the state of a feature flag.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">FeatureGate</span><span class="p">(</span><span class="s">"my-feature"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">MyController</span> <span class="p">:</span> <span class="n">Controller</span>
<span class="p">{</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If your app already uses .NET Feature Management, you can switch to using PostHog with very little effort.</p>

<p>To use PostHog feature flags with the .NET Feature Management library, implement the <code class="language-plaintext highlighter-rouge">IPostHogFeatureFlagContextProvider</code> interface. The simplest way to do that is to inherit from the <code class="language-plaintext highlighter-rouge">PostHogFeatureFlagContextProvider</code> class and override the <code class="language-plaintext highlighter-rouge">GetDistinctId</code> and <code class="language-plaintext highlighter-rouge">GetFeatureFlagOptionsAsync</code> methods. This is required so that .NET Feature Management can evaluate feature flags locally with the correct <code class="language-plaintext highlighter-rouge">distinctId</code> and <code class="language-plaintext highlighter-rouge">personProperties</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">MyFeatureFlagContextProvider</span><span class="p">(</span>
    <span class="n">IHttpContextAccessor</span> <span class="n">httpContextAccessor</span><span class="p">)</span>
    <span class="p">:</span> <span class="n">PostHogFeatureFlagContextProvider</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="k">override</span> <span class="kt">string</span><span class="p">?</span> <span class="nf">GetDistinctId</span><span class="p">()</span> <span class="p">=&gt;</span>
       <span class="n">httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">?.</span><span class="n">User</span><span class="p">.</span><span class="n">Identity</span><span class="p">?.</span><span class="n">Name</span><span class="p">;</span>
    
    <span class="k">protected</span> <span class="k">override</span> <span class="n">ValueTask</span><span class="p">&lt;</span><span class="n">FeatureFlagOptions</span><span class="p">&gt;</span> <span class="nf">GetFeatureFlagOptionsAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// In a real app, you might get this information from a database or other source for the current user.</span>
        <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">FeatureFlagOptions</span>
            <span class="p">{</span>
                <span class="n">PersonProperties</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">object</span><span class="p">?&gt;</span>
                <span class="p">{</span>
                    <span class="p">[</span><span class="s">"email"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"some-test@example.com"</span><span class="p">,</span>
                    <span class="p">[</span><span class="s">"plan"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"pro"</span>
                <span class="p">},</span>
                <span class="n">OnlyEvaluateLocally</span> <span class="p">=</span> <span class="k">true</span>
            <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then, register your implementation in <code class="language-plaintext highlighter-rouge">Program.cs</code> (or <code class="language-plaintext highlighter-rouge">Startup.cs</code>):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">PostHog</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="nf">AddPostHog</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="n">options</span><span class="p">.</span><span class="n">UseFeatureManagement</span><span class="p">&lt;</span><span class="n">MyFeatureFlagContextProvider</span><span class="p">&gt;();</span>
<span class="p">});</span>
</code></pre></div></div>

<p>This registers a feature flag provider that uses your implementation of <code class="language-plaintext highlighter-rouge">IPostHogFeatureFlagContextProvider</code> to evaluate feature flags against PostHog.</p>

<h2 id="non-aspnet-core-usage">Non-ASP.NET Core usage</h2>

<p>The <code class="language-plaintext highlighter-rouge">PostHog.AspNetCore</code> package adds ASP.NET Core specific functionality on top of the core <code class="language-plaintext highlighter-rouge">PostHog</code> package. But if you’re not using ASP.NET Core, you can use the core <code class="language-plaintext highlighter-rouge">PostHog</code> package directly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package PostHog.AspNetCore
</code></pre></div></div>

<p>And then register it with your dependency injection container:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddPostHog</span><span class="p">();</span>
</code></pre></div></div>

<p>If you’re not using dependency injection, you can still use the registration method:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">PostHog</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">services</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ServiceCollection</span><span class="p">();</span>
<span class="n">services</span><span class="p">.</span><span class="nf">AddPostHog</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">serviceProvider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">posthog</span> <span class="p">=</span> <span class="n">serviceProvider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IPostHogClient</span><span class="p">&gt;();</span>
</code></pre></div></div>

<p>For a console app (or apps not using dependency injection), you can also use the <code class="language-plaintext highlighter-rouge">PostHogClient</code> directly, just make sure it’s a singleton:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">PostHog</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">posthog</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PostHogClient</span><span class="p">(</span>
  <span class="n">Environment</span><span class="p">.</span><span class="nf">GetEnvironmentVariable</span><span class="p">(</span><span class="s">"PostHog__PersonalApiKey"</span><span class="p">));</span>
</code></pre></div></div>

<h2 id="examples">Examples</h2>

<p>To see all this in action, the <a href="https://github.com/posthog/posthog-dotnet"><code class="language-plaintext highlighter-rouge">posthog-dotnet</code> GitHub repository</a> has a <a href="https://github.com/PostHog/posthog-dotnet/tree/main/samples">samples directory</a> with a growing number of example projects. For example, the <a href="https://github.com/PostHog/posthog-dotnet/tree/main/samples/HogTied.Web">HogTied.Web</a> project is an ASP.NET Core web app that uses PostHog for analytics and feature flags and shows some advanced configuration.</p>

<h2 id="whats-next">What’s next?</h2>

<p>With this release done, I’ll be focusing my attention on the Feature Flags product. Even so, I’ll continue to maintain the SDK and fix any reported bugs.</p>

<p>If anyone reports bugs, I’ll be sure to fix them. But I won’t be adding any new features for the moment.</p>

<p>Down the road, I’m hoping to add a <code class="language-plaintext highlighter-rouge">PostHog.Unity</code> package. I just don’t have a lot of experience with Unity yet. My game development experience mostly consists of getting shot in the face by squeaky voiced kids playing Fortnite. I’m hoping someone will contribute a Unity sample project to the repo which I can use as a starting point.</p>

<p>If you have any feedback, questions, or issues with the PostHog .NET SDK, please reach file an issue at https://github.com/PostHog/posthog-dotnet.</p>]]></content><author><name>Phil Haack</name></author><category term="career" /><category term="work" /><summary type="html"><![CDATA[PostHog is a product analytics platform that helps you build better products faster. It contains a set of tools to help you capture analytics and leverage feature flags.]]></summary></entry><entry><title type="html">New Year, New Job</title><link href="https://haacked.com/archive/2025/01/07/new-year-new-job/" rel="alternate" type="text/html" title="New Year, New Job" /><published>2025-01-07T00:00:00+00:00</published><updated>2025-01-07T00:00:00+00:00</updated><id>https://haacked.com/archive/2025/01/07/new-year-new-job</id><content type="html" xml:base="https://haacked.com/archive/2025/01/07/new-year-new-job/"><![CDATA[<p>Last year I wrote a post, <a href="/archive/2024/12/10/chutes-and-ladder">career chutes and ladders</a>, where I proposed that a linear climb to the C-suite is not the only approach to a satisfying career. At the end of the post, I mentioned I was stepping off the ladder to take on an IC role.</p>

<p><img src="https://github.com/user-attachments/assets/0a825e78-c3e5-4aa4-ac1f-13ba597da4a5" alt="Hedge hog typing on a keyboard" title="Might be easier to code if my arms were longer" /></p>

<p>After over a year of being on a personally funded sabbatical, I started a new job at <a href="https://posthog.com">PostHog</a> as a Senior Product Engineer. This week is my orientation where I get to <a href="https://haacked.com/archive/2007/10/26/drinking-from-the-firehose.aspx/">drink from the firehose</a> once again.</p>

<h2 id="what-is-posthog">What is PostHog?</h2>

<p>Apart from being a company that seems to really love cute hedgehogs, PostHog is an open-source product analytics platform. They have a set of tools to help product engineers build better products. Each product can be used as a standalone tool, but they’re designed to level-up when you put them together.</p>

<p>In particular, I’ve started on the <a href="https://posthog.com/feature-flags">Feature Flags team</a>. Yesterday was my first day of onboarding and so far I really like my team.</p>

<p>Today is day two and I’ve already submitted <a href="https://github.com/PostHog/posthog/pull/27340">a small fix for my first pull request</a>!</p>

<h2 id="why-posthog">Why PostHog?</h2>

<p>When I was looking around at companies, an <a href="https://github.com/phanatic">old buddy from GitHub</a> who worked at PostHog reached out to me and suggested I take a look at this company. He said it reminded him of the good parts of working at GitHub.</p>

<p><a href="https://posthog.com/handbook">Their company handbook</a> really impressed me. What it communicates to me is that this is a remote-friendly company that values transparency, autonomy, and trust. It’s a company that treats its employees like adults and tries to minimize overhead.</p>

<p>Not only that, they’ve embraced a lot of employee-friendly practices. For example, a while back my friend Zach wrote about his <a href="https://zachholman.com/posts/fuck-your-90-day-exercise-window/">distaste for the 90 day exercise window</a>. PostHog <a href="https://posthog.com/handbook/people/compensation">provides a 10-year window</a>. Not only that, they offer employes double trigger acceleration!</p>

<div class="markdown-alert markdown-alert-note"><p class="markdown-alert-title"><svg class="octicon octicon-info" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg> Note</p><p>Double trigger acceleration means if you are let go or forced to leave due to the company being acquired, you receive all of your options at that time</p>
</div>

<p>This is a perk usually only offerered to executives.</p>

<p><em>I should <a href="https://posthog.com/careers">mention we’re hiring</a>! Please mention me if you apply. If we’ve worked together, let me know so I can provide feedback internally.</em></p>

<p>I’m excited to be part of a company that’s small, but growing. The company is at a stage similar to the stage GitHub was at when I joined. This is a team with a strong product engineering culture and I’m excited to contribute what I can and learn from them.</p>

<h2 id="the-challenge">The Challenge</h2>

<p>The other part that’s exciting for me is that I’ll be working in a stack that I don’t have a huge amount of experience with. The front-end is React with TypeScript and the back-end is Django with Python. I’ve done a bit of work in all these technologies except Django. However, I believe my experience with ASP.NET MVC will help me pick up Django quickly.</p>

<p>Not to mention, I’ve always taken the stance that I’m a software engineer, not just a .NET developer. Don’t get me wrong, I love working in .NET. But at the same time, I think it’s healthy for me to get production experience in other stacks. It’ll be an area of personal growth. Not to mention, they don’t quite have a .NET Client SDK yet so once I get settled in, that’s something I’m interested in getting started on.</p>

<h2 id="the-future">The Future</h2>

<p>I’ll share more about my experience here as I get settled in. In the meanwhile, wish me luck!</p>]]></content><author><name>Phil Haack</name></author><category term="career" /><category term="work" /><summary type="html"><![CDATA[Taking the plunge into a new job at PostHog.]]></summary></entry><entry><title type="html">Deserializing JSON to a string or a value</title><link href="https://haacked.com/archive/2024/12/17/string-or-value/" rel="alternate" type="text/html" title="Deserializing JSON to a string or a value" /><published>2024-12-17T00:00:00+00:00</published><updated>2024-12-17T00:00:00+00:00</updated><id>https://haacked.com/archive/2024/12/17/string-or-value</id><content type="html" xml:base="https://haacked.com/archive/2024/12/17/string-or-value/"><![CDATA[<p>I love using <a href="https://github.com/reactiveui/refit">Refit</a> to call web APIs in a nice type-safe manner. Sometimes though, APIs don’t want to cooperate with your strongly-typed hopes. For example, you might run into an API written by a hipster in a beanie, aka a dynamic-type enthusiast. I don’t say that pejoratively. Some of my closest friends write Python and Ruby.</p>

<p>For example, I came across an API that returned a value like this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"important"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>No problem, I defined a class like this to deserialize it to:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ImportantResponse</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">Important</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And life was good. Until that awful day that the API returned this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"important"</span><span class="p">:</span><span class="w"> </span><span class="s2">"What is important is subjective to the viewer."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Damn! This philosophy lesson broke my client. One workaround is to do this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ImportantResponse</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">JsonElement</span> <span class="n">Important</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It works, but it’s not great. It doesn’t communicate to the consumer that this value can only be a <code class="language-plaintext highlighter-rouge">string</code> or a <code class="language-plaintext highlighter-rouge">bool</code>. That’s when I remembered an old blog post from my past.</p>

<p><img src="https://github.com/user-attachments/assets/15a13230-644f-4e5e-827e-c4c35051c77e" alt="A ball of string on the left, &quot;or&quot; in the middle, a present on the right" title="Is it one or is it the other?" /></p>

<h2 id="april-fools-joke-to-the-rescue">April Fool’s Joke to the Rescue</h2>

<p>When I was the Program Manager (PM) for ASP.NET MVC, my colleague and lead developer, Eilon, wrote a blog post entitled <a href="https://asp-blogs.azurewebsites.net/leftslipper/the-string-or-the-cat-a-new-net-framework-library">“The String or the Cat: A New .NET Framework Library</a> where he introduced the class <code class="language-plaintext highlighter-rouge">StringOr&lt;TOther&gt;</code>. This class could represent a dual-state value that’s either a string or another type.</p>

<blockquote>
  <p>The concepts presented here are based on a thought experiment proposed by scientist Erwin Schrödinger. While an understanding of quantum physics will help to understand the new types and APIs, it is not required.</p>
</blockquote>

<p>It turned out his blog post was an April Fool’s joke. But the idea stuck with me. And now, here’s a case where I need a real implementation of it. But I’m going to name mine, <code class="language-plaintext highlighter-rouge">StringOrValue&lt;T&gt;</code>.</p>

<h2 id="a-modern-stringorvaluet">A modern StringOrValue&lt;T&gt;</h2>

<p>One nice thing about implementing this today is we can leverage modern C# features. Here’s the starting implementation:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">JsonConverter</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">StringOrValueConverter</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">readonly</span> <span class="k">struct</span> <span class="nc">StringOrValue</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="p">:</span> <span class="n">IStringOrObject</span> <span class="p">{</span>
    <span class="k">public</span> <span class="nf">StringOrValue</span><span class="p">(</span><span class="kt">string</span> <span class="n">stringValue</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">StringValue</span> <span class="p">=</span> <span class="n">stringValue</span><span class="p">;</span>
        <span class="n">IsString</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="nf">StringOrValue</span><span class="p">(</span><span class="n">T</span> <span class="k">value</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">Value</span> <span class="p">=</span> <span class="k">value</span><span class="p">;</span>
        <span class="n">IsValue</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">T</span><span class="p">?</span> <span class="n">Value</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">StringValue</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">MemberNotNullWhen</span><span class="p">(</span><span class="k">true</span><span class="p">,</span> <span class="k">nameof</span><span class="p">(</span><span class="n">StringValue</span><span class="p">))]</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsString</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">MemberNotNullWhen</span><span class="p">(</span><span class="k">true</span><span class="p">,</span> <span class="k">nameof</span><span class="p">(</span><span class="n">Value</span><span class="p">))]</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsValue</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="c1">/// &lt;summary&gt;</span>
<span class="c1">/// Internal interface for &lt;see cref="StringOrValue{T}"/&gt;.</span>
<span class="c1">/// &lt;/summary&gt;</span>
<span class="c1">/// &lt;remarks&gt;</span>
<span class="c1">/// This is here to make serialization and deserialization easy.</span>
<span class="c1">/// &lt;/remarks&gt;</span>
<span class="p">[</span><span class="nf">JsonConverter</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">StringOrValueConverter</span><span class="p">))]</span>
<span class="k">internal</span> <span class="k">interface</span> <span class="nc">IStringOrObject</span>
<span class="p">{</span>
    <span class="kt">bool</span> <span class="n">IsString</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>

    <span class="kt">bool</span> <span class="n">IsValue</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>

    <span class="kt">string</span><span class="p">?</span> <span class="n">StringValue</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>

    <span class="kt">object</span><span class="p">?</span> <span class="n">ObjectValue</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can use the <code class="language-plaintext highlighter-rouge">MemberNotNullWhen</code> attribute to tell the compiler that when <code class="language-plaintext highlighter-rouge">IsString</code> is true, <code class="language-plaintext highlighter-rouge">StringValue</code> is not null. And when <code class="language-plaintext highlighter-rouge">IsValue</code> is true, <code class="language-plaintext highlighter-rouge">Value</code> is not null. That way, code like this compiles just fine without raising null warnings:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="k">value</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StringOrValue</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;(</span><span class="s">"Hello"</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">IsString</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">StringValue</span><span class="p">.</span><span class="n">Length</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>and</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="k">value</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StringOrValue</span><span class="p">&lt;</span><span class="n">SomeType</span><span class="p">&gt;(</span><span class="m">42</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">IsValue</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It also is decorated with the <code class="language-plaintext highlighter-rouge">JsonConverter</code> attribute to tell the JSON serializer to use the <code class="language-plaintext highlighter-rouge">StringOrValueConverter</code> class to serialize and deserialize this type. I wanted this type to Just Work™. I didn’t want consumers of this class have to bother with registering a <code class="language-plaintext highlighter-rouge">JsonConverterFactory</code> for this type.</p>

<p>This also explains why I introduced the internal <code class="language-plaintext highlighter-rouge">IStringOrObject</code> interface. We can’t implement the <code class="language-plaintext highlighter-rouge">JsonConverter</code> attribute on a open generic type, so we need a non-generic interface to apply the attribute to. It also makes it easier to write the converter as you’ll see.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// &lt;summary&gt;</span>
<span class="c1">/// Value converter for &lt;see cref="StringOrValue{T}"/&gt;.</span>
<span class="c1">/// &lt;/summary&gt;</span>
<span class="k">internal</span> <span class="k">class</span> <span class="nc">StringOrValueConverter</span> <span class="p">:</span> <span class="n">JsonConverter</span><span class="p">&lt;</span><span class="n">IStringOrObject</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="kt">bool</span> <span class="nf">CanConvert</span><span class="p">(</span><span class="n">Type</span> <span class="n">typeToConvert</span><span class="p">)</span>
        <span class="p">=&gt;</span> <span class="n">typeToConvert</span><span class="p">.</span><span class="n">IsGenericType</span>
           <span class="p">&amp;&amp;</span> <span class="n">typeToConvert</span><span class="p">.</span><span class="nf">GetGenericTypeDefinition</span><span class="p">()</span> <span class="p">==</span> <span class="k">typeof</span><span class="p">(</span><span class="n">StringOrValue</span><span class="p">&lt;&gt;);</span>

    <span class="k">public</span> <span class="k">override</span> <span class="n">IStringOrObject</span> <span class="nf">Read</span><span class="p">(</span><span class="k">ref</span> <span class="n">Utf8JsonReader</span> <span class="n">reader</span><span class="p">,</span> <span class="n">Type</span> <span class="n">typeToConvert</span><span class="p">,</span> <span class="n">JsonSerializerOptions</span> <span class="n">options</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">targetType</span> <span class="p">=</span> <span class="n">typeToConvert</span><span class="p">.</span><span class="nf">GetGenericArguments</span><span class="p">()[</span><span class="m">0</span><span class="p">];</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">reader</span><span class="p">.</span><span class="n">TokenType</span> <span class="p">==</span> <span class="n">JsonTokenType</span><span class="p">.</span><span class="n">String</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">stringValue</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="nf">GetString</span><span class="p">();</span>
            <span class="k">return</span> <span class="n">stringValue</span> <span class="k">is</span> <span class="k">null</span>
                <span class="p">?</span> <span class="nf">CreateEmptyInstance</span><span class="p">(</span><span class="n">targetType</span><span class="p">)</span>
                <span class="p">:</span> <span class="nf">CreateStringInstance</span><span class="p">(</span><span class="n">targetType</span><span class="p">,</span> <span class="n">stringValue</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="k">value</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="k">ref</span> <span class="n">reader</span><span class="p">,</span> <span class="n">targetType</span><span class="p">,</span> <span class="n">options</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">value</span> <span class="k">is</span> <span class="k">null</span>
            <span class="p">?</span> <span class="nf">CreateEmptyInstance</span><span class="p">(</span><span class="n">targetType</span><span class="p">)</span>
            <span class="p">:</span> <span class="nf">CreateValueInstance</span><span class="p">(</span><span class="n">targetType</span><span class="p">,</span> <span class="k">value</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="n">ConstructorInfo</span> <span class="nf">GetEmptyConstructor</span><span class="p">(</span><span class="n">Type</span> <span class="n">targetType</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">typeof</span><span class="p">(</span><span class="n">StringOrValue</span><span class="p">&lt;&gt;)</span>
                   <span class="p">.</span><span class="nf">MakeGenericType</span><span class="p">(</span><span class="n">targetType</span><span class="p">).</span>
                   <span class="nf">GetConstructor</span><span class="p">([])</span>
               <span class="p">??</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">$"No constructor found for StringOrValue&lt;</span><span class="p">{</span><span class="n">targetType</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">&gt;."</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="n">ConstructorInfo</span> <span class="nf">GetConstructor</span><span class="p">(</span><span class="n">Type</span> <span class="n">targetType</span><span class="p">,</span> <span class="n">Type</span> <span class="n">argumentType</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">typeof</span><span class="p">(</span><span class="n">StringOrValue</span><span class="p">&lt;&gt;)</span>
            <span class="p">.</span><span class="nf">MakeGenericType</span><span class="p">(</span><span class="n">targetType</span><span class="p">).</span>
            <span class="nf">GetConstructor</span><span class="p">([</span><span class="n">argumentType</span><span class="p">])</span>
            <span class="p">??</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">$"No constructor found for StringOrValue&lt;</span><span class="p">{</span><span class="n">targetType</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">&gt;."</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="n">IStringOrObject</span> <span class="nf">CreateEmptyInstance</span><span class="p">(</span><span class="n">Type</span> <span class="n">targetType</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">ctor</span> <span class="p">=</span> <span class="nf">GetEmptyConstructor</span><span class="p">(</span><span class="n">targetType</span><span class="p">);</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">IStringOrObject</span><span class="p">)</span><span class="n">ctor</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">([]);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="n">IStringOrObject</span> <span class="nf">CreateStringInstance</span><span class="p">(</span><span class="n">Type</span> <span class="n">targetType</span><span class="p">,</span> <span class="kt">string</span> <span class="k">value</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">ctor</span> <span class="p">=</span> <span class="nf">GetConstructor</span><span class="p">(</span><span class="n">targetType</span><span class="p">,</span> <span class="k">typeof</span><span class="p">(</span><span class="kt">string</span><span class="p">));</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">IStringOrObject</span><span class="p">)</span><span class="n">ctor</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">([</span><span class="k">value</span><span class="p">]);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="n">IStringOrObject</span> <span class="nf">CreateValueInstance</span><span class="p">(</span><span class="n">Type</span> <span class="n">targetType</span><span class="p">,</span> <span class="kt">object</span> <span class="k">value</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">ctor</span> <span class="p">=</span> <span class="nf">GetConstructor</span><span class="p">(</span><span class="n">targetType</span><span class="p">,</span> <span class="n">targetType</span><span class="p">);</span>
        <span class="k">return</span> <span class="p">(</span><span class="n">IStringOrObject</span><span class="p">)</span><span class="n">ctor</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">([</span><span class="k">value</span><span class="p">]);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Write</span><span class="p">(</span><span class="n">Utf8JsonWriter</span> <span class="n">writer</span><span class="p">,</span> <span class="n">IStringOrObject</span> <span class="k">value</span><span class="p">,</span> <span class="n">JsonSerializerOptions</span> <span class="n">options</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">IsString</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">writer</span><span class="p">.</span><span class="nf">WriteStringValue</span><span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">StringValue</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="k">value</span><span class="p">.</span><span class="n">IsValue</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">Serialize</span><span class="p">(</span><span class="n">writer</span><span class="p">,</span> <span class="k">value</span><span class="p">.</span><span class="n">ObjectValue</span><span class="p">,</span> <span class="n">options</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">else</span>
        <span class="p">{</span>
            <span class="n">writer</span><span class="p">.</span><span class="nf">WriteNullValue</span><span class="p">();</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the actual implementation of <code class="language-plaintext highlighter-rouge">StringOrValue&lt;T&gt;</code>, I implemented IEquatable&lt;T&gt;, IEquatable&lt;StringOrValue&lt;T&gt;&gt; and overrode the implicit operators:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">implicit</span> <span class="k">operator</span> <span class="n">StringOrValue</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="kt">string</span> <span class="n">stringValue</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="k">new</span><span class="p">(</span><span class="n">stringValue</span><span class="p">);</span>
<span class="k">public</span> <span class="k">static</span> <span class="k">implicit</span> <span class="k">operator</span> <span class="n">StringOrValue</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">T</span> <span class="k">value</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="k">new</span><span class="p">(</span><span class="k">value</span><span class="p">);</span>
</code></pre></div></div>

<p>This allows you to write code like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">StringOrValue</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="n">valueAsString</span> <span class="p">=</span> <span class="s">"Hello"</span><span class="p">;</span>
<span class="n">StringOrValue</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="n">valueAsNumber</span> <span class="p">=</span> <span class="m">42</span><span class="p">;</span>

<span class="n">Assert</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">,</span> <span class="n">valueAsString</span><span class="p">);</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="m">42</span><span class="p">,</span> <span class="n">valueAsNumber</span><span class="p">);</span>
</code></pre></div></div>

<p>So with this implementation in place, I can go back to the original example and write this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ImportantResponse</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">StringOrValue</span><span class="p">&lt;</span><span class="kt">bool</span><span class="p">&gt;</span> <span class="n">Important</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And now I can handle both cases:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">ImportantResponse</span><span class="p">&gt;(</span><span class="n">json</span><span class="p">)</span>
    <span class="p">??</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">"Deserialization failed."</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Important</span><span class="p">.</span><span class="n">IsValue</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Important</span><span class="p">.</span><span class="n">Value</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"It's important!"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">else</span> <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"It's not important."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Important</span><span class="p">.</span><span class="n">StringValue</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It’s time to go shopping for a beanie!</p>

<p>Here’s <a href="https://gist.github.com/haacked/2fd1f8f0818c27184f2d08704f6f06f6">the full implementation</a> for those interested in using this in your own projects!</p>

<script src="https://gist.github.com/haacked/2fd1f8f0818c27184f2d08704f6f06f6.js"></script>]]></content><author><name>Phil Haack</name></author><category term="csharp json" /><summary type="html"><![CDATA[You may want to deserialize JSON to strongly typed values, but sometimes you run into a situation where the API doesn't comply, until now.]]></summary></entry><entry><title type="html">Career Chutes and Ladder</title><link href="https://haacked.com/archive/2024/12/10/chutes-and-ladder/" rel="alternate" type="text/html" title="Career Chutes and Ladder" /><published>2024-12-10T00:00:00+00:00</published><updated>2024-12-10T00:00:00+00:00</updated><id>https://haacked.com/archive/2024/12/10/chutes-and-ladder</id><content type="html" xml:base="https://haacked.com/archive/2024/12/10/chutes-and-ladder/"><![CDATA[<p>The career ladder is a comforting fiction we’re sold as we embark on our careers: Start as Junior, climb to Senior, then Principal, Director, and VP. One day, you defeat the final boss and receive a key to the executive bathroom and and join the C-suite. You’ve made it!</p>

<p>If you’re lucky, your company supports a tall Individual Contributor (IC) ladder. But often, the next rung swaps your hoodie for a power shirt, suspenders, and management duties.</p>

<p><img src="https://github.com/user-attachments/assets/71945726-4bc8-494f-b076-22f0518f24d4" alt="Lumbergh from office space" title="Yeah, I'm going to need you to come in on Saturday" /></p>

<p>My first job followed this script: developer to team lead to Senior Manager. I was hustling up that ladder.</p>

<p>But life isn’t linear. Careers are more like Chutes and Ladders (which the Brits know as Snakes and Ladders, but that messes up my metaphor). Lucky breaks shoot you up; surprises send you sliding down. Those chutes? Not failures. Opportunities.</p>

<p><img src="https://github.com/user-attachments/assets/3b66f137-fd6b-4c3c-aedd-0c46701657fb" alt="The game of chutes and ladders" /></p>

<p>Take my journey. I took a chute from my first job back to an IC role at my second. Climbed back up into management. Took a chute to Microsoft as a Senior Program Manager. Then came another chute: GitHub.</p>

<p>I started as a developer, became a manager, then Director of Engineering. After that, I co-founded a startup, took the CTO title, but spent most of my time writing code.</p>

<p>It looked like the classic ladder climb, but in startups, titles are smoke and mirrors. So it only counted as a step up if we succeeded. <a href="https://haacked.com/archive/2023/11/13/failure/">Spoiler: we didn’t.</a></p>

<h2 id="ladders-are-overrated">Ladders Are Overrated</h2>

<p>Ladders are narrow and rigid. Titles—while shiny—hide what matters: the work. They’re bumper stickers for your career. Nice for signaling, but they don’t tell the whole story.</p>

<p>As Director, I mentored teams and focused on broad initiatives. But over time, I started to miss the tech. The longer I stayed out of writing production code, the more disconnected I felt. As a CTO, I got back to coding and loved it. When the startup failed, I promised myself at least a year off to reflect before jumping into something new.</p>

<h2 id="growth-on-the-slide">Growth on the Slide</h2>

<p>Over a year later, I’ve been thinking about my next move. It turns out nobody will pay me to be a man of leisure.</p>

<p>Conventional wisdom (and ego) says aim higher: VP of Software Development or CTO at an established company.</p>

<p>The irony of big titles is they mean more power at work, but less power over your time.</p>

<p>For me, my recent life circumstances make time autonomy among my top priorities. Sure, big titles pay well, but maximizing income isn’t my goal.</p>

<p>Here’s what I realized: By my own definition, I’ve succeeded. I’ve been part of great teams, built great products, and helped grow companies. I have nothing left to prove. I don’t need a lofty title or a giant paycheck (but I wouldn’t turn one down either).</p>

<p>So, once again, I’m setting aside my pride and stepping off the ladder. Next year on January 6th I start a new IC role. Details to come, but I’m thrilled to be back in the trenches, building and learning. It’s a place that treats its employees like adults and gives me the autonomy to structure my day as I see fit.</p>

<p>This isn’t about rejecting leadership. It’s about recalibrating. Leadership is broad. It’s guiding organizations or leading by example. I think it’s healthy—even advantageous—to bounce between IC and management roles over the course of a career. Life changes. I might return to management someday. Or not. The point is to stay open to what fits now.</p>

<h2 id="your-move">Your Move</h2>

<p>If you’re staring at a chute, wondering if stepping off the ladder will hurt you, consider this: maybe it’s not a setback. Maybe it’s a shortcut to what you really want.</p>

<p>Careers aren’t about perfect titles. They’re about collecting experiences, relationships, and skills that shape you. Sometimes, the most important moves don’t look like progress—until you’re somewhere unexpected, doing work that matters.</p>

<p>Spin the dial. Take the slide. Even in Chutes and Ladders, the winner isn’t who climbs highest. It’s who enjoys the game. At least, that’s what I told my friends when they beat me.</p>]]></content><author><name>Phil Haack</name></author><category term="career" /><summary type="html"><![CDATA[Sometimes stepping off the career ladder is the best move you can make.]]></summary></entry><entry><title type="html">Supercharge your debugging with git bisect</title><link href="https://haacked.com/archive/2024/11/11/git-bisect/" rel="alternate" type="text/html" title="Supercharge your debugging with git bisect" /><published>2024-11-11T00:00:00+00:00</published><updated>2024-11-11T00:00:00+00:00</updated><id>https://haacked.com/archive/2024/11/11/git-bisect</id><content type="html" xml:base="https://haacked.com/archive/2024/11/11/git-bisect/"><![CDATA[<p>Ever look for a recipe online only to scroll through a self-important rambling 10-page essay about a trip to Tuscany that inspired the author to create the recipe? Finally, after wearing out your mouse, trackpad, or Page Down key to scroll to the end, you get to the actual recipe. I hate those.</p>

<p>So I’ll spare you the long scroll and start this post with a <code class="language-plaintext highlighter-rouge">git bisect</code> cheat sheet and then I’ll tell you about my trip to hell that lead me to write this post.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git bisect start
<span class="nv">$ </span>git bisect bad                 <span class="c"># Current version is bad</span>
<span class="nv">$ </span>git bisect good v2.6.13-rc2    <span class="c"># v2.6.13-rc2 is known to be good</span>
<span class="nb">.</span> <span class="c"># Repeat git bisect [good|bad] until you find the commit that introduced the bug</span>
<span class="nv">$ </span>git bisect reset
</code></pre></div></div>

<p><img src="https://github.com/user-attachments/assets/0b9e8bd0-e26d-490a-9642-fcc9cc49e2bc" alt="bisect" /></p>

<h2 id="the-one-where-poor-phil-is-stumped">The One Where Poor Phil is Stumped</h2>

<p>Like Groot, I was stumped.</p>

<p>I’m learning Blazor by building a simple app. After a while of working in a feature branch, I decided to test a logged out scenario.</p>

<p>When I tried to log back in, the login page was stuck in an infinite redirect loop.</p>

<p>I tried everything I could think of. I found every line of code that did a redirect and put a breakpoint in it, none of them were hit. I allowed anonymous on the login page. I tried playing with authorization policies. No dice. I asked Copilot for help. It offered some support and good advice, but it led nowhere. I even sacrificed two chickens and a goat, but not even the denizens of the seven hells could help me.</p>

<p>I switched back to my <code class="language-plaintext highlighter-rouge">main</code> branch to see if the bug was there, and lo and behold it was! That meant this bug had been in the code for a while and I hadn’t noticed because I was always logged in.</p>

<p>Often, when faced with such a bug, you might go on a divide and conquer mission. Start removing code you think might be related and see if the bug goes away. But in my case, that would encompass a large search area because I had no idea what the cause was or where to start cutting.</p>

<p>It was clear to me that some cross-cutting concern was causing this bug. I needed to find the commit that introduced it to reduce the scope of my search. Enter <code class="language-plaintext highlighter-rouge">git bisect</code>.</p>

<h2 id="git-bisect-to-the-rescue">Git Bisect to the Rescue</h2>

<p>From <a href="https://git-scm.com/docs/git-bisect">the docs</a>:</p>

<blockquote>
  <p>This command uses a binary search algorithm to find which commit in your project’s history introduced a bug. You use it by first telling it a “bad” commit that is known to contain the bug, and a “good” commit that is known to be before the bug was introduced. Then git bisect picks a commit between those two endpoints and asks you whether the selected commit is “good” or “bad”. It continues narrowing down the range until it finds the exact commit that introduced the change.</p>
</blockquote>

<p>The key thing to note here is that it’s a binary search. So even if the span of commits you’re searching is 128 commits, it’ll take at most 7 steps to find the commit that introduced the bug (<code>2<sup>7</sup> = 128</code>).</p>

<p>Here’s how I used it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git bisect start <span class="c"># Get the ball rolling.</span>
<span class="nv">$ </span>git bisect bad   <span class="c"># The current commit is bad.</span>
</code></pre></div></div>

<p>Now I need to supply the last known good commit. That could be a search of its own, but usually you have a good idea. For example, you might know the last release was good so you use the tag for that release.</p>

<p>In my case, I found the commit, <code class="language-plaintext highlighter-rouge">543ada5</code>, where I first implemented the login page because I know it worked then. Yes, I do <a href="https://haacked.com/archive/2013/03/04/test-better.aspx/">test my own code</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git bisect good 543ada5
Bisecting: 7 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 3 steps<span class="o">)</span>
<span class="o">[</span>9736e3f90b571bebf512c2acb1f7ef14f3a77df4] Update all the NPMs
</code></pre></div></div>

<p>After calling <code class="language-plaintext highlighter-rouge">git bisect good</code> with the known good commit, git bisect picked a commit between the bad and good commit, <code class="language-plaintext highlighter-rouge">9736e3f</code>.</p>

<p>I tested that commit and it turns out the bug wasn’t there! So I told git bisect that commit was good.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>git bisect good
Bisecting: 3 revisions left to <span class="nb">test </span>after this <span class="o">(</span>roughly 2 steps<span class="o">)</span>
<span class="o">[</span>b9db65316a7f569c3ef9ed1eb4caa2072a6ba5d8] Show guests on Details page
</code></pre></div></div>

<p>After a few more iterations of this, git bisect found the commit that introduced the bug.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4e08eb48956b80a7a33987df272d30acb5bd6ee2 is the first bad commit
commit 4e08eb48956b80a7a33987df272d30acb5bd6ee2
Author: Phil Haack &lt;haacked@gmail.com&gt;
Date:   Thu Oct 10 15:38:21 2024 <span class="nt">-0700</span>
</code></pre></div></div>

<p>That commit seemed pretty innocuous but I did notice something odd.</p>

<p>I made this change in the <code class="language-plaintext highlighter-rouge">App.razor</code> file because I was tired of adding render mode <code class="language-plaintext highlighter-rouge">InteractiveServer</code> to nearly every page.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- &lt;Routes /&gt;
</span><span class="gi">+ &lt;Routes @rendermode="InteractiveServer" /&gt;
</span></code></pre></div></div>

<p>It turns out, this change wasn’t exactly wrong, just incomplete. I can save the proper fix as a follow-up post. I’m annoyed that Copilot wasn’t able to offer up the eventual solution because I found it by googling around.</p>

<p>Now that I found the culprit, I can get back to my original state before running <code class="language-plaintext highlighter-rouge">git bisect</code> by calling <code class="language-plaintext highlighter-rouge">git bisect reset</code>.</p>

<h2 id="challenges-with-git-bisect">Challenges with Git Bisect</h2>

<p>I encourage you to read the docs on <code class="language-plaintext highlighter-rouge">git bisect</code> as there are other sub-commands that are important.</p>

<p>For example, sometimes a commit cannot be tested, such as a broken build. In that case, you can call <code class="language-plaintext highlighter-rouge">git bisect skip</code> to skip that commit.</p>

<p>In practice, I found cases where you have to do a bit of tweaking to get the commit to run. For example, one commit had the following build error:</p>

<blockquote>
  <p>error NU1903: Warning As Error: Package ‘System.Text.Json’ 8.0.4 has a known high severity vulnerability</p>
</blockquote>

<p>At the time that I wrote that commit, everything built fine. Since I only want to build and test locally, I ignored that warning in order to test the commit.</p>

<h2 id="automating-git-bisect">Automating Git Bisect</h2>

<p>The reason I bring up these challenges is <code class="language-plaintext highlighter-rouge">git bisect</code> has the potential to be automated. You could write a script that builds and tests each commit. If a commit fails to build or test, the script could call <code class="language-plaintext highlighter-rouge">git bisect skip</code> for you.</p>

<p>For example, it’d be nice to do something like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git bisect run dotnet <span class="nb">test</span>
</code></pre></div></div>

<p>This would run <code class="language-plaintext highlighter-rouge">dotnet test</code> on each commit and automatically call <code class="language-plaintext highlighter-rouge">git bisect skip</code> if the test fails.</p>

<p>However, in practice, it doesn’t work as well as you’d like. Commits that were fine when you wrote them might not build any longer. Also, what you really probably want to do is inject a new test to be run during the git bisect process.</p>

<p>Not to mention if you have integration tests that hit the database. You’d have to have migrations run up and down during the process of <code class="language-plaintext highlighter-rouge">git bisect</code>.</p>

<p>I’ve considered building tooling that would solve these problems, but in my experience, so few .NET developers I know make regular use of <code class="language-plaintext highlighter-rouge">git bisect</code> that it’s hard to justify the effort. Maybe this post will convince you to add this tool to your repoirtoire.</p>]]></content><author><name>Phil Haack</name></author><category term="git" /><summary type="html"><![CDATA[Git bisect is an underrated but very powerful tool to include in your debugging toolbox. In short, it helps you find the commit that introduced a bug. Here's an example of how to use it.]]></summary></entry></feed>