untested changes so DP might work with this again. Who knows if they even compile.
[divverent/netradiant.git] / branch-manager
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 sub OutputOf($@)
7 {
8         my ($cmd, @args) = @_;
9         $cmd =~ s/#(\d)/"\$ARG$1"/g;
10         $ENV{"ARG$_"} = $args[$_-1]
11                 for 1..@args;
12
13         open my $fh, '-|', $cmd
14                 or die "popen $cmd: $!";
15
16         $ENV{"ARG$_"} = ''
17                 for 1..@args;
18
19         undef local $/;
20         my $ret = <$fh>;
21         close $fh;
22
23         return $ret;
24 }
25
26 my %conf = 
27 (
28         master => '',
29         revisions_applied => ''
30 );
31 my @revisions_applied = (); # array of [first, last] ranges
32
33 sub LoadSettings()
34 {
35         open my $fh, '<', ".patchsets"
36                 or return;
37         while(<$fh>)
38         {
39                 chomp;
40                 /^([^=]*?)\s*=\s*(.*?)$/s
41                         or next;
42                 die "Invalid config item: $1 (allowed: @{[sort keys %conf]})"
43                         if not exists $conf{$1};
44                 $conf{$1} = $2;
45         }
46
47         @revisions_applied = map { /^(\d+)-(\d+)$/s or die "Invalid revision spec $_"; [$1, $2]; } split /,/, $conf{revisions_applied};
48 }
49
50 sub WriteSettings()
51 {
52         $conf{revisions_applied} = join ',', map { "$_->[0]-$_->[1]" } @revisions_applied;
53
54         open my $fh, '>', ".patchsets"
55                 or die "writing settings: $!";
56         for(sort keys %conf)
57         {
58                 print $fh "$_ = $conf{$_}\n";
59         }
60 }
61
62 sub AddPatch($$)
63 {
64         my ($first, $last) = @_;
65         die "Invalid range" if $first > $last;
66
67         my @rev = sort { $a->[0] <=> $b->[0] } (@revisions_applied, [$first, $last]);
68
69         my $i = 0;
70         while($i < @rev - 1)
71         {
72                 my $a = $rev[$i][0];
73                 my $b = $rev[$i][1];
74                 my $c = $rev[$i+1][0];
75                 my $d = $rev[$i+1][1];
76
77                 if($b >= $c)
78                 {
79                         die "overlapping patch: $a-$b overlaps $c-$d";
80                 }
81                 if($b == $c - 1)
82                 {
83                         splice @rev, $i, 2, [$a, $d];
84                         next;
85                 }
86                 ++$i;
87         }
88
89         @revisions_applied = @rev;
90 }
91
92 sub RemovePatch($$)
93 {
94         my ($first, $last) = @_;
95         die "Invalid range" if $first > $last;
96
97         my @rev = sort { $a->[0] <=> $b->[0] } (@revisions_applied);
98
99         my $i = 0;
100         while($i < @rev)
101         {
102                 my $a = $rev[$i][0];
103                 my $b = $rev[$i][1];
104
105                 if($first >= $a && $last <= $b)
106                 {
107                         # this is the range
108                         my @replacement;
109
110                         if($first == $a && $last == $b)
111                         {
112                                 @replacement = ();
113                         }
114                         elsif($first == $a)
115                         {
116                                 @replacement = ([$last + 1, $b]);
117                         }
118                         elsif($last == $b)
119                         {
120                                 @replacement = ([$a, $first - 1]);
121                         }
122                         else
123                         {
124                                 @replacement = ([$a, $first - 1], [$last + 1, $b]);
125                         }
126                         splice @rev, $i, 1, @replacement;
127                         @revisions_applied = @rev;
128                         return;
129                 }
130         }
131
132         die "could not remove range: not in set";
133 }
134
135 sub GetUnappliedRanges($)
136 {
137         my ($lastrev) = @_;
138         my @unapplied = ();
139
140         my $cur = 0;
141         for(@revisions_applied)
142         {
143                 my ($a, $b) = @$_;
144                 if($a - 1 >= $cur + 1)
145                 {
146                         push @unapplied, [$cur + 1, $a - 1];
147                 }
148                 $cur = $b;
149         }
150         if($lastrev >= $cur + 1)
151         {
152                 push @unapplied, [$cur + 1, $lastrev];
153         }
154         return @unapplied;
155 }
156
157 sub GetMasterRev()
158 {
159         my $svninfo = OutputOf 'svn info #1', $conf{master};
160         $svninfo =~ /^Last Changed Rev: (\d+)$/m
161                 or die "could not get svn info";
162         return $1;
163 }
164
165 sub GetLog($$)
166 {
167         my ($first, $last) = @_;
168         my $log = OutputOf 'svn log -r#1:#2 #3', $first, $last, $conf{master};
169         $log =~ s/^-*$//gm;
170         $log =~ s/\n+/\n/gs;
171         $log =~ s/^\n//s;
172         $log =~ s/\n$//s;
173         return $log;
174 }
175
176 sub GetDiff($$)
177 {
178         my ($first, $last) = @_;
179         return OutputOf 'svn diff -r#1:#2 #3', $first-1, $last, $conf{master};
180 }
181
182 my ($cmd, @args) = @ARGV;
183 $cmd = 'help' if not defined $cmd;
184
185 if($cmd eq 'info')
186 {
187         LoadSettings();
188         for(@revisions_applied)
189         {
190                 my ($a, $b) = @$_;
191                 print "Applied: $a to $b\n";
192         }
193         for(GetUnappliedRanges(GetMasterRev()))
194         {
195                 my ($a, $b) = @$_;
196                 print "Unapplied: $a to $b\n";
197         }
198 }
199 elsif($cmd eq 'unmerged-diff')
200 {
201         LoadSettings();
202         my @autoadd = ();
203         for(GetUnappliedRanges(GetMasterRev()))
204         {
205                 my ($a, $b) = @$_;
206                 my $log = GetLog $a, $b;
207                 if($log eq '')
208                 {
209                         push @autoadd, [$a, $b];
210                         next;
211                 }
212                 $log =~ s/^/  /gm;
213                 print "Unapplied: $a to $b\n";
214                 print "$log\n";
215                 print GetDiff $a, $b;
216                 print "\n";
217         }
218         for(@autoadd)
219         {
220                 my ($a, $b) = @$_;
221                 print "Autofilled revision hole $a to $b\n";
222                 AddPatch $a, $b;
223         }
224         WriteSettings() if @autoadd;
225 }
226 elsif($cmd eq 'unmerged')
227 {
228         LoadSettings();
229         my @autoadd = ();
230         for(GetUnappliedRanges(GetMasterRev()))
231         {
232                 my ($a, $b) = @$_;
233                 my $log = GetLog $a, $b;
234                 if($log eq '')
235                 {
236                         push @autoadd, [$a, $b];
237                         next;
238                 }
239                 $log =~ s/^/  /gm;
240                 print "Unapplied: $a to $b\n";
241                 print "$log\n\n";
242         }
243         for(@autoadd)
244         {
245                 my ($a, $b) = @$_;
246                 print "Autofilled revision hole $a to $b\n";
247                 AddPatch $a, $b;
248         }
249         WriteSettings() if @autoadd;
250 }
251 elsif($cmd eq 'merge')
252 {
253         my ($first, $last, $force) = @args;
254         die "Usage: $0 merge first last [--force]"
255                 if not defined $first;
256         $last = $first if not defined $last;
257
258         die "Usage: $0 merge first last"
259                 if "$first$last" =~ /[^0-9]/;
260
261         die "There is an uncommitted merge"
262                 if -f '.commitmsg' and $force ne '--force';
263
264         LoadSettings();
265         AddPatch $first, $last;
266
267         print OutputOf 'svn merge -r#1:#2 #3', ($first - 1), $last, $conf{master};
268         print "You may also want to run $0 unmerged to fill possible revision holes\n";
269         print "Make sure there are no conflicts, then run $0 commit\n";
270         print "To abort, use $0 revert\n";
271         
272         open my $fh, '>>', '.commitmsg'
273                 or die "open .commitmsg";
274         print $fh GetLog $first, $last;
275         print $fh "\n";
276         close $fh;
277
278         WriteSettings();
279 }
280 elsif($cmd eq 'undo')
281 {
282         my ($first, $last, $force) = @args;
283         die "Usage: $0 undo first last"
284                 if not defined $first;
285         $last = $first if not defined $last;
286
287         die "Usage: $0 merge first last"
288                 if "$first$last" =~ /[^0-9]/;
289
290         die "There is an uncommitted merge"
291                 if -f '.commitmsg' and $force ne '--force';
292
293         LoadSettings();
294         RemovePatch $first, $last;
295
296         print OutputOf 'svn merge -r#2:#1 #3', ($first - 1), $last, $conf{master};
297         print "Make sure there are no conflicts, then run $0 commit\n";
298         print "To abort, use $0 revert\n";
299
300         open my $fh, '>>', '.commitmsg'
301                 or die "open .commitmsg";
302         print $fh "undo the following merge:\n", GetLog $first, $last;
303         close $fh;
304
305         WriteSettings();
306 }
307 elsif($cmd eq 'commit')
308 {
309         system 'vim .commitmsg';
310         print "Hit Enter if OK to commit, Ctrl-C otherwise...\n";
311         my $ok = <STDIN>;
312         if(!system 'svn commit -F .commitmsg')
313         {
314                 unlink '.commitmsg';
315         }
316 }
317 elsif($cmd eq 'revert')
318 {
319         if(!system 'svn revert -R .')
320         {
321                 unlink '.commitmsg';
322         }
323 }
324 elsif($cmd eq 'init')
325 {
326         my ($master, $rev) = @args;
327         $conf{master} = $master;
328         @revisions_applied = [1, $rev];
329         WriteSettings();
330 }
331 else
332 {
333         print <<EOF;
334 Usage:
335   $0 init masterrepo rev
336   $0 info
337   $0 unmerged
338   $0 unmerged-diff
339   $0 merge rev1 [rev2] [--force]
340   $0 undo rev1 [rev2] [--force]
341   $0 commit
342   $0 revert
343 EOF
344 }