]> Pileus Git - ~andy/sunrise/blob - scripts/echangelog
fix repoman problems with sunrise-commit, svn problems with echangelog and use sunris...
[~andy/sunrise] / scripts / echangelog
1 #!/usr/bin/perl -w
2 #
3 # echangelog: Update the ChangeLog for an ebuild.  For example:
4 #
5 #   $ echangelog 'Add ~alpha to KEYWORDS'
6 #   4a5,7
7 #   >   10 Feb 2003; Aron Griffis <agriffis@gentoo.org> oaf-0.6.8-r1.ebuild :
8 #   >   Add ~alpha to KEYWORDS
9 #   >
10
11 use strict;
12 use POSIX qw(strftime getcwd setlocale);
13
14 # Fix bug 21022 by restricting to C locale
15 setlocale(&POSIX::LC_ALL, "C");
16
17 use Text::Wrap;
18 $Text::Wrap::columns = 77;
19 $Text::Wrap::unexpand = 0;
20
21 # Global variables
22 my (@files, @ebuilds, @conflicts, @trivial, @unknown, @new_versions, %actions);
23 my ($input, $editor, $entry, $user, $date, $text, $version, $year, $vcs);
24
25 my %vcs =     ( cvs => { diff => "cvs -f -U0 diff",
26                          status => "cvs -fn up",
27                          add => "cvs -f add",
28                          skip => 6,
29                          entries => "CVS/Entries" },
30                 svn => { diff => "svn diff -N",
31                          status => "svn status",
32                          add => "svn add",
33                          skip => 4,
34                          entries => ".svn/entries" },
35                 git => { diff => "git diff",
36                          status => "git up",
37                          add => "git add",
38                          skip => 0,
39                          entries => "wtf" }
40 );
41
42 # Figure out what kind of repo we are in.
43
44 if ( -d "CVS" ) {
45    $vcs = "cvs";
46 } elsif ( -d '.svn' ) {
47     $vcs = "svn";
48 } elsif ( -d '.git' ) {
49     $vcs = "git";
50 } else {
51     die "No CVS, .git, .svn directories found, what kind of repo is this?";
52 }
53
54 # Read the current ChangeLog
55 if (-f 'ChangeLog') {
56     open I, '<ChangeLog' or die "Can't open ChangeLog for input: $!\n";
57     { local $/ = undef; $text = <I>; }
58     close I;
59 } else {
60     # No ChangeLog here, maybe we should make one...
61     if (<*.ebuild>) {
62         open I, '<../../skel.ChangeLog' 
63             or die "Can't open ../../skel.ChangeLog for input: $!\n";
64         { local $/ = undef; $text = <I>; }
65         close I;
66         my ($cwd) = getcwd();
67         $cwd =~ m|.*/(\w+-\w+)/([^/]+)| 
68             or die "Can't figure out category/package.. sorry!\n";
69         my ($category, $package_name) = ($1, $2);
70         $text =~ s/^\*.*//ms;   # don't need the fake entry
71         $text =~ s/<CATEGORY>/$category/;
72         $text =~ s/<PACKAGE_NAME>/$package_name/;
73     } else {
74         die "This should be run in a directory with ebuilds...\n";
75     }
76 }
77
78 # Figure out what has changed around here
79 open C, $vcs{$vcs}{status}.' 2>&1 |' or die "Can't run ".$vcs{$vcs}{status}.": $!\n";
80 while (<C>) {
81     if (/^C\s+\+?\s+(\S+)/) {
82         push @conflicts, $1; 
83         next;
84     } elsif (/^\?\s+\+?\s+(\S+)/) {
85         push @unknown, $1;
86         $actions{$1} = '+';
87         next;
88     } elsif (/^([ARMD])\s+\+?\s+(\S+)/) {
89         push @files, $2;
90         ($actions{$2} = $1) =~ tr/ARDM/+--/d;
91     }
92 }
93
94 # Separate out the trivial files for now
95 @files = grep { 
96     !/files.digest|Manifest|ChangeLog|^files$|^\.$/ or do { push @trivial, $_; 0; }
97 } @files;
98
99 @unknown = grep { 
100     !/files.digest|Manifest|ChangeLog|^files$|^\.$/ or do { push @trivial, $_; 0; }
101 } @unknown;
102
103 # Don't allow any conflicts
104 if (@conflicts) {
105     print STDERR <<EOT;
106 $vcs reports the following conflicts.  Please resolve them before
107 running echangelog.
108 EOT
109     print STDERR map "C $_\n", @conflicts;
110     exit 1;
111 }
112
113 # Don't allow unknown files (other than the trivial files that were separated
114 # out above)
115 if (@unknown) {
116     print STDERR <<EOT;
117 $vcs reports the following unknown files.  Please use "cvs add" before
118 running echangelog, or remove the files in question.
119 EOT
120     print STDERR map "? $_\n", @unknown;
121     exit 1;
122 }
123
124 # Sort the list of files as portage does.  None of the operations through
125 # the rest of the script should break this sort.
126 sub sortfunc($$) {
127     my ($a, $b) = @_;
128     (my $va = $a) =~ s/.*?-(\d.*?)(?:\.ebuild)?$/$1/;
129     (my $vb = $b) =~ s/.*?-(\d.*?)(?:\.ebuild)?$/$1/;
130     my ($na, $sa, $sna, $ra) = ($va =~ /^(.*?)(?:_(alpha|beta||pre|rc|p)(\d*))?(?:-r(\d+))?$/);
131     my ($nb, $sb, $snb, $rb) = ($vb =~ /^(.*?)(?:_(alpha|beta||pre|rc|p)(\d*))?(?:-r(\d+))?$/);
132     my (@na) = split /\.|(?<=\d)(?=[^\d\.])/, $na;
133     my (@nb) = split /\.|(?<=\d)(?=[^\d\.])/, $nb;
134     my $retval;
135
136     #
137     # compare version numbers first
138     #
139     for (my $i = 0; defined $na[$i] or defined $nb[$i]; $i++) {
140         # def vs. undef
141         return +1 if defined $na[$i] and !defined $nb[$i];
142         return -1 if defined $nb[$i] and !defined $na[$i];
143
144         # num vs. num
145         if ($na[$i] =~ /^\d/ and $nb[$i] =~ /^\d/) {
146             $retval = ($na[$i] <=> $nb[$i]);
147             return $retval if $retval;
148             next;
149         }
150
151         # char vs. char
152         if ($na[$i] =~ /^\D/ and $nb[$i] =~ /^\D/) {
153             $retval = ($na[$i] cmp $nb[$i]);
154             return $retval if $retval;
155             next;
156         }
157
158         # num vs. char
159         $retval = ($na[$i] =~ /\d/ and -1 or +1);
160         return $retval;
161     }
162
163     #
164     # compare suffix second
165     #
166     if (defined $sa and !defined $sb) {
167         return +2 if $sa eq "p";
168         return -2;
169     }
170     if (defined $sb and !defined $sa) {
171         return -3 if $sb eq "p";
172         return +3;
173     }
174
175     if (defined $sa) {  # and defined $sb
176         $retval = ($sa cmp $sb);
177         if ($retval) {
178             return +4 if $sa eq "p";
179             return -4 if $sb eq "p";
180             return $retval; # suffixes happen to be alphabetical order, mostly
181         }
182
183         # compare suffix number
184         return +5 if defined $sna and !defined $snb;
185         return -5 if defined $snb and !defined $sna;
186         if (defined $sna) {  # and defined $snb
187             $retval = ($sna <=> $snb);
188             return $retval if $retval;
189         }
190     }
191
192     #
193     # compare rev third
194     #
195     return +6 if defined $ra and !defined $rb;
196     return -6 if defined $rb and !defined $ra;
197     if (defined $ra) {  # and defined $rb
198         return ($ra <=> $rb);
199     }
200
201     #
202     # nothing left to compare
203     #
204     return 0;
205 }
206 @files = sort sortfunc @files;
207
208 # Forget ebuilds that only have changed copyrights, unless that's all
209 # the changed files we have
210 # does not work with svn TODO
211 #@ebuilds = grep /\.ebuild$/, @files;
212 #@files = grep !/\.ebuild$/, @files;
213
214 if (@ebuilds) {
215     open C, $vcs{$vcs}{diff}.@ebuilds." 2>&1 |" or die "Can't run: ".$vcs{$vcs}{diff}."$!\n";
216     $_ = <C>;
217     while (defined $_) {
218         if (/^$vcs diff: (([^\/]*?)\.ebuild) was removed/) { 
219             push @files, $1;
220         }
221         elsif (/^Index: (([^\/]*?)\.ebuild)\s*$/) { 
222             my ($f, $v) = ($1, $2);
223             # check if more than just copyright date changed.
224             # skip some lines
225             foreach(1..$vcs{$vcs}{skip}){
226                     $_ = <C>;
227             }
228             while (<C>) {
229                 last if /^[A-Za-z]/;
230                 if (/^[-+](?!# Copyright)/) {
231                     push @files, $f;
232                     last;
233                 }
234             }
235             # at this point we've either added $f to @files or not,
236             # and we have the next line in $_ for processing
237             next;
238         }
239         elsif (/^$vcs.*?: (([^\/]*?)\.ebuild) is a new entry/) { 
240             push @files, $1;
241             push @new_versions, $2;  # new ebuild, will create a new entry
242         }
243         # other cvs output is ignored
244         $_ = <C>;
245     }
246 }
247 close C;
248
249 # When a package move occurs, the versions appear to be new even though they are
250 # not.  Trim them from @new_versions in that case.
251 @new_versions = grep { $text !~ /^\*\Q$_\E\s/m } @new_versions;
252
253 # Check if we have any files left, otherwise re-insert ebuild list
254 # (of course, both might be empty anyway)
255 @files = @ebuilds unless (@files);
256
257 # Allow ChangeLog entries with no changed files, but give a fat warning
258 unless (@files) {
259     print STDERR "**\n";
260     print STDERR "** NOTE: No non-trivial changed files found.  Normally echangelog\n";
261     print STDERR "** should be run after all affected files have been added and/or\n";
262     print STDERR "** modified.  Did you forget to cvs add?\n";
263     print STDERR "**\n";
264     @files = sort sortfunc @trivial;
265     @files = qw/ChangeLog/ unless @files;  # last resort to put something in the list
266 }
267
268 # Get the input from the cmdline, editor or stdin
269 if ($ARGV[0]) {
270     $input = "@ARGV";
271 } else {
272     # Testing for defined() allows ECHANGELOG_EDITOR='' to cancel EDITOR
273     $editor = defined($ENV{'ECHANGELOG_EDITOR'}) ? $ENV{'ECHANGELOG_EDITOR'} :
274         $ENV{'EDITOR'} || undef;
275     if ($editor) {
276         system("$editor ChangeLog.new");
277         if ($? != 0) {
278             # This usually happens when the editor got forcefully killed; and
279             # the terminal is probably messed up: so we reset things.
280             system('/usr/bin/stty sane');
281             print STDERR "Editor died!  Reverting to stdin method.\n";
282             undef $editor;
283         } else {
284             if (open I, "<ChangeLog.new") {
285                 local $/ = undef;
286                 $input = <I>;
287                 close I;
288             } else {
289                 print STDERR "Error opening ChangeLog.new: $!\n";
290                 print STDERR "Reverting to stdin method.\n";
291                 undef $editor;
292             }
293             unlink 'ChangeLog.new';
294         }
295     }
296     unless ($editor) {
297         print "Please type the log entry: use Ctrl-d to finish, Ctrl-c to abort...\n";
298         local $/ = undef;
299         $input = <>;
300     }
301 }
302 die "Empty entry; aborting\n" unless $input =~ /\S/;
303
304 # If there are any long lines, then wrap the input at $columns chars
305 # (leaving 2 chars on left, one char on right, after adding indentation below).
306 $input =~ s/^\s*(.*?)\s*\z/$1/s;  # trim whitespace
307 $input = Text::Wrap::fill('', '', $input) if ($input =~ /^.{80}/m);
308 $input =~ s/^/  /gm;        # add indentation
309
310 # Prepend the user info to the input
311 unless ($user = $ENV{'ECHANGELOG_USER'}) {
312     my ($fullname, $username) = (getpwuid($<))[6,0];
313     $fullname =~ s/,.*//;       # remove GECOS, bug 80011
314     $user = sprintf "%s <%s\@gentoo.org>", $fullname, $username;
315 }
316 # Make sure that we didn't get "root"
317 die "Please set ECHANGELOG_USER or run as non-root\n" if $user =~ /<root@/;
318 $date = strftime("%d %b %Y", gmtime);
319 $entry = "$date; $user ";
320 $entry .= join ', ', map "$actions{$_}$_", @files;
321 $entry .= ':';
322 $entry = Text::Wrap::fill('  ', '  ', $entry);  # does not append a \n
323 $entry .= "\n$input";                           # append user input
324
325 # Each one of these regular expressions will eat the whitespace
326 # leading up to the next entry (except the two-space leader on the
327 # front of a dated entry), so it needs to be replaced with a
328 # double carriage-return.  This helps to normalize the spacing in
329 # the ChangeLogs.
330 if (@new_versions) {
331     # Insert at the top with a new version marker
332     $text =~ s/^( .*? )               # grab header
333                \s*\n(?=\ \ \d|\*|\z)  # suck up trailing whitespace
334     /"$1\n\n" .
335      join("\n", map "*$_ ($date)", reverse @new_versions) .
336      "\n\n$entry\n\n"/sxe
337         or die "Failed to insert new entry (4)\n";
338 } else {
339     # Changing an existing patch or ebuild, no new version marker
340     # required
341     $text =~ s/^( .*? )               # grab header
342                \s*\n(?=\ \ \d|\*|\z)  # suck up trailing whitespace
343     /$1\n\n$entry\n\n/sx
344         or die "Failed to insert new entry (3)\n";
345 }
346
347 sub update_copyright {
348     my ($t) = @_;
349     (my $year = $date) =~ s/.* //;
350     $t =~ s/^# Copyright \d+(?= )/$&-$year/m or
351     $t =~ s/^(# Copyright \d+)-(\d+)/$1-$year/m;
352     return $t;
353 }
354
355 # Update the copyright year in the ChangeLog
356 $text = update_copyright($text);
357
358 # Write the new ChangeLog
359 open O, '>ChangeLog.new' or die "Can't open ChangeLog.new for output: $!\n";
360 print O $text            or die "Can't write ChangeLog.new: $!\n";
361 close O                  or die "Can't close ChangeLog.new: $!\n";
362
363 # Update affected ebuild copyright dates.  There is no reason to update the
364 # copyright lines on ebuilds that haven't changed.  I verified this with an IP
365 # lawyer.
366 for my $e (grep /\.ebuild$/, @files) {
367     my ($etext, $netext);
368     open E, "<$e" or warn("Can't read $e to update copyright year\n"), next;
369     { local $/ = undef; $etext = <E>; }
370     close E;
371
372     # Attempt the substitution and compare
373     $netext = update_copyright($etext);
374     next if $netext eq $etext; # skip this file if no change.
375
376     # Write the new ebuild
377     open E, ">$e.new" or warn("Can't open $e.new\n"), next;
378     print E $netext and
379     close E or warn("Can't write $e.new\n"), next;
380
381     # Move things around and show the diff
382     system "diff -U 0 $e $e.new";
383     rename "$e.new", $e or warn("Can't rename $e.new: $!\n");
384 }
385
386 # Move things around and show the ChangeLog diff
387 system 'diff -Nu ChangeLog ChangeLog.new';
388 rename 'ChangeLog.new', 'ChangeLog' or die "Can't rename ChangeLog.new: $!\n";
389
390 # Okay, now we have a starter ChangeLog to work with.
391 # The text will be added just like with any other ChangeLog below.  
392 # Add the new ChangeLog to cvs before continuing.
393 if (open F, $vcs{$vcs}{entries} ) {
394     system("$vcs{$vcs}{add} ChangeLog") unless (scalar grep /\/?ChangeLog\/?/, <F>);
395 }
396
397 # vim:sw=4 ts=8 expandtab