xref: /curl/scripts/release-notes.pl (revision 2bc1d775)
1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
10#
11# This software is licensed as described in the file COPYING, which
12# you should have received as part of this distribution. The terms
13# are also available at https://curl.se/docs/copyright.html.
14#
15# You may opt to use, copy, modify, merge, publish, distribute and/or sell
16# copies of the Software, and permit persons to whom the Software is
17# furnished to do so, under the terms of the COPYING file.
18#
19# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20# KIND, either express or implied.
21#
22# SPDX-License-Identifier: curl
23#
24###########################################################################
25
26###############################################
27#
28# ==== How to use this script ====
29#
30# 1. Get recent commits added to RELEASE-NOTES:
31#
32# $ ./scripts/release-notes.pl
33#
34# 2. Edit RELEASE-NOTES and remove all entries that don't belong.  Unused
35# references below will be cleaned up in the next step. Make sure to move
36# "changes" up to the changes section. All entries will by default be listed
37# under bug-fixes as this script can't know where to put them.
38#
39# 3. Run the cleanup script and let it sort the entries and remove unused
40# references from lines you removed in step (2):
41#
42# $ ./scripts/release-notes.pl cleanup
43#
44# 4. Reload RELEASE-NOTES and verify that things look okay. The cleanup
45# procedure can and should be re-run when lines are removed or rephrased.
46#
47# 5. Run ./scripts/contributors.sh and update the contributor list of names
48# The list can also be extended or edited manually.
49#
50# 6. Run ./scripts/delta and update the contributor count at the top, and
51# double-check/update the other counters.
52#
53# 7. Commit the file using "RELEASE-NOTES: synced" as commit message.
54#
55################################################
56
57my $cleanup = ($ARGV[0] eq "cleanup");
58my @gitlog=`git log @^{/RELEASE-NOTES:.synced}..` if(!$cleanup);
59my @releasenotes=`cat RELEASE-NOTES`;
60
61my @o; # the entire new RELEASE-NOTES
62my @refused; # [num] = [2 bits of use info]
63my @refs; # [number] = [URL]
64for my $l (@releasenotes) {
65    if($l =~ /^ o .*\[(\d+)\]/) {
66        # referenced, set bit 0
67        $refused[$1]=1;
68    }
69    elsif($l =~ /^ \[(\d+)\] = (.*)/) {
70        # listed in a reference, set bit 1
71        $refused[$1] |= 2;
72        $refs[$1] = $2;
73    }
74}
75
76# Return a new fresh reference number
77sub getref {
78    for my $r (1 .. $#refs) {
79        if(!$refused[$r] & 1) {
80            return $r;
81        }
82    }
83    # add at the end
84    return $#refs + 1;
85}
86
87# '#num'
88# 'num'
89# 'https://github.com/curl/curl/issues/6939'
90# 'https://github.com/curl/curl-www/issues/69'
91# 'https://elsewhere.example.com/discussion'
92
93sub extract {
94    my ($ref)=@_;
95    if($ref =~ /^(\#|)(\d+)/) {
96        # return the plain number
97        return $2;
98    }
99    elsif($ref =~ /^https:\/\/github.com\/curl\/curl\/.*\/(\d+)/) {
100        # return the plain number
101        return $1;
102    }
103    elsif($ref =~ /:\/\//) {
104        # contains a '://', return the URL
105        return $ref;
106    }
107    # false alarm, not a valid line
108}
109
110my $short;
111my $first;
112for my $l (@gitlog) {
113    chomp $l;
114    if($l =~ /^commit/) {
115        if($first) {
116            onecommit($short);
117        }
118        # starts a new commit
119        undef @fixes;
120        undef @closes;
121        undef @bug;
122        $short = "";
123        $first = 0;
124    }
125    elsif(($l =~ /^    (.*)/) && !$first) {
126        # first line
127        $short = $1;
128        $short =~ s/ ?\[(ci skip|skip ci)\]//g;
129        $first = 1;
130        push @line, $short;
131    }
132    elsif(($l =~ /^    (.*)/) && $first) {
133        # not the first
134        my $line = $1;
135
136        if($line =~ /^Fixes(:|) *(.*)/i) {
137            my $ref = extract($2);
138            push @fixes, $ref if($ref);
139        }
140        elsif($line =~ /^Clo(s|)es(:|) *(.*)/i) {
141            my $ref = extract($3);
142            push @closes, $ref if($ref);
143        }
144        elsif($line =~ /^Bug: (.*)/i) {
145            my $ref = extract($1);
146            push @bug, $ref if($ref);
147        }
148    }
149}
150if($first) {
151    onecommit($short);
152}
153
154# call at the end of a parsed commit
155sub onecommit {
156    my ($short)=@_;
157    my $ref;
158
159    if($bug[0]) {
160        $ref = $bug[0];
161    }
162    elsif($fixes[0]) {
163        $ref = $fixes[0];
164    }
165    elsif($closes[0]) {
166        $ref = $closes[0];
167    }
168
169    if($ref =~ /^#?(\d+)/) {
170        $ref = "https://curl.se/bug/?i=$1"
171    }
172    if($ref) {
173        my $r = getref();
174        $refs[$r] = $ref;
175        $moreinfo{$short}=$r;
176        $refused[$r] |= 1;
177    }
178}
179
180#### Output the new RELEASE-NOTES
181
182my @bullets;
183for my $l (@releasenotes) {
184    if(($l =~ /^This release includes the following bugfixes:/) && !$cleanup) {
185        push @o, $l;
186        push @o, "\n";
187        for my $f (@line) {
188            push @o, sprintf " o %s%s\n", $f,
189                $moreinfo{$f}? sprintf(" [%d]", $moreinfo{$f}): "";
190            $refused[$moreinfo{$f}]=3;
191        }
192        push @o, " --- new entries are listed above this ---";
193        next;
194    }
195    elsif($cleanup) {
196        if($l =~ /^ --- new entries are listed/) {
197            # ignore this if still around
198            next;
199        }
200        elsif($l =~ /^ o .*/) {
201            push @bullets, $l;
202            next;
203        }
204        elsif($bullets[0]) {
205            # output them case insensitively
206            for my $b (sort { "\L$a" cmp "\L$b" } @bullets) {
207                push @o, $b;
208            }
209            undef @bullets;
210        }
211    }
212    if($l =~ /^ \[(\d+)\] = /) {
213        # stop now
214        last;
215    }
216    else {
217        push @o, $l;
218    }
219}
220
221my @srefs;
222my $ln;
223for my $n (1 .. $#refs) {
224    my $r = $refs[$n];
225    if($r && ($refused[$n] & 1)) {
226        push @o, sprintf " [%d] = %s\n", $n, $r;
227    }
228}
229
230open(O, ">RELEASE-NOTES");
231for my $l (@o) {
232    print O $l;
233}
234close(O);
235
236exit;
237
238# Debug: show unused references
239for my $r (1 .. $#refs) {
240    if($refused[$r] != 3) {
241        printf "%s is %d!\n", $r, $refused[$r];
242    }
243}
244