git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* git-fixup-assigner.perl -- automatically decide where to "fixup!"
@ 2010-12-14  2:09 Thomas Rast
  2011-10-26 14:37 ` fREW Schmidt
  0 siblings, 1 reply; 3+ messages in thread
From: Thomas Rast @ 2010-12-14  2:09 UTC (permalink / raw)
  To: git

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

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: git-fixup-assigner.perl -- automatically decide where to "fixup!"
  2010-12-14  2:09 git-fixup-assigner.perl -- automatically decide where to "fixup!" Thomas Rast
@ 2011-10-26 14:37 ` fREW Schmidt
  2011-10-26 19:40   ` Thomas Rast
  0 siblings, 1 reply; 3+ messages in thread
From: fREW Schmidt @ 2011-10-26 14:37 UTC (permalink / raw)
  To: Thomas Rast; +Cc: git

On Mon, Dec 13, 2010 at 8:09 PM, Thomas Rast <trast@student.ethz.ch> wrote:
>
> 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 ---

This is super neat, but I'm having trouble getting it to work.

First, I made one small change:

  -        print "git fixup $commit\n\n";
  +       print "git commit --fixup $commit\n\n";

To try it out I made a very simple change:

  $ git diff
  diff --git a/App/lib/MyApp/Controller/DashboardTemplates.pm
b/App/lib/MyApp/Controller/DashboardTemplates.pm
  index aefdc3c..a53b534 100644
  --- a/App/lib/MyApp/Controller/DashboardTemplates.pm
  +++ b/App/lib/MyApp/Controller/DashboardTemplates.pm
  @@ -13,7 +13,7 @@ cat_has $_ => ( is => 'rw' ) for qw(set);

   sub base : Chained('/') PathPart('dashboard_templates') CaptureArgs(0) {
      my ($self, $c) = @_;
  -   $self->set($c,
$c->model('DB')->schema->kiokudb_handle->lookup('dashboard
templates'));
  +   $self->set($c, $c->model('Kioku')->lookup('dashboard templates'));
   }

   my $renderer = sub {


Then I tried it out

  $ git fixup-assigner.pl > fixups && less fixups
  #!/bin/sh

  git apply --cached <<EOF
  diff --git a/App/lib/MyApp/Controller/DashboardTemplates.pm
b/App/lib/MyApp/Controller/DashboardTemplates.pm
  --- a/App/lib/MyApp/Controller/DashboardTemplates.pm
  +++ b/App/lib/MyApp/Controller/DashboardTemplates.pm
  @@ -15,3 +15,3 @@ sub base : Chained('/')
PathPart('dashboard_templates') CaptureArgs(0) {
      my ($self, $c) = @_;
  -   $self->set($c,
$c->model('DB')->schema->kiokudb_handle->lookup('dashboard
templates'));
  +   $self->set($c, $c->model('Kioku')->lookup('dashboard templates'));
   }
  EOF

  git commit --fixup 7765cbd2

Looks fine to me.  But then I try to use it:

  $ git checkout . && sh fixups
  error: patch failed: App/lib/MyApp/Controller/DashboardTemplates.pm:15
  error: App/lib/MyApp/Controller/DashboardTemplates.pm: patch does not apply

Any ideas what I'm doing wrong?

--
fREW Schmidt
http://blog.afoolishmanifesto.com

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: git-fixup-assigner.perl -- automatically decide where to "fixup!"
  2011-10-26 14:37 ` fREW Schmidt
@ 2011-10-26 19:40   ` Thomas Rast
  0 siblings, 0 replies; 3+ messages in thread
From: Thomas Rast @ 2011-10-26 19:40 UTC (permalink / raw)
  To: fREW Schmidt; +Cc: git

fREW Schmidt wrote:
> 
>   $ git fixup-assigner.pl > fixups && less fixups
>   #!/bin/sh
> 
>   git apply --cached <<EOF
[...]
>   -   $self->set($c,
> $c->model('DB')->schema->kiokudb_handle->lookup('dashboard
> templates'));
>   +   $self->set($c, $c->model('Kioku')->lookup('dashboard templates'));
>    }
>   EOF
> 
>   git commit --fixup 7765cbd2
> 
> Looks fine to me.  But then I try to use it:

The shell will expand variables in the <<EOF section above.  You can
fix that by changing it to output <<\EOF instead, which is clearly a
bug in the original script.

-- 
Thomas Rast
trast@{inf,student}.ethz.ch

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2011-10-26 19:40 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2010-12-14  2:09 git-fixup-assigner.perl -- automatically decide where to "fixup!" Thomas Rast
2011-10-26 14:37 ` fREW Schmidt
2011-10-26 19:40   ` Thomas Rast

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).