• Git: Amend any commit

    I recently read The (lazy) Git UI You Didn’t Know You Need and experimented with lazygit. My usual workflow involves invoking git directly, staging commits with git add and git add -p, often selecting files using a fuzzy finder. The article highlighted lazygit’s ability to amend any commit, not just the most recent one, which sparked an idea.

    Current Workflow for Amending Commits

    To amend a commit, I first stage the changes. Then, when I want to amend the last commit, I use:

    $ git commit --amend
    

    This opens my editor with the previous commit message, which I usually accept and close without change.

    Amending a previous commit is more involved. After staging the changes I:

    $ git commit --fixup <sha>
    

    (The sha is typically picked through fuzzy finding as well).

    Then I rebase to actually apply the fixup those two commits:

    $ git rebase -i --autosquash <earlier-sha>
    

    The <earlier-sha> is also selected with a fuzzy finder, and must be a commit prior to the one being fixed.

    I think we can do better.

    git-fix

    I realized much of this could be automated. I prompted an agent to create a script for me that would do the following:

    # Amends the last commit with staged changes
    $ git fix
    # Amends the specific commit with staged changes
    $ git fix <sha>
    

    What about conflicts? Abort and leave the repo in the same state as it was before the command.

    I also instructed it to use TDD, which I believe effectively guided the agent.

    Specs:

    require "spec_helper"
    require "open3"
    require "tmpdir"
    require "fileutils"
    
    RSpec.describe "git-fix" do
      let(:script_path) { File.expand_path("../settings/.bin/git-fix", __dir__) }
    
      around do |example|
        Dir.mktmpdir do |dir|
          Dir.chdir(dir) do
            # Initialize a git repo
            system("git init --quiet")
            system("git config user.email 'test@example.com'")
            system("git config user.name 'Test User'")
            system("git config commit.gpgsign false")
    
            example.run
          end
        end
      end
    
      it "amends a commit with staged changes" do
        # Create initial commit
        File.write("file.txt", "line 1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial commit'")
    
        # Create a second commit that we'll want to fix
        File.write("file.txt", "line 1\nline 2\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add line 2'")
        target_commit = `git rev-parse HEAD`.strip
    
        # Now we want to fix the second commit (no commits after it)
        File.write("file.txt", "line 1\nline 2 fixed\n")
        system("git add file.txt")
    
        # Run git-fix
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be(true), "Command failed with output: #{output}"
    
        # Verify the commit was amended
        fixed_content = `git show HEAD:file.txt`
        expect(fixed_content).to eq("line 1\nline 2 fixed\n")
    
        # Verify commit count is still 2
        commit_count = `git rev-list --count HEAD`.strip.to_i
        expect(commit_count).to eq(2)
      end
    
      it "works with short commit references" do
        # Create initial commit
        File.write("file.txt", "initial\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial'")
    
        # Create target commit
        File.write("file.txt", "initial\nv1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add v1'")
        target_commit = `git rev-parse --short HEAD`.strip
    
        # Stage a fix
        File.write("file.txt", "initial\nv1 fixed\n")
        system("git add file.txt")
    
        # Run git-fix with short SHA
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be true
        expect(File.read("file.txt")).to eq("initial\nv1 fixed\n")
      end
    
      it "amends a middle commit when changes don't overlap" do
        # Create initial commit with two files
        File.write("file1.txt", "content 1\n")
        File.write("file2.txt", "content 2\n")
        system("git add .")
        system("git commit --quiet -m 'Initial commit'")
    
        # Modify file1 in second commit
        File.write("file1.txt", "content 1 modified\n")
        system("git add file1.txt")
        system("git commit --quiet -m 'Modify file1'")
        target_commit = `git rev-parse HEAD`.strip
    
        # Modify file2 in third commit (different file, no overlap)
        File.write("file2.txt", "content 2 modified\n")
        system("git add file2.txt")
        system("git commit --quiet -m 'Modify file2'")
    
        # Now fix the second commit (file1)
        File.write("file1.txt", "content 1 fixed\n")
        system("git add file1.txt")
    
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be(true), "Command failed with output: #{output}"
    
        # Verify both files have correct content
        expect(File.read("file1.txt")).to eq("content 1 fixed\n")
        expect(File.read("file2.txt")).to eq("content 2 modified\n")
    
        # Verify commit count is still 3
        commit_count = `git rev-list --count HEAD`.strip.to_i
        expect(commit_count).to eq(3)
      end
    
      it "defaults to HEAD when no commit is specified" do
        # Create initial commit
        File.write("file.txt", "line 1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial commit'")
    
        # Create second commit
        File.write("file.txt", "line 1\nline 2\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add line 2'")
    
        # Stage changes to fix the last commit (HEAD)
        File.write("file.txt", "line 1\nline 2 fixed\n")
        system("git add file.txt")
    
        # Run git-fix without specifying commit (should default to HEAD)
        output, status = Open3.capture2e(script_path)
    
        expect(status.success?).to be true
    
        # Verify the last commit was amended
        expect(File.read("file.txt")).to eq("line 1\nline 2 fixed\n")
    
        # Verify commit count is still 2
        commit_count = `git rev-list --count HEAD`.strip.to_i
        expect(commit_count).to eq(2)
      end
    
      it "shows error when nothing is staged" do
        File.write("file.txt", "content\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial'")
        target_commit = `git rev-parse HEAD`.strip
    
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be false
        expect(output).to match(/nothing.*staged/i)
      end
    
      it "aborts rebase and restores staged changes on conflict" do
        # Create initial commit
        File.write("file.txt", "line 1\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Initial commit'")
    
        # Create second commit
        File.write("file.txt", "line 1\nline 2\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Add line 2'")
        target_commit = `git rev-parse HEAD`.strip
    
        # Create third commit that modifies the same line
        File.write("file.txt", "line 1\nline 2 modified\n")
        system("git add file.txt")
        system("git commit --quiet -m 'Modify line 2'")
    
        # Try to fix the second commit with a conflicting change
        File.write("file.txt", "line 1\nline 2 different fix\n")
        system("git add file.txt")
    
        # Run git-fix (this should fail due to conflict)
        output, status = Open3.capture2e(script_path, target_commit)
    
        expect(status.success?).to be false
        expect(output).to match(/conflict/i)
        expect(output).to match(/aborted/i)
    
        # Verify we're not in the middle of a rebase
        expect(Dir.exist?(".git/rebase-merge")).to be false
        expect(Dir.exist?(".git/rebase-apply")).to be false
    
        # Verify the fixup commit was removed
        last_commit_msg = `git log -1 --pretty=%B`.strip
        expect(last_commit_msg).to eq("Modify line 2")
    
        # Verify the changes are still staged
        staged_diff = `git diff --cached --name-only`.strip
        expect(staged_diff).to eq("file.txt")
    
        # Verify the staged content is what we wanted to fix
        staged_content = `git diff --cached file.txt`
        expect(staged_content).to include("-line 2 modified")
        expect(staged_content).to include("+line 2 different fix")
      end
    end
    

    Code:

    #!/usr/bin/env bash
    # Amends the referenced commit with the code currently staged
    # Usage:
    #   git fix [commit-sha-reference]
    #
    # If no commit reference is provided, defaults to HEAD (the last commit).
    #
    # This script uses git's --fixup and --autosquash features to amend a commit
    # in your history. It works best when the changes don't overlap with subsequent
    # commits. If there are conflicts during the rebase, you'll need to resolve them
    # manually.
    
    set -euo pipefail
    
    # Default to HEAD if no commit reference is provided
    commit_ref="${1:-HEAD}"
    
    # Check if there are staged changes
    if git diff --cached --quiet; then
      echo "Error: Nothing is staged. Stage your changes with 'git add' first." >&2
      exit 1
    fi
    
    # Resolve the commit reference to a SHA before creating the fixup
    # This is important because creating the fixup will change where HEAD points
    target_commit=$(git rev-parse "$commit_ref")
    
    # Create a fixup commit
    git commit --fixup="$target_commit"
    
    # Get the parent of the target commit to use as rebase base
    rebase_base=$(git rev-parse "$target_commit^")
    
    # Rebase with autosquash (non-interactive)
    # If the rebase fails, we need to clean up
    if ! GIT_SEQUENCE_EDITOR=: git rebase --autosquash -i "$rebase_base" 2>&1; then
      echo "" >&2
      echo "Error: Rebase failed due to conflicts." >&2
      echo "Aborting rebase and restoring your staged changes..." >&2
    
      # Abort the rebase
      git rebase --abort
    
      # Uncommit the fixup commit but keep changes staged
      git reset --soft HEAD~1
    
      echo "Aborted. Your changes are still staged." >&2
      echo "Please resolve conflicts manually or modify your changes." >&2
      exit 1
    fi
    

    After only a few days of use, I can already see me using this regularly.

    Read on →

  • The REPL: Issue 134 - October 2025

    Vibing a Non-Trivial Ghostty Feature

    Mitchell Hashimoto’s extensive post details his use of AI to code a Ghostty1 feature, mirroring my own experience with AI. It excels at prototyping and boilerplate, and is good at explaining existing code. However, it requires constant supervision; I frequently tweak and guide its output. Sometimes, it misses the mark entirely and gets stuck.

    Abstraction, not syntax

    Alternative configuration formats solve superficial problems. Configuration languages solve the deeper problem: the need for abstraction.

    Abstraction, while simplifying expression, comes at the the cost of generating the configuration file, as the article explains. However, the author omits that YAML supports references, offering a degree of abstraction:

    # Define reusable base configurations
    .defaults: &defaults
      region: "eu-west"
    
    .lifecycle_policies:
      hourly: &hourly-policy
        delete_after_seconds: 345600  # 4 days
      daily: &daily-policy
        delete_after_seconds: 2592000  # 30 days
      monthly: &monthly-policy
        delete_after_seconds: 31536000  # 365 days
    
    buckets:
      - <<: *defaults
        name: "alpha-hourly"
        lifecycle_policy: *hourly-policy
      - <<: *defaults
        name: "alpha-daily"
        lifecycle_policy: *daily-policy
      - <<: *defaults
        name: "alpha-monthly"
        lifecycle_policy: *monthly-policy
      - <<: *defaults
        name: "bravo-hourly"
        lifecycle_policy: *hourly-policy
      - <<: *defaults
        name: "bravo-daily"
        lifecycle_policy: *daily-policy
      - <<: *defaults
        name: "bravo-monthly"
        lifecycle_policy: *monthly-policy
    

    While this is not a while loop, it does remove the repetition, and the need to check each of the values multiple times.

    Locating Elements in Hash Arrays Using Pattern Matching in Ruby

    Pattern matching in Ruby is still relatively new. I’ve only seen it used sparingly, but this use is concise. I’m still mulling over the syntax. Assigning to feels weird. A few years ago, when numbered parameters (e.g., collection.each { _1.do_something }) were introduced, I didn’t care much for them. Now, I am on a team that uses them constantly and the syntax has grown on me. Perhaps pattern matching like this will take hold in time.

    system = {
      users: [
        { username: 'alice', role: 'admin', email: 'alice@example.com' },
        { username: 'bob', role: 'user', email: 'bob@example.com' },
        { username: 'charlie', role: 'moderator', email: 'charlie@example.com' }
      ]
    }
    
    system => {users: [*, { role: 'moderator', email: }, *]}
    puts email # charlie@example.com
    
    1. Ghostty is an excellent terminal emulator. After using iTerm 2 for years, I tried ghostty and haven’t looked back. 

    Read on →

  • The REPL: Issue 133 - September 2025

    Yet another LLM rant

    Dennis Schubert discussed LLMs – like half the internet.

    LLMs can be a useful tool, maybe. But don’t anthropomorphize them. They don’t know anything, they don’t think, they don’t learn, they don’t deduct.

    This is true. Although they can fake it quite a bit, especially agents that break down the next steps, read files, and come up with a list of todos, etc.

    I find myself thinking that AI is both unreasonably hyped-up and incredibly useful. I’ve been able to get proof-of-concept projects working in minutes with only superficial knowledge of the underlying technologies. Denying that agentic AI is powerful seems foolish. Also, thinking that they can replace engineers wholesale is equally foolish.

    We are in for a time of disruption.

    Pick the wrong tool for the job

    Sometimes the wrong tool for the job is right for you at a given moment in time. In this case, the author wanted to use Ruby to be able to iterate faster. Being able to learn what the users really want was more important than having the perfect technical solution

    Read on →

  • Improving Writing with AI: A Zinsser-Inspired Approach

    A few years ago, I read “On Writing Well” by William Zinsser to improve my technical writing skills. Last week, it occurred to me that I could ask an agent to proofread using Zinsser’s principles. I particularly like Zinsser’s style because I want my writing to be clear, effective, and low on fluff.

    Proofreading and improving text is something any chatbot can do well. I particularly like using a coding agent for this because:

    1. I do most of my writing in my editor: Notes, blog posts, drafts of long emails or messages, etc.
    2. Coding agents know how to show you a diff between the text you are writing and the suggestions from the AI. I can tweak and keep what I like. I previously tried chatbots, but it was hard for me to quickly see what was changed.

    At the moment, I am experimenting with both Roo and claude.

    Roo custom mode:

    customModes:
      - slug: writing-well
        name: ✍️ Writing Well
        roleDefinition: You are Roo Code, a writing specialist who applies the
          principles from William Zinsser's "On Writing Well" to eliminate clutter,
          ensure clarity, improve simplicity, and strengthen unity in text. You
          focus on word choice, sentence structure, style, voice, and technical
          issues like grammar and punctuation.
    
          When using em-dashes, prefer using "--" with spaces before and after
        whenToUse: Use this mode when you need to proofread text and apply the
          principles of "On Writing Well" to improve its clarity, conciseness, and
          style. This mode is suitable for any type of text, including articles,
          essays, reports, and blog posts.
        description: Apply principles from Zinsser's "On Writing Well."
        groups:
          - read
          - edit
        source: project
    

    Claude configuration (in ~/.claude/CLAUDE.md):

    # Writing Mode Instructions
    When asked to write, proofread or edit text, apply principles from William Zinsser's "On Writing Well":
    - Eliminate clutter and unnecessary words
    - Ensure clarity and simplicity
    - Strengthen unity and flow
    - Focus on word choice and sentence structure
    - Use "--" with spaces for em-dashes
    - Address grammar and punctuation issues
    

    Read on →

  • The REPL: Issue 132 - August 2025

    Stop concatenating URLs with strings — Use proper tools instead

    I am glad someone wrote this. I point this out in code review all the time. Now I can send them this link.

    If you are using ActiveSupport (e.g. On a Rails project), the query generation can be made easier with #to_query:

    require "active_support/all"
    
    query_params = { format: "json", include: "profile" }
    URI.encode_www_form(query_params)
    # => "format=json&include=profile"
    
    query_params.to_query
    # => "format=json&include=profile"
    

    I’m sold. Agentic coding is the future of web application development

    Nate Berkopec is a smart guy, and he points out that agentic coding is a game changer. I feel it too. Once I started using Roo, it felt like there is no going back. I am still working out how to best work with it and improve it, learning how to prompt it and thinking about TDD. Overall, it has certainly been a boost, especially in architectural planning and coding; it’s been very effective.

    Some folks make it seem like we won’t write any code anymore. I don’t think that is true. I find myself tweaking nearly all of the agent’s suggestions. However, it certainly feels like a big shift in how we code.

    The New Skill in AI is Not Prompting, It’s Context Engineering

    The article resonates with my usage of AI: To get good answers, you need to give good context to the LLM. Tell it what to do, what files are relevant, and what parts of the code you expect it to touch. Very importantly, tell it what not to do in the task.

    Read on →