<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[TienNg21's Newsletter: Learning]]></title><description><![CDATA[Learning Everything]]></description><link>https://news.tienng21.com/s/learning-everything</link><image><url>https://substackcdn.com/image/fetch/$s_!XJnB!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b0151a8-bb4b-4122-9bf7-d8067b59d966_1254x1254.png</url><title>TienNg21&apos;s Newsletter: Learning</title><link>https://news.tienng21.com/s/learning-everything</link></image><generator>Substack</generator><lastBuildDate>Wed, 28 Oct 2026 01:30:11 GMT</lastBuildDate><atom:link href="https://news.tienng21.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[TienNg21]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[nghuutien0912@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[nghuutien0912@substack.com]]></itunes:email><itunes:name><![CDATA[TienNg21]]></itunes:name></itunes:owner><itunes:author><![CDATA[TienNg21]]></itunes:author><googleplay:owner><![CDATA[nghuutien0912@substack.com]]></googleplay:owner><googleplay:email><![CDATA[nghuutien0912@substack.com]]></googleplay:email><googleplay:author><![CDATA[TienNg21]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Using git commit fixup to simplify commit squashing]]></title><description><![CDATA[Stop manually squashing commits &#8212; use --fixup with autosquash to clean up your history automatically]]></description><link>https://news.tienng21.com/p/using-git-commit-fixup-to-simplify</link><guid isPermaLink="false">https://news.tienng21.com/p/using-git-commit-fixup-to-simplify</guid><dc:creator><![CDATA[TienNg21]]></dc:creator><pubDate>Fri, 21 Mar 2025 17:19:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!XJnB!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b0151a8-bb4b-4122-9bf7-d8067b59d966_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We've all been there. You're working on a feature branch, making small incremental commits with messages like "wip", "fix typo", "update imports". Before you know it, you have 15 commits for what should logically be one clean PR. Squashing these commits manually is tedious and error-prone.</p><p>Enter <code>git commit --fixup</code> &#8212; a powerful flag that, combined with <code>git rebase --autosquash</code>, lets Git automatically arrange and squash your messy commits into a clean history. Here's how to use it.</p><h2>What is git commit --fixup?</h2><p><code>git commit --fixup</code> creates a new commit that is explicitly marked as a fixup for an earlier commit. The key difference from <code>--amend</code>:</p><ul><li><p><code>git commit --amend</code>: rewrites the most recent commit in place (changes its hash).</p></li><li><p><code>git commit --fixup</code>: creates a <em>new</em> commit prefixed with <code>fixup!</code>, which <code>rebase --autosquash</code> will later merge into its target automatically.</p></li></ul><p>When combined with <code>git rebase -i --autosquash</code>, you can:</p><ul><li><p>Add forgotten files to an earlier commit</p></li><li><p>Fix typos or bugs that belong to a previous commit</p></li><li><p>Squash multiple related commits into one &#8212; automatically, without manual reordering</p></li></ul><h2>The Traditional Problem</h2><p>Consider this common scenario:</p><pre><code><code>git commit -m "wip: add authentication"
git commit -m "fix: typo in auth"
git commit -m "add tests for auth"
git commit -m "update auth logic"
git commit -m "fix off-by-one error"
git commit -m "wip: add authorization"
git commit -m "refactor auth module"</code></code></pre><p>Seven commits for what should be one cohesive "Add authentication" feature. Without <code>--fixup</code>, you'd open <code>git rebase -i</code> and manually mark each commit as <code>squash</code> or <code>fixup</code> &#8212; repetitive and easy to get wrong.</p><h2>The Basic Fixup Workflow</h2><p>Here's the pattern:</p><pre><code><code># 1. Make your original commit
git commit -m "feat: add user authentication"

# 2. Later, realise you forgot something &#8212; add it as a fixup
git add password-reset.ts
git commit --fixup=HEAD

# 3. When ready to clean up, run autosquash
git rebase -i --autosquash HEAD~2</code></code></pre><p>The <code>--fixup=HEAD</code> flag creates a commit named <code>fixup! feat: add user authentication</code>. When you run <code>rebase --autosquash</code>, Git automatically moves that commit directly after its target and marks it <code>fixup</code> in the interactive editor &#8212; you just save and close.</p><h2>Step-by-Step: Fixup with autosquash</h2><p>Say your log looks like this:</p><pre><code><code>abc1234 feat: implement login
def5678 fixup! feat: implement login
ghi9012 wip: update docs
jkl3456 fixup! feat: implement login</code></code></pre><p>Run:</p><pre><code><code>git rebase -i --autosquash HEAD~4</code></code></pre><p>Git automatically opens the editor pre-arranged as:</p><pre><code><code>pick abc1234 feat: implement login
fixup def5678 fixup! feat: implement login
fixup jkl3456 fixup! feat: implement login
pick ghi9012 wip: update docs</code></code></pre><p>Save and close &#8212; Git squashes all three login commits into one and leaves the docs commit untouched. No manual reordering needed.</p><h2>Real-World Example: Bug Fix with Multiple Passes</h2><pre><code><code># Initial fix
git commit -m "fix: handle null pointer in user service"

# Test reveals another case
vim user.service.ts
git add user.service.ts
git commit --fixup=HEAD

# Add a regression test
git add user.test.ts
git commit --fixup=HEAD

# Log now shows:
# abc1234 fix: handle null pointer in user service
# def5678 fixup! fix: handle null pointer in user service
# ghi9012 fixup! fix: handle null pointer in user service

# Clean up
git rebase -i --autosquash HEAD~3
# All three combined into one clean commit automatically</code></code></pre><h2>Real-World Example: Multi-Step Feature</h2><p>You want this final history:</p><ul><li><p><code>feat: add authentication system</code></p></li><li><p><code>refactor: clean up auth flow</code></p></li></ul><p>But your current log is:</p><pre><code><code>f47ac8 refactor: clean up auth flow
ba3d92 wip: add logout
8c7e1a wip: add token refresh
4a5b6c fix: handle edge case
9c8d7e wip: add login form
2a3b4c wip: setup auth context</code></code></pre><p>Run <code>git rebase -i HEAD~6</code> and arrange:</p><pre><code><code>pick   2a3b4c wip: setup auth context
squash 9c8d7e wip: add login form
fixup  4a5b6c fix: handle edge case
fixup  8c7e1a wip: add token refresh
squash ba3d92 wip: add logout
pick   f47ac8 refactor: clean up auth flow</code></code></pre><p>Git squashes the first five into one, opens the editor so you can write a clean message like <code>feat: add authentication system</code>, then keeps the refactor commit separate.</p><h2>Best Practices</h2><h3>Set autosquash globally</h3><p>Save yourself from typing <code>--autosquash</code> every time:</p><pre><code><code>git config --global rebase.autoSquash true</code></code></pre><p>Now <code>git rebase -i HEAD~N</code> automatically honours <code>fixup!</code> commits.</p><h3>Write good original messages</h3><p>Since fixup commits inherit the target's message, a descriptive original message means you won't need to edit anything when squashing:</p><pre><code><code># Bad &#8212; you'll need to rewrite this during squash
git commit -m "fix"

# Good &#8212; already describes the final state
git commit -m "fix: handle null pointer in auth service when user not found"</code></code></pre><h3>Test after rebasing</h3><pre><code><code>git rebase -i --autosquash HEAD~5
npm test
# If tests fail, recover from reflog:
git reflog
git reset --hard HEAD@{1}</code></code></pre><h3>Back up before large rebases</h3><pre><code><code>git branch backup-before-cleanup
git rebase -i HEAD~10
# If something goes wrong:
git reset --hard backup-before-cleanup</code></code></pre><h2>Common Pitfalls</h2><ul><li><p><strong>Don't rebase public history.</strong> If commits are already on a shared branch, use merge instead. Check first: <code>git log origin/main..HEAD</code>. If the output is empty, don't rebase.</p></li><li><p><strong>Force-push safely.</strong> After rebasing a feature branch, use <code>git push --force-with-lease</code> instead of <code>--force</code> to avoid overwriting a teammate's push.</p></li><li><p><strong>Don't squash too aggressively.</strong> Three focused commits reviewable on their own are better than one giant commit that's impossible to review.</p></li><li><p><strong>Resolve conflicts carefully.</strong> If a conflict arises mid-rebase, edit the file, stage it with <code>git add</code>, then <code>git rebase --continue</code>. If it gets messy, <code>git rebase --abort</code> returns you to where you started.</p></li></ul><h2>Troubleshooting</h2><p><strong>Git doesn't recognise </strong><code>--fixup</code><strong>:</strong> ensure you're on Git 2.24 or later (<code>git --version</code>).</p><p><strong>Squashed commit has the wrong message:</strong></p><pre><code><code>git commit --amend   # if it's the most recent commit
# or
git rebase -i HEAD~2  # to edit an earlier commit interactively</code></code></pre><p><strong>Unexpected conflicts during rebase:</strong> abort, review what changed between the commits you're squashing, and try again with a tighter range.</p><h2>Summary</h2><p><code>git commit --fixup</code> combined with <code>rebase --autosquash</code> is the fastest way to keep your commit history clean without the manual overhead of interactive rebase every time. The workflow is simple: commit normally, mark corrections with <code>--fixup</code> as you go, then run one <code>rebase --autosquash</code> command before opening your PR. Git does the reordering and squashing for you.</p>]]></content:encoded></item><item><title><![CDATA[Understanding Git DAG]]></title><description><![CDATA[A mental model that turns Git from a set of memorized commands into a predictable, powerful tool you can reason about]]></description><link>https://news.tienng21.com/p/understanding-git-dag</link><guid isPermaLink="false">https://news.tienng21.com/p/understanding-git-dag</guid><dc:creator><![CDATA[TienNg21]]></dc:creator><pubDate>Sat, 25 Jan 2025 06:25:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!XJnB!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5b0151a8-bb4b-4122-9bf7-d8067b59d966_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every time you run <code>git commit</code>, <code>git merge</code>, or <code>git rebase</code>, you&#8217;re manipulating a Directed Acyclic Graph (DAG). Yet most developers interact with Git through commands without understanding what&#8217;s happening beneath the surface. This abstraction works fine until something breaks &#8212; you encounter a merge conflict, accidentally rewrite public history, or find yourself in &#8220;detached HEAD&#8221; state with no idea how you got there.</p><p>Understanding Git&#8217;s DAG structure transforms Git from a set of memorized commands into a predictable, powerful tool. Instead of asking &#8220;what command do I run?&#8221; you&#8217;ll ask &#8220;what structure do I want to create?&#8221; This mental model helps you reason about complex situations, debug issues, and perform advanced operations with confidence.</p><p>In this guide, we&#8217;ll explore Git&#8217;s internal graph structure from first principles. You&#8217;ll learn how commits, branches, and references fit together, how to visualize the DAG, and how to manipulate it safely.</p><h2>What Is Git&#8217;s DAG?</h2><p>Git stores your project&#8217;s history as a Directed Acyclic Graph where commits are nodes and parent relationships are edges. Let&#8217;s break down what this actually means and why it matters.</p><h3>What Makes It a DAG?</h3><p>A Directed Acyclic Graph has three defining properties: <strong>direction</strong> (edges point from child to parent), <strong>no cycles</strong> (you can&#8217;t follow parent pointers back to a commit you&#8217;ve already visited), and <strong>connectivity</strong> (every commit except the initial one has at least one parent). These aren&#8217;t just mathematical trivia &#8212; they fundamentally shape how Git works.</p><p>The direction of edges means there&#8217;s always a clear &#8220;backward&#8221; path from any commit to the beginning of history. When you run <code>git log</code>, Git follows these parent pointers backward, showing you increasingly older commits. The absence of cycles guarantees this process terminates. And connectivity ensures every commit is traceable back to the project&#8217;s origin.</p><p>Consider what happens when you create a merge commit with two parents. Git stores this as a single node with two edges pointing backward, one to each parent. The graph structure makes parallel development natural and explicit.</p><h3>Git&#8217;s Object Storage</h3><p>Git implements this DAG using four core object types: <strong>blobs</strong> (file contents), <strong>trees</strong> (directory listings), <strong>commits</strong> (snapshots with metadata), and <strong>tags</strong> (pointers to commits). Each commit object contains a SHA-1 hash, the tree hash, parent commit hashes, timestamps, and the commit message.</p><p>The hash uniquely identifies the commit based on its content &#8212; if anything changes, the hash changes. This content-addressable storage means Git can detect corruption and guarantee integrity. Here&#8217;s what a commit object looks like internally:</p><pre><code><code>tree 3a4b5c6d7e8f0g1h2i3j4k5l6m7n8o9p0q1r2s
parent 9z8y7x6w5v4u3t2s1r0q9o8n7m6l5k4j3i2h1g
author Jane Developer &lt;jane@example.com&gt; 1735689600 +0000
committer Jane Developer &lt;jane@example.com&gt; 1735689600 +0000

Add user authentication feature</code></code></pre><p>The parent reference creates the edge in our DAG. This is why Git&#8217;s history is immutable &#8212; you can&#8217;t change a commit&#8217;s parent without changing its hash, which would require updating all descendant commits.</p><h2>References, Branches, and HEAD</h2><p>References are human-readable names that point to commits. The most common reference is a branch, which is simply a <strong>movable pointer</strong> to a commit. When you create a branch with <code>git branch feature-x</code>, Git creates a file at <code>.git/refs/heads/feature-x</code> containing the SHA-1 hash of the current commit.</p><p>HEAD is a special reference that points to the branch you&#8217;re currently working on. When you make a new commit, Git updates the branch HEAD points to, moving the branch reference forward. This design explains why branching in Git is instant and cheap &#8212; creating a branch just means writing a 41-byte file containing a commit hash.</p><p>Other important references include:</p><ul><li><p>Remote-tracking branches (<code>refs/remotes/origin/main</code>) track remote branches</p></li><li><p>Tags point to specific commits and don&#8217;t move</p></li><li><p>The reflog records every position HEAD has occupied</p></li><li><p>Stashes are commits pointed to by special refs</p></li></ul><p>When you delete a branch with <code>git branch -d</code>, Git checks whether the branch&#8217;s commits are reachable from another reference. If not, it warns you. These commits still exist in the object database until garbage collection, but they&#8217;re no longer accessible through normal references.</p><h2>Visualizing the Graph</h2><p>The most basic visualization is <code>git log --graph --oneline --all</code>, which shows commits as nodes with lines connecting them:</p><pre><code><code>* a1b2c3d (HEAD -&gt; main) Fix critical bug
| * e5f6g7h (feature-x) Add new feature
|/
* d4e5f6g Initial commit</code></code></pre><p>When reading the graph, look for important patterns:</p><ul><li><p><strong>Linear chains</strong> represent sequential development</p></li><li><p><strong>Merge commits</strong> have multiple parents (two lines converging)</p></li><li><p><strong>Branch points</strong> occur where divergent commits share a parent</p></li><li><p><strong>Orphaned commits</strong> with no path from HEAD will be garbage collected</p></li></ul><p>More sophisticated tools like <code>gitk</code>, <code>tig</code>, or GitKraken make these patterns even more explicit.</p><h2>Safe vs. Dangerous Operations</h2><p>The key to working confidently with Git is understanding which operations rewrite history and which only add new commits.</p><h3>Safe Operations (Add New Commits)</h3><p>Any operation that adds new commits without changing existing ones is safe, even for shared history. <strong>Merging</strong> is the canonical example:</p><pre><code><code>git checkout main
git merge feature-branch</code></code></pre><p>This creates a new commit with two parents. No existing commits are modified, so this is safe even if others have based work on main.</p><p><strong>Cherry-picking</strong> also creates new commits rather than modifying existing ones:</p><pre><code><code>git cherry-pick abc1234</code></code></pre><p>Git creates a new commit containing the same changes but with a different hash (different parent, different timestamp). The original commit remains untouched.</p><h3>Dangerous Operations (Rewrite History)</h3><p>Rebasing rewrites history by creating new commits and abandoning old ones. This is powerful but dangerous for shared history:</p><pre><code><code>git checkout feature-branch
git rebase main</code></code></pre><p>This finds the merge base between feature-branch and main, then recreates each commit with main as the new parent. Anyone who built work on the original commits will need to reconcile &#8212; the old commits still exist in the reflog, but they&#8217;re no longer referenced by a branch name.</p><p>Interactive rebasing gives you surgical control:</p><pre><code><code>git rebase -i HEAD~5</code></code></pre><p>This opens an editor showing the last five commits, letting you reorder, squash, edit, or drop them. Each action rewrites history.</p><h2>Recovery: The Reflog Is Your Safety Net</h2><p>When things go wrong, the reflog records every position HEAD has occupied &#8212; including commits you&#8217;ve &#8220;lost&#8221; through rebasing or branch deletion:</p><pre><code><code># Show everywhere HEAD has been
git reflog

# Restore to a previous state
git reset --hard HEAD@{5}</code></code></pre><p>Git garbage collection won&#8217;t remove commits still in the reflog, giving you a window to recover mistakes. Before panicking about lost work, always check the reflog first.</p><p><strong>Detached HEAD state</strong> occurs when HEAD points directly to a commit rather than a branch reference. Any commits you create in this state aren&#8217;t referenced by a branch name and can be lost if you switch away. To preserve them:</p><pre><code><code>git checkout -b rescue-branch</code></code></pre><h2>Key Takeaways</h2><ul><li><p><strong>Git stores history as a DAG</strong> where commits are nodes and parent references are directed edges. This structure provides integrity, enables efficient operations, and makes parallel development explicit.</p></li><li><p><strong>References are movable pointers to commits.</strong> Branches are simply references, and HEAD is a special reference pointing to your current location. Understanding this clarifies branch operations and detached HEAD.</p></li><li><p><strong>Safe operations add new commits without modifying existing ones.</strong> Merging, cherry-picking, and branching are safe even for shared history because they only extend the graph.</p></li><li><p><strong>Rebasing rewrites history by creating new commits.</strong> Use rebase on local feature branches; avoid it on public branches others have based work on.</p></li><li><p><strong>The reflog provides recovery from mistakes.</strong> Every position HEAD occupies is recorded. Before panicking about lost work, check the reflog.</p></li></ul><p>Git&#8217;s DAG structure isn&#8217;t just an implementation detail &#8212; it&#8217;s the foundation for everything Git does. Next time you encounter a merge conflict, a confusing rebase, or an unexpected Git state, visualize the graph. Ask yourself: what does the graph look like right now? What references point where? What operation will produce the structure I want? This mental model will serve you through every Git challenge you face.</p>]]></content:encoded></item></channel></rss>