devel/review: make the range argument optional
[ganeti-github.git] / devel / review
1 #!/bin/bash
2
3 # Copyright (C) 2009 Google Inc.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 # 02110-1301, USA.
19
20 # To set user mappings, use this command:
21 #   git config gnt-review.johndoe 'John Doe <johndoe@domain.tld>'
22
23 # To disable strict mode (enabled by default):
24 #   git config gnt-review.strict false
25
26 # To enable strict mode:
27 #   git config gnt-review.strict true
28
29 set -e
30
31 # Get absolute path to myself
32 me_plain="$0"
33 me=$(readlink -f "$me_plain")
34
35 add_reviewed_by() {
36   local msgfile="$1"
37
38   grep -q '^Reviewed-by: ' "$msgfile" && return
39
40   perl -i -e '
41   my $sob = 0;
42   while (<>) {
43     if ($sob == 0 and m/^Signed-off-by:/) {
44       $sob = 1;
45
46     } elsif ($sob == 1 and not m/^Signed-off-by:/) {
47       print "Reviewed-by: \n";
48       $sob = -1;
49     }
50
51     print;
52   }
53
54   if ($sob == 1) {
55     print "Reviewed-by: \n";
56   }
57   ' "$msgfile"
58 }
59
60 replace_users() {
61   local msgfile="$1"
62
63   if perl -i -e '
64   use strict;
65   use warnings;
66
67   my $error = 0;
68   my $strict;
69
70   sub map_username {
71     my ($name) = @_;
72
73     return $name unless $name;
74
75     my @cmd = ("git", "config", "--get", "gnt-review.$name");
76
77     open(my $fh, "-|", @cmd) or die "Command \"@cmd\" failed: $!";
78     my $output = do { local $/ = undef; <$fh> };
79     close($fh);
80
81     if ($? == 0) {
82       chomp $output;
83       $output =~ s/\s+/ /;
84       return $output;
85     }
86
87     unless (defined $strict) {
88       @cmd = ("git", "config", "--get", "--bool", "gnt-review.strict");
89
90       open($fh, "-|", @cmd) or die "Command \"@cmd\" failed: $!";
91       $output = do { local $/ = undef; <$fh> };
92       close($fh);
93
94       $strict = ($? != 0 or not $output or $output !~ m/^false$/);
95     }
96
97     if ($strict and $name !~ m/^.+<.+\@.+>$/) {
98       $error = 1;
99     }
100
101     return $name;
102   }
103
104   while (<>) {
105     if (m/^Reviewed-by:(.*)$/) {
106       my @names = grep {
107         # Ignore empty entries
108         !/^$/
109       } map {
110         # Normalize whitespace
111         $_ =~ s/(^\s+|\s+$)//g;
112         $_ =~ s/\s+/ /g;
113
114         # Map names
115         $_ = map_username($_);
116
117         $_;
118       } split(m/,/, $1);
119
120       # Get unique names
121       my %saw;
122       @names = grep(!$saw{$_}++, @names);
123       undef %saw;
124
125       foreach (sort @names) {
126         print "Reviewed-by: $_\n";
127       }
128     } else {
129       print;
130     }
131   }
132
133   exit($error? 33 : 0);
134   ' "$msgfile"
135   then
136     :
137   else
138     [[ "$?" == 33 ]] && return 1
139     exit 1
140   fi
141
142   if ! grep -q '^Reviewed-by: ' "$msgfile"
143   then
144     echo 'Missing Reviewed-by: line' >&2
145     sleep 1
146     return 1
147   fi
148
149   return 0
150 }
151
152 run_editor() {
153   local filename="$1"
154   local editor=${EDITOR:-vi}
155   local args
156
157   case "$(basename "$editor")" in
158     vi* | *vim)
159       # Start edit mode at Reviewed-by: line
160       args='+/^Reviewed-by: +nohlsearch +startinsert!'
161     ;;
162     *)
163       args=
164     ;;
165   esac
166
167   $editor $args "$filename"
168 }
169
170 commit_editor() {
171   local msgfile="$1"
172
173   local tmpf=$(mktemp)
174   trap "rm -f $tmpf" EXIT
175
176   cp "$msgfile" "$tmpf"
177
178   while :
179   do
180     add_reviewed_by "$tmpf"
181
182     run_editor "$tmpf"
183
184     replace_users "$tmpf" && break
185   done
186
187   cp "$tmpf" "$msgfile"
188 }
189
190 copy_commit() {
191   local rev="$1" target_branch="$2"
192
193   echo "Copying commit $rev ..."
194
195   git cherry-pick -n "$rev"
196   GIT_EDITOR="$me --commit-editor \"\$@\"" git commit -c "$rev" -s
197 }
198
199 usage() {
200   echo "Usage: $me_plain [from..to] <target-branch>" >&2
201   echo "  If not passed from..to defaults to target-branch..HEAD" >&2
202   exit 1
203 }
204
205 main() {
206   local range target_branch
207
208   case "$#" in
209   1)
210     target_branch="$1"
211     range="$target_branch..$(git rev-parse HEAD)"
212   ;;
213   2)
214     range="$1"
215     target_branch="$2"
216     if [[ "$range" != *..* ]]; then
217       usage
218     fi
219   ;;
220   *)
221     usage
222   ;;
223   esac
224
225   git checkout "$target_branch"
226   local old_head=$(git rev-parse HEAD)
227
228   for rev in $(git rev-list --reverse "$range")
229   do
230     copy_commit "$rev"
231   done
232
233   git log "$old_head..$target_branch"
234 }
235
236 if [[ "$1" == --commit-editor ]]
237 then
238   shift
239   commit_editor "$@"
240 else
241   main "$@"
242 fi