{"thread":{"id":"26049","subject":"git-fixup-assigner.perl -- automatically decide where to \"fixup!\"","startedAt":"2010-12-14T02:09:59Z","lastAt":"2011-10-26T19:40:50Z","messageCount":3,"participants":["Thomas Rast","fREW Schmidt"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"158026","messageId":"201012140309.59378.trast@student.ethz.ch","threadId":"26049","inReplyTo":null,"subject":"git-fixup-assigner.perl -- automatically decide where to \"fixup!\"","fromName":"Thomas Rast","fromEmail":"trast@student.ethz.ch","sentAt":"2010-12-14T02:09:59Z","receivedAt":"2010-12-14T02:09:59Z","isPatch":false,"sender":{"key":"tr@thomasrast.ch","avatar":"https://avatars.githubusercontent.com/u/153510?v=4"},"body":"While cleaning up the 'log -L' series I gathered a large number of\nlittle fixups, and decided it would be smart if git could\nautomatically figure out where to put them.\n\nIt works like this:\n\n* Split the diff by hunk.  I'm using -U1 here for finer splits, but it\n  could be tunable.\n\n* For each hunk, run blame to find out which commit's lines were\n  affected.\n\n* Group the hunks by this commit, and output them with a suitable\n  command to make a fixup.\n\nMy git-fixup is\n\n  $ g config alias.fixup\n  !sh -c 'r=$1; git commit -m\"fixup! $(git log -1 --pretty=%s $r)\"' -\n\nso that is \"suitable\".\n\nYou would run it with the changes unstaged in your tree as\n\n  ./git-fixup-assigner.perl > fixups\n\nand can then review with 'less fixups', or run 'sh fixups' to commit\nthem.\n\nIt's certainly not perfect, notably the detection logic should ignore\ncontext, but it got the job done.\n\n--- 8< ---\n#!/usr/bin/perl\n\nuse warnings;\nuse strict;\n\nsub parse_hunk_header {\n        my ($line) = @_;\n        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =\n            $line =~ /^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/;\n        $o_cnt = 1 unless defined $o_cnt;\n        $n_cnt = 1 unless defined $n_cnt;\n        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);\n}\n\nsub find_commit {\n\tmy ($file, $begin, $end) = @_;\n\tmy $blame;\n\topen($blame, '-|', 'git', '--no-pager', 'blame', 'HEAD', \"-L$begin,$end\", $file) or die;\n\tmy %candidate;\n\twhile (<$blame>) {\n\t\t$candidate{$1} += 1 if /^([0-9a-f]+)/;\n\t}\n\tclose $blame or die;\n\tmy @sorted = sort { $candidate{$b} <=> $candidate{$a} } keys %candidate;\n\tif (1 < scalar @sorted) {\n\t\tprint STDERR \"ambiguous split $file:$begin..$end\\n\";\n\t\tforeach my $c (@sorted) {\n\t\t\tprint STDERR \"\\t$candidate{$c}\\t$c\\n\";\n\t\t}\n\t}\n\treturn $sorted[0];\n}\n\nmy $diff;\nopen($diff, '-|', 'git', '--no-pager', 'diff', '-U1') or die;\n\nmy %by_commit;\nmy @cur_hunk = ();\nmy $cur_commit;\nmy ($filename, $prefilename, $postfilename);\n\nwhile (<$diff>) {\n        if (m{^diff --git ./(.*) ./\\1$}) {\n\t\tif (@cur_hunk) {\n\t\t\tpush @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;\n\t\t\t@cur_hunk = ();\n\t\t}\n\t\t$filename = $1;\n                $prefilename = \"./\" . $1;\n                $postfilename = \"./\" . $1;\n\t} elsif (m{^index}) {\n\t\t# ignore\n        } elsif (m{^new file}) {\n\t\t$prefilename = '/dev/null';\n        } elsif (m{^delete file}) {\n\t\t$postfilename = '/dev/null';\n        } elsif (m{^--- $prefilename$}) {\n        } elsif (m{^\\+\\+\\+ $postfilename$}) {\n        } elsif (m{^@@ }) {\n\t\tif (@cur_hunk) {\n\t\t\tpush @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;\n\t\t\t@cur_hunk = ();\n\t\t}\n\t\tpush @cur_hunk, $_;\n\t\tdie \"I don't handle this diff\" if ($prefilename ne $postfilename);\n                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt)\n                        = parse_hunk_header($_);\n                my $o_end = $o_ofs + $o_cnt - 1;\n\t\t$cur_commit = find_commit($filename, $o_ofs, $o_end);\n        } elsif (m{^[-+ \\\\]}) {\n\t\tpush @cur_hunk, $_;\n\t} else {\n\t\tdie \"unhandled diff line: '$_'\";\n\t}\n}\n\nclose $diff or die;\n\nif (@cur_hunk) {\n\tpush @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;\n\t@cur_hunk = ();\n}\n\nprint \"#!/bin/sh\\n\\n\";\n\nforeach my $commit (keys %by_commit) {\n\tprint \"git apply --cached <<EOF\\n\";\n\tforeach my $filename (keys %{$by_commit{$commit}}) {\n\t\tprint \"diff --git a/$filename b/$filename\\n\";\n\t\tprint \"--- a/$filename\\n\";\n\t\tprint \"+++ b/$filename\\n\";\n\t\tprint @{$by_commit{$commit}{$filename}};\n\t}\n\tprint \"EOF\\n\\n\";\n\tprint \"git fixup $commit\\n\\n\";\n}\n--- >8 ---\n\n-- \nThomas Rast\ntrast@{inf,student}.ethz.ch\n"},{"id":"178321","messageId":"CADVrmKT1woYpJoe=L9sAbQ30TUw44zMc7y4WF=PMrT5Gj9kDNw@mail.gmail.com","threadId":"26049","inReplyTo":"201012140309.59378.trast@student.ethz.ch","subject":"Re: git-fixup-assigner.perl -- automatically decide where to \"fixup!\"","fromName":"fREW Schmidt","fromEmail":"frioux@gmail.com","sentAt":"2011-10-26T14:37:56Z","receivedAt":"2011-10-26T14:37:56Z","isPatch":false,"sender":{"key":"frioux@gmail.com","avatar":"https://gravatar.com/avatar/639ef3cc221c7f2c225568697bc9d42be4d4e3bada6b4a3b9c32685548f3c105?d=mp&s=160"},"body":"On Mon, Dec 13, 2010 at 8:09 PM, Thomas Rast <trast@student.ethz.ch> wrote:\n>\n> While cleaning up the 'log -L' series I gathered a large number of\n> little fixups, and decided it would be smart if git could\n> automatically figure out where to put them.\n>\n> It works like this:\n>\n> * Split the diff by hunk.  I'm using -U1 here for finer splits, but it\n>  could be tunable.\n>\n> * For each hunk, run blame to find out which commit's lines were\n>  affected.\n>\n> * Group the hunks by this commit, and output them with a suitable\n>  command to make a fixup.\n>\n> My git-fixup is\n>\n>  $ g config alias.fixup\n>  !sh -c 'r=$1; git commit -m\"fixup! $(git log -1 --pretty=%s $r)\"' -\n>\n> so that is \"suitable\".\n>\n> You would run it with the changes unstaged in your tree as\n>\n>  ./git-fixup-assigner.perl > fixups\n>\n> and can then review with 'less fixups', or run 'sh fixups' to commit\n> them.\n>\n> It's certainly not perfect, notably the detection logic should ignore\n> context, but it got the job done.\n>\n> --- 8< ---\n> #!/usr/bin/perl\n>\n> use warnings;\n> use strict;\n>\n> sub parse_hunk_header {\n>        my ($line) = @_;\n>        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =\n>            $line =~ /^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/;\n>        $o_cnt = 1 unless defined $o_cnt;\n>        $n_cnt = 1 unless defined $n_cnt;\n>        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);\n> }\n>\n> sub find_commit {\n>        my ($file, $begin, $end) = @_;\n>        my $blame;\n>        open($blame, '-|', 'git', '--no-pager', 'blame', 'HEAD', \"-L$begin,$end\", $file) or die;\n>        my %candidate;\n>        while (<$blame>) {\n>                $candidate{$1} += 1 if /^([0-9a-f]+)/;\n>        }\n>        close $blame or die;\n>        my @sorted = sort { $candidate{$b} <=> $candidate{$a} } keys %candidate;\n>        if (1 < scalar @sorted) {\n>                print STDERR \"ambiguous split $file:$begin..$end\\n\";\n>                foreach my $c (@sorted) {\n>                        print STDERR \"\\t$candidate{$c}\\t$c\\n\";\n>                }\n>        }\n>        return $sorted[0];\n> }\n>\n> my $diff;\n> open($diff, '-|', 'git', '--no-pager', 'diff', '-U1') or die;\n>\n> my %by_commit;\n> my @cur_hunk = ();\n> my $cur_commit;\n> my ($filename, $prefilename, $postfilename);\n>\n> while (<$diff>) {\n>        if (m{^diff --git ./(.*) ./\\1$}) {\n>                if (@cur_hunk) {\n>                        push @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;\n>                        @cur_hunk = ();\n>                }\n>                $filename = $1;\n>                $prefilename = \"./\" . $1;\n>                $postfilename = \"./\" . $1;\n>        } elsif (m{^index}) {\n>                # ignore\n>        } elsif (m{^new file}) {\n>                $prefilename = '/dev/null';\n>        } elsif (m{^delete file}) {\n>                $postfilename = '/dev/null';\n>        } elsif (m{^--- $prefilename$}) {\n>        } elsif (m{^\\+\\+\\+ $postfilename$}) {\n>        } elsif (m{^@@ }) {\n>                if (@cur_hunk) {\n>                        push @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;\n>                        @cur_hunk = ();\n>                }\n>                push @cur_hunk, $_;\n>                die \"I don't handle this diff\" if ($prefilename ne $postfilename);\n>                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt)\n>                        = parse_hunk_header($_);\n>                my $o_end = $o_ofs + $o_cnt - 1;\n>                $cur_commit = find_commit($filename, $o_ofs, $o_end);\n>        } elsif (m{^[-+ \\\\]}) {\n>                push @cur_hunk, $_;\n>        } else {\n>                die \"unhandled diff line: '$_'\";\n>        }\n> }\n>\n> close $diff or die;\n>\n> if (@cur_hunk) {\n>        push @{$by_commit{$cur_commit}{$filename}}, @cur_hunk;\n>        @cur_hunk = ();\n> }\n>\n> print \"#!/bin/sh\\n\\n\";\n>\n> foreach my $commit (keys %by_commit) {\n>        print \"git apply --cached <<EOF\\n\";\n>        foreach my $filename (keys %{$by_commit{$commit}}) {\n>                print \"diff --git a/$filename b/$filename\\n\";\n>                print \"--- a/$filename\\n\";\n>                print \"+++ b/$filename\\n\";\n>                print @{$by_commit{$commit}{$filename}};\n>        }\n>        print \"EOF\\n\\n\";\n>        print \"git fixup $commit\\n\\n\";\n> }\n> --- >8 ---\n\nThis is super neat, but I'm having trouble getting it to work.\n\nFirst, I made one small change:\n\n  -        print \"git fixup $commit\\n\\n\";\n  +       print \"git commit --fixup $commit\\n\\n\";\n\nTo try it out I made a very simple change:\n\n  $ git diff\n  diff --git a/App/lib/MyApp/Controller/DashboardTemplates.pm\nb/App/lib/MyApp/Controller/DashboardTemplates.pm\n  index aefdc3c..a53b534 100644\n  --- a/App/lib/MyApp/Controller/DashboardTemplates.pm\n  +++ b/App/lib/MyApp/Controller/DashboardTemplates.pm\n  @@ -13,7 +13,7 @@ cat_has $_ => ( is => 'rw' ) for qw(set);\n\n   sub base : Chained('/') PathPart('dashboard_templates') CaptureArgs(0) {\n      my ($self, $c) = @_;\n  -   $self->set($c,\n$c->model('DB')->schema->kiokudb_handle->lookup('dashboard\ntemplates'));\n  +   $self->set($c, $c->model('Kioku')->lookup('dashboard templates'));\n   }\n\n   my $renderer = sub {\n\n\nThen I tried it out\n\n  $ git fixup-assigner.pl > fixups && less fixups\n  #!/bin/sh\n\n  git apply --cached <<EOF\n  diff --git a/App/lib/MyApp/Controller/DashboardTemplates.pm\nb/App/lib/MyApp/Controller/DashboardTemplates.pm\n  --- a/App/lib/MyApp/Controller/DashboardTemplates.pm\n  +++ b/App/lib/MyApp/Controller/DashboardTemplates.pm\n  @@ -15,3 +15,3 @@ sub base : Chained('/')\nPathPart('dashboard_templates') CaptureArgs(0) {\n      my ($self, $c) = @_;\n  -   $self->set($c,\n$c->model('DB')->schema->kiokudb_handle->lookup('dashboard\ntemplates'));\n  +   $self->set($c, $c->model('Kioku')->lookup('dashboard templates'));\n   }\n  EOF\n\n  git commit --fixup 7765cbd2\n\nLooks fine to me.  But then I try to use it:\n\n  $ git checkout . && sh fixups\n  error: patch failed: App/lib/MyApp/Controller/DashboardTemplates.pm:15\n  error: App/lib/MyApp/Controller/DashboardTemplates.pm: patch does not apply\n\nAny ideas what I'm doing wrong?\n\n--\nfREW Schmidt\nhttp://blog.afoolishmanifesto.com\n"},{"id":"178331","messageId":"201110262140.50786.trast@student.ethz.ch","threadId":"26049","inReplyTo":"CADVrmKT1woYpJoe=L9sAbQ30TUw44zMc7y4WF=PMrT5Gj9kDNw@mail.gmail.com","subject":"Re: git-fixup-assigner.perl -- automatically decide where to \"fixup!\"","fromName":"Thomas Rast","fromEmail":"trast@student.ethz.ch","sentAt":"2011-10-26T19:40:50Z","receivedAt":"2011-10-26T19:40:50Z","isPatch":false,"sender":{"key":"tr@thomasrast.ch","avatar":"https://avatars.githubusercontent.com/u/153510?v=4"},"body":"fREW Schmidt wrote:\n> \n>   $ git fixup-assigner.pl > fixups && less fixups\n>   #!/bin/sh\n> \n>   git apply --cached <<EOF\n[...]\n>   -   $self->set($c,\n> $c->model('DB')->schema->kiokudb_handle->lookup('dashboard\n> templates'));\n>   +   $self->set($c, $c->model('Kioku')->lookup('dashboard templates'));\n>    }\n>   EOF\n> \n>   git commit --fixup 7765cbd2\n> \n> Looks fine to me.  But then I try to use it:\n\nThe shell will expand variables in the <<EOF section above.  You can\nfix that by changing it to output <<\\EOF instead, which is clearly a\nbug in the original script.\n\n-- \nThomas Rast\ntrast@{inf,student}.ethz.ch\n"}]}