-
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 invokinggitdirectly, staging commits withgit addandgit add -p, often selecting files using a fuzzy finder. The article highlightedlazygit’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 --amendThis 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 endCode:
#!/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 fiAfter only a few days of use, I can already see me using this regularly.
-
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-policyWhile 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 -
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
-
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:
- I do most of my writing in my editor: Notes, blog posts, drafts of long emails or messages, etc.
- 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: projectClaude 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 -
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.