git/list[1] front-page[2] threads[3] people[4] search[5] about
 

git-fixup-assigner.perl -- automatically decide where to "fixup!"

From
Thomas Rast <trast@student.ethz.ch>
Date
Dec 14, 2010, 02:09 UTC
Message-ID
<201012140309.59378.trast@student.ethz.ch>

While cleaning up the 'log -L' series I gathered a large number of little fixups, and decided it would be smart if git could automatically figure out where to put them.

It works like this:
* Split the diff by hunk.  I'm using -U1 here for finer splits, but it
  could be tunable.
* For each hunk, run blame to find out which commit's lines were
  affected.
* Group the hunks by this commit, and output them with a suitable
  command to make a fixup.
My git-fixup is
  $ g config alias.fixup
  !sh -c 'r=$1; git commit -m"fixup! $(git log -1 --pretty=%s $r)"' -
so that is "suitable".
You would run it with the changes unstaged in your tree as
  ./git-fixup-assigner.perl > fixups

and can then review with 'less fixups', or run 'sh fixups' to commit them.

It's certainly not perfect, notably the detection logic should ignore context, but it got the job done.

--- 8< --- #!/usr/bin/perl

use warnings; use strict;

sub parse_hunk_header {
        my ($line) = @_;
        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
            $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
        $o_cnt = 1 unless defined $o_cnt;
        $n_cnt = 1 unless defined $n_cnt;
        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
}
sub find_commit {
	my ($file, $begin, $end) = @_;
	my $blame;
	open($blame, '-|', 'git', '--no-pager', 'blame', 'HEAD', "-L$begin,$end", $file) or die;
	my %candidate;
	while (<$blame>) {
		$candidate{$1} += 1 if /^([0-9a-f]+)/;
	}
	close $blame or die;
	my @sorted = sort { $candidate{$b} <=> $candidate{$a} } keys %candidate;
	if (1 < scalar @sorted) {
		print STDERR "ambiguous split $file:$begin..$end\n";
		foreach my $c (@sorted) {
			print STDERR "\t$candidate{$c}\t$c\n";
		}
	}
	return $sorted[0];
}

my $diff; open($diff, '-|', 'git', '--no-pager', 'diff', '-U1') or die;

my %by_commit; my @cur_hunk = (); my $cur_commit; my ($filename, $prefilename, $postfilename);

while (<$diff>) {
        if (m{^diff --git ./(.*) ./\1$}) {
		if (@cur_hunk) {
			push @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;
			@cur_hunk = ();
		}
		$filename = $1;
                $prefilename = "./" . $1;
                $postfilename = "./" . $1;
	} elsif (m{^index}) {
		# ignore
        } elsif (m{^new file}) {
		$prefilename = '/dev/null';
        } elsif (m{^delete file}) {
		$postfilename = '/dev/null';
        } elsif (m{^--- $prefilename$}) {
        } elsif (m{^\+\+\+ $postfilename$}) {
        } elsif (m{^@@ }) {
		if (@cur_hunk) {
			push @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;
			@cur_hunk = ();
		}
		push @cur_hunk, $_;
		die "I don't handle this diff" if ($prefilename ne $postfilename);
                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt)
                        = parse_hunk_header($_);
                my $o_end = $o_ofs + $o_cnt - 1;
		$cur_commit = find_commit($filename, $o_ofs, $o_end);
        } elsif (m{^[-+ \\]}) {
		push @cur_hunk, $_;
	} else {
		die "unhandled diff line: '$_'";
	}
}
close $diff or die;
if (@cur_hunk) {
	push @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;
	@cur_hunk = ();
}
print "#!/bin/sh\n\n";
foreach my $commit (keys %by_commit) {
	print "git apply --cached <<EOF\n";
	foreach my $filename (keys %{$by_commit{$commit}}) {
		print "diff --git a/$filename b/$filename\n";
		print "--- a/$filename\n";
		print "+++ b/$filename\n";
		print @{$by_commit{$commit}{$filename}};
	}
	print "EOF\n\n";
	print "git fixup $commit\n\n";
}
--- >8 ---
-- 
Thomas Rast
trast@{inf,student}.ethz.ch
Next: fREW Schmidt
Message 1 of 3 in “git-fixup-assigner.perl -- automatically decide where to "fixup!"”
  1. Thomas RastDec 14, 2010
  2. fREW SchmidtOct 26, 2011
  3. Thomas RastOct 26, 2011

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.