Last active
July 9, 2025 21:12
-
-
Save kch/e4d5859516af51c85938a52b238d3488 to your computer and use it in GitHub Desktop.
reword commits inline in interactive rebase file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# git-reword: Interactive tool for bulk editing commit messages' first line | |
# | |
# Opens the rebase todo sequence for editing, amends any commit messages with | |
# the inline-reworded message. Other rebase operations are performed normally. | |
# | |
# If you reword inline but also use a reword action on the same line, the | |
# inline edit wins. | |
# | |
# Usage: | |
# git-reword [base-ref] # Open the rebase todo for editing with inline rewording | |
# | |
# Examples: | |
# git-reword # Last 10 commits | |
# git-reword HEAD~5 # Last 5 commits | |
# git-reword main # All commits since main branch | |
require "shellwords" | |
def die msg=nil, code=1 | |
$stderr.puts "#{File.basename($0)}: #{msg}" if msg | |
exit code | |
end | |
git_dir = `git rev-parse --git-dir`.chomp | |
die "Can't find git directory" unless $?.success? | |
ARGV << "HEAD~10" if ARGV.empty? | |
# Get the actual rebase file that git would show by capturing it | |
todo_content = `GIT_SEQUENCE_EDITOR=cat git rebase --quiet --interactive #{ARGV.shelljoin} < /dev/null`.chomp | |
exit 1 unless $?.success? | |
die "No commits to rebase", 0 if todo_content.empty? || todo_content.start_with?("noop") | |
def parse_lines(rebase_todo) = rebase_todo.lines.map(&:strip).grep_v(/^#|^$/).map{ it.split(/ +/, 3) } | |
# Parse original commits to track message changes later | |
original_commits = parse_lines todo_content | |
# Let user edit the rebase file | |
reword_dir = "#{git_dir}/reword" | |
todo_file = "#{reword_dir}/git-rebase-todo" | |
Dir.mkdir reword_dir unless Dir.exist? reword_dir | |
File.write todo_file, todo_content | |
system "#{ENV["EDITOR"]||"vim"} #{todo_file.shellescape}" | |
reworded_commits = parse_lines File.read todo_file | |
# Find commits where messages changed - match by SHA | |
original_by_sha = original_commits.to_h { it[1..] } | |
reworded_by_sha = reworded_commits.to_h { it[1..] } | |
reworded_by_sha = reworded_by_sha.slice(*original_by_sha.keys).reject { |sha, msg| original_by_sha[sha] == msg } | |
# Build final rebase script with exec commands for message changes | |
lines = [] | |
reworded_commits.each do |commit| | |
action, sha, message = commit | |
lines << "#{action} #{sha} #{message}" | |
next unless reworded_by_sha[sha] | |
message.sub!(/^# /, "") # git 2.50 adds # marks before msgs | |
original_body = `git log --max-count=1 --format=%b #{sha} --`.chomp | |
# If single line, inline it. Otherwise use temp file. | |
if original_body.empty? | |
lines << "exec git commit --amend --message='#{message.gsub(%['], %['"'"'])}'" | |
else | |
msg_file = "#{reword_dir}/COMMIT_EDITMSG_#{sha}" | |
File.write(msg_file, message + "\n\n" + original_body) | |
lines << "exec git commit --amend --file=#{msg_file.shellescape} && rm #{msg_file.shellescape} # #{message}" | |
end | |
end | |
rebase_script = lines.join("\n") | |
# # Execute the rebase with our modified script | |
IO.popen("GIT_SEQUENCE_EDITOR='cat >' git rebase --interactive #{ARGV.shelljoin}", "w") { it << rebase_script } | |
exit if $?.success? | |
die unless Dir.exist?("#{git_dir}/rebase-merge") # rebase in progress from error caused by todo contents | |
die "Rebase failed. Run 'git rebase --abort' to cancel." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment