ef0c87006d89b2163644e2c9b16b9fcc4ee8f2fc
[git-perl-utils.git] / script / git-to-bugzilla.pl
1 #!/usr/bin/env perl
2 #
3 #
4 use strict;
5 use warnings;
6 use Carp;
7 use Config::Any;
8 use Data::Dump;
9 use File::Slurp;
10 use FindBin;
11 use Getopt::Long;
12 use Git::Repository;
13
14 use lib "$FindBin::Bin/../lib";
15 use WWW::Bugzilla;
16
17 my $verbose;
18 my $debug;
19
20 # ------------------------------------------------------------------------
21 sub update_bugzilla {
22     my $cfg  = shift;
23     my $info = shift;
24     my $set  = shift;
25
26     my $bz = WWW::Bugzilla->new(
27         server     => $cfg->{bugzilla}{server},
28         email      => $cfg->{bugzilla}{user},
29         password   => $cfg->{bugzilla}{pass},
30         bug_number => $set->{bug}
31     ) || croak "Cannot open bz - $!";
32
33     my $header = sprintf( "Git commit: %s/commitdiff/%s\n", $cfg->{gitweb}, $info->{rev} );
34     if ( scalar( @{ $info->{diff} } ) > 50 ) {
35
36         # big diff - we skip the diff
37         $bz->additional_comments(
38             join( "\n", $header, @{ $info->{info} }, '', @{ $info->{log} }, '----', @{ $info->{diffstat} } ) );
39     }
40     else {
41
42         # otherwise we do the whole thing
43         $bz->additional_comments( join( "\n", $header, @{ $info->{all} } ) );
44     }
45
46     $bz->change_status("fixed")  if ( $set->{action} eq 'fixes' );
47     $bz->change_status("closed") if ( $set->{action} eq 'closes' );
48
49     $bz->commit;
50
51     printf( "[%d] %s %s [%s]\n", $set->{bug}, $info->{rev}, $info->{log}[0], $set->{action} );
52 }
53
54 # ------------------------------------------------------------------------
55 sub find_bugzilla_references {
56     my $info = shift;
57     my $cfg  = shift;
58
59     my @results;
60     my $action = '';
61     my $bugid;
62     foreach my $line ( @{ $info->{log} } ) {
63         $line = lc($line);
64         if ( $line =~ /(closes|fixes|references):?\s*(?:bug(?:zilla)?)?\s*\#?(\d+)/ ) {
65             $action = $1;
66             $bugid  = $2;
67         }
68         elsif ( $line =~ /\b(?:bug(?:zilla)?)\s*\#?(\d+)/ ) {
69             $action = 'references';
70             $bugid  = $1;
71         }
72         else {
73             next;
74         }
75
76         # remap actions
77         $action = 'closes' if ( $action =~ /^fix/ );
78
79         push( @results, { bug => $bugid, action => $action } );
80         ##printf( "%s\n\taction = %s bugid = %s\n", $info->{rev}, $action, $bugid );
81     }
82     return @results;
83 }
84
85 # ------------------------------------------------------------------------
86
87 sub git_commit_info {
88     my $git = shift;
89     my $rev = shift;
90
91     my @lines = $git->run( 'show', '-M', '-C', '--patch-with-stat', '--pretty=fuller', $rev );
92
93     my $info = {
94         rev      => $rev,
95         info     => [],
96         log      => [],
97         diffstat => [],
98         diff     => [],
99         all      => [@lines],    # deliberate copy
100     };
101
102     while ( my $line = shift @lines ) {
103         last if ( $line =~ /^$/ );
104         push( @{ $info->{info} }, $line );
105     }
106
107     while ( my $line = shift @lines ) {
108         last if ( $line =~ /^---\s*$/ );
109         push( @{ $info->{log} }, $line );
110     }
111
112     while ( my $line = shift @lines ) {
113         last if ( $line =~ /^$/ );
114         push( @{ $info->{diffstat} }, $line );
115     }
116
117     # all the rest
118     $info->{diff} = \@lines;
119
120     return $info;
121 }
122
123 # ------------------------------------------------------------------------
124
125 sub walk_git_commits {
126     my $git = shift;
127     my $cfg = shift;
128
129     my $lastrev = $git->run( 'rev-parse', $cfg->{lastref} );
130     my $headrev = $git->run( 'rev-parse', $cfg->{branch_head} );
131
132     return if ( $lastrev eq $headrev );
133
134     my @revs = $git->run( 'rev-list', '--topo-order', '--no-merges', ( $lastrev . '..' . $headrev ) );
135
136     foreach my $rev ( reverse(@revs) ) {
137         my $info = git_commit_info( $git, $rev );
138
139         #ddx($info);
140         #dd( $info->{info}, $info->{log}[0] );
141         my @sets = find_bugzilla_references( $info, $cfg );
142         foreach my $set (@sets) {
143             update_bugzilla( $cfg, $info, $set );
144         }
145     }
146     return $headrev;
147 }
148
149 # ------------------------------------------------------------------------
150
151 # main
152 {
153     my $config;
154
155     GetOptions(
156         'config=s' => \$config,
157         'debug!'   => \$debug,
158         'verbose!' => \$verbose,
159     ) or die "Incorrect options";
160     die "No config file given\n" unless ( $config and -f $config );
161     my $cfg = ( values( %{ Config::Any->load_files( { files => [$config], use_ext => 1 } )->[0] } ) )[0];
162
163     die "No git_dir specified\n" unless ( $cfg->{git_dir} );
164     $cfg->{lasttag} ||= $cfg->{git_dir} . '/refs/tags/BugzillaDone';
165     $cfg->{branch_head} ||= 'HEAD';
166
167     $cfg->{lastref} = -f $cfg->{lasttag} ? read_file( $cfg->{lasttag} ) : 'HEAD';
168     chomp( $cfg->{lastref} );
169
170     my $git = Git::Repository->new( git_dir => $cfg->{git_dir} ) || die "No valid git repo\n";
171
172     my $newlast = walk_git_commits( $git, $cfg );
173     if ($newlast) {
174         write_file( $cfg->{lasttag}, $newlast );
175     }
176 }