diff options
Diffstat (limited to 'tools/dist')
-rw-r--r-- | tools/dist/README.backport | 36 | ||||
-rwxr-xr-x | tools/dist/backport.pl | 15 | ||||
-rw-r--r-- | tools/dist/backport/status.py | 10 | ||||
-rw-r--r-- | tools/dist/backport_tests.py | 22 | ||||
-rw-r--r-- | tools/dist/backport_tests_data/backport_logsummary_colon.dump | 522 | ||||
-rwxr-xr-x | tools/dist/changes-to-html.py | 88 | ||||
-rwxr-xr-x | tools/dist/create-minor-release-branch.py | 328 | ||||
-rwxr-xr-x | tools/dist/edit-N-log-messages | 94 | ||||
-rwxr-xr-x | tools/dist/nightly.sh | 2 | ||||
-rwxr-xr-x | tools/dist/release.py | 555 | ||||
-rw-r--r-- | tools/dist/security/parser.py | 13 | ||||
-rw-r--r-- | tools/dist/templates/download.ezt | 6 | ||||
-rw-r--r-- | tools/dist/templates/rc-news.ezt | 4 | ||||
-rw-r--r-- | tools/dist/templates/rc-release-ann.ezt | 22 | ||||
-rw-r--r-- | tools/dist/templates/stable-news.ezt | 6 | ||||
-rw-r--r-- | tools/dist/templates/stable-release-ann.ezt | 22 |
16 files changed, 1573 insertions, 172 deletions
diff --git a/tools/dist/README.backport b/tools/dist/README.backport index 0b9c66e..c3fdc68 100644 --- a/tools/dist/README.backport +++ b/tools/dist/README.backport @@ -7,20 +7,23 @@ F1. Auto-merge bot; the nightly svn-role commits. F2. Conflicts detector bot; the svn-backport-conflicts-1.9.x buildbot task. -And two interactive functions, described later. +And two interactive functions¹: + +F3. Reviewing STATUS nominations and casting votes. + +F4. Adding new entries to STATUS. The scripts are: backport.pl: - oldest script, implements both [F1] and [F2], plus two interactive - functions¹. As of March 2015, used in production by svn-role and - by svn-backport-conflicts-1.9.x. + oldest script, implements [F1], [F2], and [F3]. As of Feb 2018, used in + production by svn-role (running on svn-qavm3) and by svn-backport-conflicts-1.9.x + (a buildbot job). nominate.pl: - Symlink to backport.pl. Implements one of the two interactive features. - Not used by bots. + Symlink to backport.pl. Implements [F4]. (The script inspects its argv[0].) backport_tests_pl.py: Regression tests for backport.pl. @@ -39,9 +42,11 @@ backport/*.py: detect-conflicting-backports.py: Implementation of [F2] using backport.py. + Not currently used in production. merge-approved-backports.py: Implementation of [F1] using backport.py. + Not currently used in production. backport_tests_py.py: Regression tests for detect-conflicting-backports.py and merge-approved-backports.py @@ -52,13 +57,28 @@ backport_tests.py: svntest framework (../../subversion/tests/cmdline/svntest/), which is written in Python 2. -backport*.dump: + Note that backport_tests.py and backport/*.py are written in different + languages, so they never 'import' each other. backport_tests.py invokes + detect-conflicting-backports.py, merge-approved-backports.py, and + backport.pl in the same manner: through subprocess.check_call(). + +backport_tests_data/backport*.dump: Expected output files for backport_tests.py; see the BackportTest - decorator. + decorator in backport_tests.py. All scripts can be run with '--help' to display their usage messages. +backport.pl is considered deprecated since backport.py is better architected +and is written in a language that many more active developers are comfortable +with. The unattended jobs [F1] and [F2] should be converted to using +backport.py whenever someone gets around to do the legwork. The interactive +versions [F3] and [F4] are still in active use, however, so the physical +backport.pl script should be kept around until Python versions of these are +available. + + +TODO: document that "Notes: ... --accept=foo ..." is parsed, see backport_tests.py #3. ¹ For backport.pl's interactive features, see: diff --git a/tools/dist/backport.pl b/tools/dist/backport.pl index 67f8313..df3da22 100755 --- a/tools/dist/backport.pl +++ b/tools/dist/backport.pl @@ -9,11 +9,11 @@ use v5.10.0; # needed for $^V # experimental and "subject to change" in v5.18 (see perl5180delta). Every # use of it now triggers a warning. # -# As of Perl v5.24.1, the semantics of given/when provided by Perl are +# As of Perl v5.26.1, the semantics of given/when provided by Perl are # compatible with those expected by the script, so disable the warning for # those Perls. But don't try to disable the the warning category on Perls # that don't know that category, since that breaks compilation. -no if (v5.17.0 le $^V and $^V le v5.24.1), +no if (v5.17.0 le $^V and $^V le v5.26.1), warnings => 'experimental::smartmatch'; # Licensed to the Apache Software Foundation (ASF) under one @@ -791,7 +791,7 @@ sub vote { # Add to state votes that aren't '+0' or 'edit' $state->{$_->{digest}}++ for grep - +{ qw/-1 t -0 t +1 t/ }->{$_->{vote}}, + +($_->{approval} or $_->{vote} =~ /^(-1|-0|[+]1)$/), @votesarray; } } @@ -1027,7 +1027,7 @@ sub handle_entry { # the "next PROMPT;" is; there's a "last;" at the end of the loop body. PROMPT: while (1) { say ""; - say "\n>>> $entry{header_start}:"; + say "\n\e\x5b32m>>> $entry{header_start}:\e\x5b0m"; say join ", ", map { "r$_" } @{$entry{revisions}} if @{$entry{revisions}}; say "$BRANCHES/$entry{branch}" if $entry{branch}; say "--accept=$entry{accept}" if $entry{accept}; @@ -1196,7 +1196,7 @@ sub backport_main { given ($lines[0]) { # Section header when (/^[A-Z].*:$/i) { - say "\n\n=== $lines[0]" unless $YES; + say "\n\n\e\x5b33m\e\x5b1m=== $lines[0]\e\x5b0m" unless $YES; $in_approved = $lines[0] =~ /^Approved changes/; } # Comment @@ -1279,7 +1279,7 @@ sub nominate_main { # Open the file in line-mode (not paragraph-mode). my @STATUS; tie @STATUS, "Tie::File", $STATUS, recsep => "\n"; - my ($index) = grep { $STATUS[$_] =~ /^Veto/ } (0..$#STATUS); + my ($index) = grep { $STATUS[$_] =~ /^Veto|^Approved/ } (0..$#STATUS); die "Couldn't find where to add an entry" unless $index; # Add an empty line if needed. @@ -1297,7 +1297,8 @@ sub nominate_main { # Done! system "$SVN diff -- $STATUS"; if (prompt "Commit this nomination? ") { - system "$SVN commit -m '* STATUS: Nominate r$revnums[0].' -- $STATUS"; + my $header = join ', ', map "r$_", @revnums; + system "$SVN commit -m '* STATUS: Nominate $header.' -- $STATUS"; exit $?; } elsif (!$had_local_mods or prompt "Revert STATUS (destroying local mods)? ") { diff --git a/tools/dist/backport/status.py b/tools/dist/backport/status.py index 727939d..7ec378d 100644 --- a/tools/dist/backport/status.py +++ b/tools/dist/backport/status.py @@ -194,10 +194,10 @@ class StatusFile: try: entry = StatusEntry(para_text, status_file=self) kind = Kind.nomination - except ParseException: + except ParseException as e: kind = Kind.unknown - logger.warning("Failed to parse entry {!r} in {!r}".format( - para_text, status_fp)) + logger.warning("Failed to parse entry {!r} in {!r}: {}".format( + para_text, status_fp, e)) else: kind = Kind.preamble @@ -379,9 +379,11 @@ class StatusEntry: raise ParseException("Entry found with neither branch nor revisions") # Parse the logsummary. - while lines and not self._is_subheader(lines[0]): + while True: self.logsummary.append(lines[0]) lines = lines[1:] + if (not lines) or self._is_subheader(lines[0]): + break # Parse votes. if "Votes:" in lines: diff --git a/tools/dist/backport_tests.py b/tools/dist/backport_tests.py index ec483a7..27df294 100644 --- a/tools/dist/backport_tests.py +++ b/tools/dist/backport_tests.py @@ -53,6 +53,12 @@ import sys @contextlib.contextmanager def chdir(dir): + """This is a context manager that saves the current working directory's + pathname. Upon entry it chdir's to the argument DIR; upon exit it chdir's + back to the saved pathname. + + The current working directory is restored using os.chdir(), not os.fchdir(). + """ try: saved_dir = os.getcwd() os.chdir(dir) @@ -660,6 +666,21 @@ def backport_unicode_entry(sbox): # Run it. run_backport(sbox) +#---------------------------------------------------------------------- +@BackportTest('76cee987-25c9-4d6c-ad40-000000000013') +def backport_logsummary_colon(sbox): + "a logsummary that looks like a header" + + # r6: nominate r4 + approved_entries = [ + make_entry([4], logsummary="HTTPv2: Add comments."), + ] + sbox.simple_append(STATUS, serialize_STATUS(approved_entries)) + sbox.simple_commit(message='Nominate r4') + + # Run it. + run_backport(sbox) + #---------------------------------------------------------------------- @@ -680,6 +701,7 @@ test_list = [ None, backport_otherproject_change, backport_STATUS_mods, backport_unicode_entry, + backport_logsummary_colon, # When adding a new test, include the test number in the last # 6 bytes of the UUID, in decimal. ] diff --git a/tools/dist/backport_tests_data/backport_logsummary_colon.dump b/tools/dist/backport_tests_data/backport_logsummary_colon.dump new file mode 100644 index 0000000..db9d6ce --- /dev/null +++ b/tools/dist/backport_tests_data/backport_logsummary_colon.dump @@ -0,0 +1,522 @@ +SVN-fs-dump-format-version: 2 + +UUID: 76cee987-25c9-4d6c-ad40-000000000013 + +Revision-number: 0 +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + +Revision-number: 1 +Prop-content-length: 83 +Content-length: 83 + +K 10 +svn:author +V 7 +jrandom +K 7 +svn:log +V 27 +Log message for revision 1. +PROPS-END + +Node-path: A +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/B +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/B/E +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/B/E/alpha +Node-kind: file +Node-action: add +Text-content-md5: d1fa4a3ced98961674a441930a51f2d3 +Text-content-sha1: b347d1da69df9a6a70433ceeaa0d46c8483e8c03 +Prop-content-length: 10 +Text-content-length: 26 +Content-length: 36 + +PROPS-END +This is the file 'alpha'. + + +Node-path: A/B/E/beta +Node-kind: file +Node-action: add +Text-content-md5: 67c756078f24f946f6ec2d00d02f50e1 +Text-content-sha1: d001710ac8e622c6d1fe59b1e265a3908acdd2a3 +Prop-content-length: 10 +Text-content-length: 25 +Content-length: 35 + +PROPS-END +This is the file 'beta'. + + +Node-path: A/B/F +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/B/lambda +Node-kind: file +Node-action: add +Text-content-md5: 911c7a8d869b8c1e566f57da54d889c6 +Text-content-sha1: 784a9298366863da2b65ebf82b4e1123755a2421 +Prop-content-length: 10 +Text-content-length: 27 +Content-length: 37 + +PROPS-END +This is the file 'lambda'. + + +Node-path: A/C +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/D +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/D/G +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/D/G/pi +Node-kind: file +Node-action: add +Text-content-md5: adddfc3e6b605b5f90ceeab11b4e8ab6 +Text-content-sha1: 411e258dc14b42701fdc29b75f653e93f8686415 +Prop-content-length: 10 +Text-content-length: 23 +Content-length: 33 + +PROPS-END +This is the file 'pi'. + + +Node-path: A/D/G/rho +Node-kind: file +Node-action: add +Text-content-md5: 82f2211cf4ab22e3555fc7b835fbc604 +Text-content-sha1: 56388a031dffbf9df7c32e1f299b1d5d7ef60881 +Prop-content-length: 10 +Text-content-length: 24 +Content-length: 34 + +PROPS-END +This is the file 'rho'. + + +Node-path: A/D/G/tau +Node-kind: file +Node-action: add +Text-content-md5: 9936e2716e469bb686deb98c280ead58 +Text-content-sha1: 62e8c07d56bee94ea4577e80414fa8805aaf0175 +Prop-content-length: 10 +Text-content-length: 24 +Content-length: 34 + +PROPS-END +This is the file 'tau'. + + +Node-path: A/D/H +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: A/D/H/chi +Node-kind: file +Node-action: add +Text-content-md5: 8f5ebad6d1f7775c2682e54417cbe4d3 +Text-content-sha1: abeac1bf62099ab66b44779198dc19f40e3244f4 +Prop-content-length: 10 +Text-content-length: 24 +Content-length: 34 + +PROPS-END +This is the file 'chi'. + + +Node-path: A/D/H/omega +Node-kind: file +Node-action: add +Text-content-md5: fe4ec8bdd3d2056db4f55b474a10fadc +Text-content-sha1: c06e671bf15a6af55086176a0931d3b5034c82e6 +Prop-content-length: 10 +Text-content-length: 26 +Content-length: 36 + +PROPS-END +This is the file 'omega'. + + +Node-path: A/D/H/psi +Node-kind: file +Node-action: add +Text-content-md5: e81f8f68ba50e749c200cb3c9ce5d2b1 +Text-content-sha1: 9c438bde39e8ccbbd366df2638e3cb6700950204 +Prop-content-length: 10 +Text-content-length: 24 +Content-length: 34 + +PROPS-END +This is the file 'psi'. + + +Node-path: A/D/gamma +Node-kind: file +Node-action: add +Text-content-md5: 412138bd677d64cd1c32fafbffe6245d +Text-content-sha1: 74b75d7f2e1a0292f17d5a57c570bd89783f5d1c +Prop-content-length: 10 +Text-content-length: 26 +Content-length: 36 + +PROPS-END +This is the file 'gamma'. + + +Node-path: A/mu +Node-kind: file +Node-action: add +Text-content-md5: baf78ae06a2d5b7d9554c5f1280d3fa8 +Text-content-sha1: b4d00c56351d1a752e24d839d41a362d8da4a4c7 +Prop-content-length: 10 +Text-content-length: 23 +Content-length: 33 + +PROPS-END +This is the file 'mu'. + + +Node-path: iota +Node-kind: file +Node-action: add +Text-content-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc +Text-content-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2 +Prop-content-length: 10 +Text-content-length: 25 +Content-length: 35 + +PROPS-END +This is the file 'iota'. + + +Revision-number: 2 +Prop-content-length: 68 +Content-length: 68 + +K 10 +svn:author +V 7 +jrandom +K 7 +svn:log +V 12 +Create trunk +PROPS-END + +Node-path: subversion +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: subversion/branches +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: subversion/tags +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: subversion/trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: subversion/trunk/A +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 1 +Node-copyfrom-path: A + + +Node-path: subversion/trunk/iota +Node-kind: file +Node-action: add +Node-copyfrom-rev: 1 +Node-copyfrom-path: iota +Text-copy-source-md5: 2d18c5e57e84c5b8a5e9a6e13fa394dc +Text-copy-source-sha1: 2c0aa9014a0cd07f01795a333d82485ef6d083e2 + + +Node-path: A +Node-action: delete + + +Node-path: iota +Node-action: delete + + +Revision-number: 3 +Prop-content-length: 87 +Content-length: 87 + +K 10 +svn:author +V 7 +jrandom +K 7 +svn:log +V 31 +Create branch, with STATUS file +PROPS-END + +Node-path: branch +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: subversion/trunk + + +Node-path: branch/STATUS +Node-kind: file +Node-action: add +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709 +Prop-content-length: 10 +Text-content-length: 0 +Content-length: 10 + +PROPS-END + + +Revision-number: 4 +Prop-content-length: 68 +Content-length: 68 + +K 10 +svn:author +V 7 +jrandom +K 7 +svn:log +V 12 +First change +PROPS-END + +Node-path: subversion/trunk/iota +Node-kind: file +Node-action: change +Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847 +Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26 +Text-content-length: 38 +Content-length: 38 + +This is the file 'iota'. +First change + + +Revision-number: 5 +Prop-content-length: 69 +Content-length: 69 + +K 10 +svn:author +V 7 +jrandom +K 7 +svn:log +V 13 +Second change +PROPS-END + +Node-path: subversion/trunk/A/mu +Node-kind: file +Node-action: change +Text-content-md5: eab751301b4e650c83324dfef4aad667 +Text-content-sha1: ab36cad564c7c50dec5ac1eb0bf879cf4e3a5f99 +Text-content-length: 37 +Content-length: 37 + +This is the file 'mu'. +Second change + + +Revision-number: 6 +Prop-content-length: 67 +Content-length: 67 + +K 10 +svn:author +V 7 +jrandom +K 7 +svn:log +V 11 +Nominate r4 +PROPS-END + +Node-path: branch/STATUS +Node-kind: file +Node-action: change +Text-content-md5: 21cf72d59d9e71a49260e3cf31f78b76 +Text-content-sha1: 9938559a403d5a18099aace3db6719bdb8940449 +Text-content-length: 245 +Content-length: 245 + +Status of 1.8.x: + +Candidate changes: +================== + +Random new subheading: +====================== + +Veto-blocked changes: +===================== + +Approved changes: +================= + + * r4 + HTTPv2: Add comments. + Votes: + +1: jrandom + + + +Revision-number: 7 +Prop-content-length: 135 +Content-length: 135 + +K 10 +svn:author +V 6 +daniel +K 7 +svn:log +V 80 +Merge r4 from trunk: + + * r4 + HTTPv2: Add comments. + Votes: + +1: jrandom + +PROPS-END + +Node-path: branch +Node-kind: dir +Node-action: change +Prop-content-length: 54 +Content-length: 54 + +K 13 +svn:mergeinfo +V 19 +/subversion/trunk:4 +PROPS-END + + +Node-path: branch/STATUS +Node-kind: file +Node-action: change +Text-content-md5: 6f71fec92afeaa5c1ebe02349f548ca9 +Text-content-sha1: eece02003d9c51610249e3fdd0d4e191e02ba3b7 +Text-content-length: 185 +Content-length: 185 + +Status of 1.8.x: + +Candidate changes: +================== + +Random new subheading: +====================== + +Veto-blocked changes: +===================== + +Approved changes: +================= + + +Node-path: branch/iota +Node-kind: file +Node-action: change +Text-content-md5: 67f471c2ecc2c9e561d122d6e6b0f847 +Text-content-sha1: 750accb6e7f880a1d05ce725c19eb60183bb4b26 +Text-content-length: 38 +Content-length: 38 + +This is the file 'iota'. +First change + + diff --git a/tools/dist/changes-to-html.py b/tools/dist/changes-to-html.py new file mode 100755 index 0000000..73ec19f --- /dev/null +++ b/tools/dist/changes-to-html.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# python: coding=utf-8 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +import re +import sys + + +HEADER = """<html> +<head> +<title>CHANGES</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> +</head> +<body> +<pre> +""" + +FOOTER = """</pre> +</body> +</html> +""" + + +url_rx = re.compile(r'(?:^|(?<=\W))(https?://[\w\d./?&=]+)') +url_sub = r'<a href="\1">\1</a>' + +issue_rx = re.compile(r'(?<=\W)(#(\d+))') +issue_sub = r'<a href="https://issues.apache.org/jira/browse/SVN-\2">\1</a>' + +revision_rx = re.compile(r'(?:^|(?<=\W))(r\d+)') +revision_sub = r'<a href="https://svn.apache.org/\1">\1</a>' + +branchtag_rx = re.compile(r'(?<=\W)(/(?:branches|tags)/[\w\d.]+)') +branchtag_sub = r'<a href="https://svn.apache.org/repos/asf/subversion\1">\1</a>' + + +def generate(stream): + sys.stdout.write(HEADER) + + beginning = True + for n in stream.readlines(): + # Skip initial comments and empty lines in the CHANGES file. + n = n.rstrip() + if beginning and (not n or n.startswith('#')): + continue + beginning = False + + n = url_rx.sub(url_sub, n) + n = issue_rx.sub(issue_sub, n) + n = revision_rx.sub(revision_sub, n) + n = branchtag_rx.sub(branchtag_sub, n) + + sys.stdout.write(n) + sys.stdout.write('\n') + + sys.stdout.write(FOOTER) + + +def generate_from(filenme): + with open(filenme, 'rt') as stream: + return generate(stream) + + +def main(): + if len(sys.argv) < 2 or sys.argv[1] == '-': + return generate(sys.stdin) + else: + return generate_from(sys.argv[1]) + +if __name__ == '__main__': + main() diff --git a/tools/dist/create-minor-release-branch.py b/tools/dist/create-minor-release-branch.py new file mode 100755 index 0000000..a68fa23 --- /dev/null +++ b/tools/dist/create-minor-release-branch.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python +# python: coding=utf-8 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + + +# About this script: +# This script is intended to automate steps in creating a new Subversion +# minor release. + +import os +import re +import sys +import logging +import subprocess +import argparse # standard in Python 2.7 + +from release import Version + + +# Some constants +repos = 'https://svn.apache.org/repos/asf/subversion' +secure_repos = 'https://svn.apache.org/repos/asf/subversion' +buildbot_repos = 'https://svn.apache.org/repos/infra/infrastructure/buildbot/aegis/buildmaster' + +# Parameters +dry_run = False + +# Local working copies +base_dir = None # set by main() + +def get_trunk_wc_path(path=None): + trunk_wc_path = os.path.join(base_dir, 'svn-trunk') + if path is None: return trunk_wc_path + return os.path.join(trunk_wc_path, path) +def get_branch_wc_path(ver, path=None): + branch_wc_path = os.path.join(base_dir, ver.branch + '.x') + if path is None: return branch_wc_path + return os.path.join(branch_wc_path, path) +def get_buildbot_wc_path(path=None): + buildbot_wc_path = os.path.join(base_dir, 'svn-buildmaster') + if path is None: return buildbot_wc_path + return os.path.join(buildbot_wc_path, path) + +def get_trunk_url(): + return secure_repos + '/trunk' +def get_branch_url(ver): + return secure_repos + '/branches/' + ver.branch + '.x' +def get_tag_url(ver): + return secure_repos + '/tags/' + ver.base +def get_buildbot_url(): + return buildbot_repos + +#---------------------------------------------------------------------- +# Utility functions + +def run(cmd, dry_run=False): + print('+ ' + ' '.join(cmd)) + if not dry_run: + stdout = subprocess.check_output(cmd) + print(stdout) + else: + print(' ## dry-run; not executed') + +def run_svn(cmd, dry_run=False): + run(['svn'] + cmd, dry_run) + +def svn_commit(cmd): + run_svn(['commit'] + cmd, dry_run=dry_run) + +def svn_copy_branch(src, dst, message): + args = ['copy', src, dst, '-m', message] + run_svn(args, dry_run=dry_run) + +def svn_checkout(url, wc, *args): + args = ['checkout', url, wc] + list(args) + run_svn(args) + +#---------------------------------------------------------------------- +def edit_file(path, pattern, replacement): + print("Editing '%s'" % (path,)) + print(" pattern='%s'" % (pattern,)) + print(" replace='%s'" % (replacement,)) + old_text = open(path, 'r').read() + new_text = re.sub(pattern, replacement, old_text) + assert new_text != old_text + open(path, 'w').write(new_text) + +def edit_changes_file(path, newtext): + """Insert NEWTEXT in the 'CHANGES' file found at PATH, + just before the first line that starts with 'Version '. + """ + print("Prepending to '%s'" % (path,)) + print(" text='%s'" % (newtext,)) + lines = open(path, 'r').readlines() + for i, line in enumerate(lines): + if line.startswith('Version '): + with open(path, 'w') as newfile: + newfile.writelines(lines[:i]) + newfile.write(newtext) + newfile.writelines(lines[i:]) + break + +#---------------------------------------------------------------------- +def make_release_branch(ver, revnum): + svn_copy_branch(get_trunk_url() + '@' + (str(revnum) if revnum else ''), + get_branch_url(ver), + 'Create the ' + ver.branch + '.x release branch.') + +#---------------------------------------------------------------------- +def update_minor_ver_in_trunk(ver, revnum): + """Change the minor version in trunk to the next (future) minor version. + """ + trunk_wc = get_trunk_wc_path() + trunk_url = get_trunk_url() + svn_checkout(trunk_url + '@' + (str(revnum) if revnum else ''), + trunk_wc) + + prev_ver = Version('1.%d.0' % (ver.minor - 1,)) + next_ver = Version('1.%d.0' % (ver.minor + 1,)) + relpaths = [] + + relpath = 'subversion/include/svn_version.h' + relpaths.append(relpath) + edit_file(get_trunk_wc_path(relpath), + r'(#define SVN_VER_MINOR *)%s' % (ver.minor,), + r'\g<1>%s' % (next_ver.minor,)) + + relpath = 'subversion/tests/cmdline/svntest/main.py' + relpaths.append(relpath) + edit_file(get_trunk_wc_path(relpath), + r'(SVN_VER_MINOR = )%s' % (ver.minor,), + r'\g<1>%s' % (next_ver.minor,)) + + relpath = 'subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java' + relpaths.append(relpath) + try: + # since r1817921 (just after branching 1.10) + edit_file(get_trunk_wc_path(relpath), + r'SVN_VER_MINOR = %s;' % (ver.minor,), + r'SVN_VER_MINOR = %s;' % (next_ver.minor,)) + except: + # before r1817921: two separate places + edit_file(get_trunk_wc_path(relpath), + r'version.isAtLeast\(1, %s, 0\)' % (ver.minor,), + r'version.isAtLeast\(1, %s, 0\)' % (next_ver.minor,)) + edit_file(get_trunk_wc_path(relpath), + r'1.%s.0, but' % (ver.minor,), + r'1.%s.0, but' % (next_ver.minor,)) + + relpath = 'CHANGES' + relpaths.append(relpath) + # insert at beginning of CHANGES file + edit_changes_file(get_trunk_wc_path(relpath), + 'Version ' + next_ver.base + '\n' + + '(?? ??? 20XX, from /branches/' + next_ver.branch + '.x)\n' + + get_tag_url(next_ver) + '\n' + + '\n') + + log_msg = '''\ +Increment the trunk version number to %s, and introduce a new CHANGES +section, following the creation of the %s.x release branch. + +* subversion/include/svn_version.h, + subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java, + subversion/tests/cmdline/svntest/main.py + (SVN_VER_MINOR): Increment to %s. + +* CHANGES: New section for %s.0. +''' % (next_ver.branch, ver.branch, next_ver.minor, next_ver.branch) + commit_paths = [get_trunk_wc_path(p) for p in relpaths] + svn_commit(commit_paths + ['-m', log_msg]) + +#---------------------------------------------------------------------- +def create_status_file_on_branch(ver): + branch_wc = get_branch_wc_path(ver) + branch_url = get_branch_url(ver) + svn_checkout(branch_url, branch_wc, '--depth=immediates') + + status_local_path = os.path.join(branch_wc, 'STATUS') + text='''\ + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * THIS RELEASE STREAM IS OPEN FOR STABILIZATION. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +This file tracks the status of releases in the %s.x line. + +See http://subversion.apache.org/docs/community-guide/releasing.html#release-stabilization +for details on how release lines and voting work, what kinds of bugs can +delay a release, etc. + +Status of %s: + +Candidate changes: +================== + + +Veto-blocked changes: +===================== + + +Approved changes: +================= +''' % (ver.branch, ver.base) + open(status_local_path, 'wx').write(text) + run_svn(['add', status_local_path]) + svn_commit([status_local_path, + '-m', '* branches/' + ver.branch + '.x/STATUS: New file.']) + +#---------------------------------------------------------------------- +def update_backport_bot(ver): + print("""MANUAL STEP: Fork & edit & pull-request on GitHub: +https://github.com/apache/infrastructure-puppet/blob/deployment/modules/svnqavm_pvm_asf/manifests/init.pp +"Add new %s.x branch to list of backport branches" +""" % (ver.branch,)) + print("""Someone needs to run the 'svn checkout' manually. +The exact checkout command is documented in machines/svn-qavm2/notes.txt +in the private repository (need to use a trunk client and the svn-master.a.o +hostname). +""") + +#---------------------------------------------------------------------- +def update_buildbot_config(ver): + """Add the new branch to the list of branches monitored by the buildbot + master. + """ + buildbot_wc = get_buildbot_wc_path() + buildbot_url = get_buildbot_url() + svn_checkout(buildbot_url, buildbot_wc) + + prev_ver = Version('1.%d.0' % (ver.minor - 1,)) + next_ver = Version('1.%d.0' % (ver.minor + 1,)) + + relpath = 'master1/projects/subversion.conf' + edit_file(get_buildbot_wc_path(relpath), + r'(MINOR_LINES=\[.*%s)(\])' % (prev_ver.minor,), + r'\1, %s\2' % (ver.minor,)) + + log_msg = '''\ +Subversion: start monitoring the %s branch. +''' % (ver.branch) + commit_paths = [get_buildbot_wc_path(relpath)] + svn_commit(commit_paths + ['-m', log_msg]) + +#---------------------------------------------------------------------- +def create_release_branch(args): + make_release_branch(args.version, args.revnum) + update_minor_ver_in_trunk(args.version, args.revnum) + create_status_file_on_branch(args.version) + update_backport_bot(args.version) + update_buildbot_config(args.version) + + +#---------------------------------------------------------------------- +# Main entry point for argument parsing and handling + +def main(): + 'Parse arguments, and drive the appropriate subcommand.' + + # Setup our main parser + parser = argparse.ArgumentParser( + description='Create an Apache Subversion release branch.') + subparsers = parser.add_subparsers(title='subcommands') + + # Setup the parser for the create-release-branch subcommand + subparser = subparsers.add_parser('create-release-branch', + help='''Create a minor release branch: branch from trunk, + update version numbers on trunk, create status + file on branch, update backport bot, + update buildbot config.''') + subparser.set_defaults(func=create_release_branch) + subparser.add_argument('version', type=Version, + help='''A version number to indicate the branch, such as + '1.7.0' (the '.0' is required).''') + subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')), + nargs='?', default=None, + help='''The trunk revision number to base the branch on. + Default is HEAD.''') + subparser.add_argument('--dry-run', action='store_true', default=False, + help='Avoid committing any changes to repositories.') + subparser.add_argument('--verbose', action='store_true', default=False, + help='Increase output verbosity') + subparser.add_argument('--base-dir', default=os.getcwd(), + help='''The directory in which to create needed files and + folders. The default is the current working + directory.''') + + # Parse the arguments + args = parser.parse_args() + + global base_dir, dry_run + base_dir = args.base_dir + dry_run = args.dry_run + + # Set up logging + logger = logging.getLogger() + if args.verbose: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + # Make timestamps in tarballs independent of local timezone + os.environ['TZ'] = 'UTC' + + # finally, run the subcommand, and give it the parsed arguments + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/tools/dist/edit-N-log-messages b/tools/dist/edit-N-log-messages new file mode 100755 index 0000000..fa6a2a2 --- /dev/null +++ b/tools/dist/edit-N-log-messages @@ -0,0 +1,94 @@ +#!/usr/bin/env zsh +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -e + +# setup +0=${0:t}; NAME=$0 +svn=${SVN:-svn} +svnadmin=${SVNADMIN:-svnadmin} + +usage() { + echo "usage: $NAME -r N:M" + echo " will retrieve all log messages in the range rN:rM, open them in" + echo " \$EDITOR, and commit any edits to them." + echo "" + echo " Run this with cwd being a working copy of ^/subversion/trunk." + # TODO: We only need the cwd in case SVN_CLIENT__REVKIND_NEEDS_WC() is true + # for N or M. When N and M are both numeric or HEAD, teach this script + # to disregard its cwd and contact ^/subversion/trunk directly. +} + +# parse argv +while getopts r: letter; do + if [[ $letter == r ]]; then + revision_range=$OPTARG + else + usage >&2 + exit 1 + fi +done +shift $((OPTIND - 1)) +[[ -n $revision_range ]] || { usage >&2; exit 1; } + +# get log messages +cd "$(mktemp -d)" +echo "${0}: Temporary directory: ${(q-)PWD}" +revisions=( ${(f)"$( + # We use $OLDPWD rather than a URL in case $revision_range is, say, + # "BASE:HEAD". + $svn log -q -r $revision_range -- $OLDPWD | + grep '^r' | cut -d' ' -f1 | grep '^r[0-9]*$' +)"} ) +for rN in $revisions; do + $svn propget --revprop -r $rN --strict 'svn:log' -- $OLDPWD > ./$rN & +done +wait + +# set up to detect which ones have changed +$svnadmin create ./.r +$svn checkout -q file://$PWD/.r ./ +$svn add -q ./r* +$svn commit -q -m "Import revisions $revision_range" +rm -rf ./.r # (!) + +# edit +case $EDITOR in + (*vim*) $EDITOR -p ./r*;; + (*) $EDITOR ./r*;; +esac + +# prompt for permission to continue +if $svn status -q | grep -q ''; then + $svn status -q + read -q "?Commit the above propedits? " || { echo "${0}: exiting"; exit 0; } + echo "" +else + echo "${0}: No changes." + echo "${0}: You can remove ${(q-)PWD} now." + exit 0 +fi + +# make propedits +# TODO: make these changes atomically +# 'svn propedit' is atomic and could drive a non-interactive $EDITOR, but for +# now, we just trust the committer to read commits@. +targets=( ${(f)"$($svn status -q | grep '^M' | cut -c9-)"} ) +for i in $targets; do + $svn propset --revprop -r $i -F ./$i -- 'svn:log' $OLDPWD +done diff --git a/tools/dist/nightly.sh b/tools/dist/nightly.sh index b167ab3..c4f8ebf 100755 --- a/tools/dist/nightly.sh +++ b/tools/dist/nightly.sh @@ -21,7 +21,7 @@ # set -e -repo=http://svn.apache.org/repos/asf/subversion +repo=https://svn.apache.org/repos/asf/subversion svn=svn olds=7 diff --git a/tools/dist/release.py b/tools/dist/release.py index 5518d6f..5b12c00 100755 --- a/tools/dist/release.py +++ b/tools/dist/release.py @@ -51,6 +51,9 @@ import operator import itertools import subprocess import argparse # standard in Python 2.7 +import io + +import backport.status # Find ezt, using Subversion's copy, if there isn't one on the system. try: @@ -71,16 +74,40 @@ tool_versions = { '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'], 'libtool' : ['2.4.6', 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'], - 'swig' : ['3.0.10', - '2939aae39dec06095462f1b95ce1c958ac80d07b926e48871046d17c0094f44c'], + 'swig' : ['3.0.12', + '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'], + }, + '1.13' : { + 'autoconf' : ['2.69', + '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'], + 'libtool' : ['2.4.6', + 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'], + 'swig' : ['3.0.12', + '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'], + }, + '1.12' : { + 'autoconf' : ['2.69', + '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'], + 'libtool' : ['2.4.6', + 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'], + 'swig' : ['3.0.12', + '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'], + }, + '1.11' : { + 'autoconf' : ['2.69', + '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'], + 'libtool' : ['2.4.6', + 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'], + 'swig' : ['3.0.12', + '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'], }, '1.10' : { 'autoconf' : ['2.69', '954bd69b391edc12d6a4a51a2dd1476543da5c6bbf05a95b59dc0dd6fd4c2969'], 'libtool' : ['2.4.6', 'e3bd4d5d3d025a36c21dd6af7ea818a2afcd4dfc1ea5a17b39d7854bcd0c06e3'], - 'swig' : ['3.0.10', - '2939aae39dec06095462f1b95ce1c958ac80d07b926e48871046d17c0094f44c'], + 'swig' : ['3.0.12', + '7cf9f447ae7ed1c51722efc45e7f14418d15d7a1e143ac9f09a668999f4fc94d'], }, '1.9' : { 'autoconf' : ['2.69', @@ -102,14 +129,16 @@ tool_versions = { # The version that is our current recommended release # ### TODO: derive this from svn_version.h; see ../../build/getversion.py -recommended_release = '1.9' +recommended_release = '1.12' +# For clean-dist, a whitelist of artifacts to keep, by version. +supported_release_lines = frozenset({"1.9", "1.10", "1.12", "1.13"}) # Some constants -repos = 'https://svn.apache.org/repos/asf/subversion' -secure_repos = 'https://svn.apache.org/repos/asf/subversion' +svn_repos = 'https://svn.apache.org/repos/asf/subversion' dist_repos = 'https://dist.apache.org/repos/dist' dist_dev_url = dist_repos + '/dev/subversion' dist_release_url = dist_repos + '/release/subversion' +dist_archive_url = 'https://archive.apache.org/dist/subversion' KEYS = 'https://people.apache.org/keys/group/subversion.asc' extns = ['zip', 'tar.gz', 'tar.bz2'] @@ -174,7 +203,7 @@ class Version(object): ver_tag = '" (Alpha %d)"' % self.pre_num ver_numtag = '"-alpha%d"' % self.pre_num elif self.pre == 'beta': - ver_tag = '" (Beta %d)"' % args.version.pre_num + ver_tag = '" (Beta %d)"' % self.pre_num ver_numtag = '"-beta%d"' % self.pre_num elif self.pre == 'rc': ver_tag = '" (Release Candidate %d)"' % self.pre_num @@ -263,6 +292,15 @@ def get_target(args): else: return get_deploydir(args.base_dir) +def get_branch_path(args): + if not args.branch: + try: + args.branch = 'branches/%d.%d.x' % (args.version.major, args.version.minor) + except AttributeError: + raise RuntimeError("Please specify the release version label or --branch-path") + + return args.branch.rstrip('/') # canonicalize for later comparisons + def get_tmpldir(): return os.path.join(os.path.abspath(sys.path[0]), 'templates') @@ -271,7 +309,7 @@ def get_tmplfile(filename): return open(os.path.join(get_tmpldir(), filename)) except IOError: # Hmm, we had a problem with the local version, let's try the repo - return urllib2.urlopen(repos + '/trunk/tools/dist/templates/' + filename) + return urllib2.urlopen(svn_repos + '/trunk/tools/dist/templates/' + filename) def get_nullfile(): return open(os.path.devnull, 'w') @@ -531,16 +569,12 @@ def replace_lines(path, actions): def roll_tarballs(args): 'Create the release artifacts.' - if not args.branch: - args.branch = 'branches/%d.%d.x' % (args.version.major, args.version.minor) - - branch = args.branch # shorthand - branch = branch.rstrip('/') # canonicalize for later comparisons + branch = get_branch_path(args) logging.info('Rolling release %s from branch %s@%d' % (args.version, branch, args.revnum)) - check_copyright_year(repos, args.branch, args.revnum) + check_copyright_year(svn_repos, branch, args.revnum) # Ensure we've got the appropriate rolling dependencies available autoconf = AutoconfDep(args.base_dir, False, args.verbose, @@ -559,7 +593,7 @@ def roll_tarballs(args): if branch != 'trunk': # Make sure CHANGES is sync'd. - compare_changes(repos, branch, args.revnum) + compare_changes(svn_repos, branch, args.revnum) # Ensure the output directory doesn't already exist if os.path.exists(get_deploydir(args.base_dir)): @@ -571,7 +605,7 @@ def roll_tarballs(args): logging.info('Preparing working copy source') shutil.rmtree(get_workdir(args.base_dir), True) run_script(args.verbose, 'svn checkout %s %s' - % (repos + '/' + branch + '@' + str(args.revnum), + % (svn_repos + '/' + branch + '@' + str(args.revnum), get_workdir(args.base_dir))) # Exclude stuff we don't want in the tarball, it will not be present @@ -712,9 +746,13 @@ def roll_tarballs(args): filepath = os.path.join(get_tempdir(args.base_dir), filename) shutil.move(filepath, get_deploydir(args.base_dir)) filepath = os.path.join(get_deploydir(args.base_dir), filename) - m = hashlib.sha1() - m.update(open(filepath, 'r').read()) - open(filepath + '.sha1', 'w').write(m.hexdigest()) + if args.version < Version("1.11.0-alpha1"): + # 1.10 and earlier generate *.sha1 files for compatibility reasons. + # They are deprecated, however, so we don't publicly link them in + # the announcements any more. + m = hashlib.sha1() + m.update(open(filepath, 'r').read()) + open(filepath + '.sha1', 'w').write(m.hexdigest()) m = hashlib.sha512() m.update(open(filepath, 'r').read()) open(filepath + '.sha512', 'w').write(m.hexdigest()) @@ -737,8 +775,12 @@ def sign_candidates(args): def sign_file(filename): asc_file = open(filename + '.asc', 'a') logging.info("Signing %s" % filename) - proc = subprocess.check_call(['gpg', '-ba', '-o', '-', filename], - stdout=asc_file) + if args.userid: + proc = subprocess.check_call(['gpg', '-ba', '-u', args.userid, + '-o', '-', filename], stdout=asc_file) + else: + proc = subprocess.check_call(['gpg', '-ba', '-o', '-', filename], + stdout=asc_file) asc_file.close() target = get_target(args) @@ -746,7 +788,7 @@ def sign_candidates(args): for e in extns: filename = os.path.join(target, 'subversion-%s.%s' % (args.version, e)) sign_file(filename) - if args.version.major >= 1 and args.version.minor <= 6: + if args.version.major == 1 and args.version.minor <= 6: filename = os.path.join(target, 'subversion-deps-%s.%s' % (args.version, e)) sign_file(filename) @@ -773,26 +815,24 @@ def post_candidates(args): #---------------------------------------------------------------------- # Create tag +# Bump versions on branch -def create_tag(args): +def create_tag_only(args): 'Create tag in the repository' target = get_target(args) logging.info('Creating tag for %s' % str(args.version)) - if not args.branch: - args.branch = 'branches/%d.%d.x' % (args.version.major, args.version.minor) - - branch = secure_repos + '/' + args.branch.rstrip('/') + branch_url = svn_repos + '/' + get_branch_path(args) - tag = secure_repos + '/tags/' + str(args.version) + tag = svn_repos + '/tags/' + str(args.version) svnmucc_cmd = ['svnmucc', '-m', 'Tagging release ' + str(args.version)] if (args.username): svnmucc_cmd += ['--username', args.username] - svnmucc_cmd += ['cp', str(args.revnum), branch, tag] + svnmucc_cmd += ['cp', str(args.revnum), branch_url, tag] svnmucc_cmd += ['put', os.path.join(target, 'svn_version.h.dist' + '-' + str(args.version)), tag + '/subversion/include/svn_version.h'] @@ -805,62 +845,84 @@ def create_tag(args): logging.error("Do you need to pass --branch=trunk?") raise +def bump_versions_on_branch(args): + 'Bump version numbers on branch' + + logging.info('Bumping version numbers on the branch') + + branch_url = svn_repos + '/' + get_branch_path(args) + + def replace_in_place(fd, startofline, flat, spare): + """In file object FD, replace FLAT with SPARE in the first line + starting with regex STARTOFLINE.""" + + pattern = r'^(%s)%s' % (startofline, re.escape(flat)) + repl = r'\g<1>%s' % (spare,) + fd.seek(0, os.SEEK_SET) + lines = fd.readlines() + for i, line in enumerate(lines): + replacement = re.sub(pattern, repl, line) + if replacement != line: + lines[i] = replacement + break + else: + raise RuntimeError("Could not replace r'%s' with r'%s' in '%s'" + % (pattern, repl, fd.url)) + + fd.seek(0, os.SEEK_SET) + fd.writelines(lines) + fd.truncate() # for current callers, new value is never shorter. + + new_version = Version('%d.%d.%d' % + (args.version.major, args.version.minor, + args.version.patch + 1)) + + HEAD = subprocess.check_output(['svn', 'info', '--show-item=revision', + '--', branch_url]).strip() + HEAD = int(HEAD) + def file_object_for(relpath): + fd = tempfile.NamedTemporaryFile() + url = branch_url + '/' + relpath + fd.url = url + subprocess.check_call(['svn', 'cat', '%s@%d' % (url, HEAD)], + stdout=fd) + return fd + + svn_version_h = file_object_for('subversion/include/svn_version.h') + replace_in_place(svn_version_h, '#define SVN_VER_PATCH *', + str(args.version.patch), str(new_version.patch)) + + STATUS = file_object_for('STATUS') + replace_in_place(STATUS, 'Status of ', + str(args.version), str(new_version)) + + svn_version_h.seek(0, os.SEEK_SET) + STATUS.seek(0, os.SEEK_SET) + subprocess.check_call(['svnmucc', '-r', str(HEAD), + '-m', 'Post-release housekeeping: ' + 'bump the %s branch to %s.' + % (branch_url.split('/')[-1], str(new_version)), + 'put', svn_version_h.name, svn_version_h.url, + 'put', STATUS.name, STATUS.url, + ]) + del svn_version_h + del STATUS + +def create_tag_and_bump_versions(args): + '''Create tag in the repository and, if not a prerelease version, + bump version numbers on the branch''' + + create_tag_only(args) + if not args.version.is_prerelease(): - logging.info('Bumping revisions on the branch') - def replace_in_place(fd, startofline, flat, spare): - """In file object FD, replace FLAT with SPARE in the first line - starting with STARTOFLINE.""" - - fd.seek(0, os.SEEK_SET) - lines = fd.readlines() - for i, line in enumerate(lines): - if line.startswith(startofline): - lines[i] = line.replace(flat, spare) - break - else: - raise RuntimeError('Definition of %r not found' % startofline) - - fd.seek(0, os.SEEK_SET) - fd.writelines(lines) - fd.truncate() # for current callers, new value is never shorter. - - new_version = Version('%d.%d.%d' % - (args.version.major, args.version.minor, - args.version.patch + 1)) - - def file_object_for(relpath): - fd = tempfile.NamedTemporaryFile() - url = branch + '/' + relpath - fd.url = url - subprocess.check_call(['svn', 'cat', '%s@%d' % (url, args.revnum)], - stdout=fd) - return fd - - svn_version_h = file_object_for('subversion/include/svn_version.h') - replace_in_place(svn_version_h, '#define SVN_VER_PATCH ', - str(args.version.patch), str(new_version.patch)) - - STATUS = file_object_for('STATUS') - replace_in_place(STATUS, 'Status of ', - str(args.version), str(new_version)) - - svn_version_h.seek(0, os.SEEK_SET) - STATUS.seek(0, os.SEEK_SET) - subprocess.check_call(['svnmucc', '-r', str(args.revnum), - '-m', 'Post-release housekeeping: ' - 'bump the %s branch to %s.' - % (branch.split('/')[-1], str(new_version)), - 'put', svn_version_h.name, svn_version_h.url, - 'put', STATUS.name, STATUS.url, - ]) - del svn_version_h - del STATUS + bump_versions_on_branch(args) #---------------------------------------------------------------------- # Clean dist def clean_dist(args): - 'Clean the distribution directory of all but the most recent artifacts.' + '''Clean the distribution directory of release artifacts of + no-longer-supported minor lines.''' stdout = subprocess.check_output(['svn', 'list', dist_release_url]) @@ -872,21 +934,20 @@ def clean_dist(args): filenames = stdout.split('\n') filenames = filter(lambda x: x.startswith('subversion-'), filenames) versions = set(map(Version, filenames)) - minor_lines = set(map(minor, versions)) to_keep = set() - # Keep 3 minor lines: 1.10.0-alpha3, 1.9.7, 1.8.19. # TODO: When we release 1.A.0 GA we'll have to manually remove 1.(A-2).* artifacts. - for recent_line in sorted(minor_lines, reverse=True)[:3]: - to_keep.add(max( + for line_to_keep in [minor(Version(x + ".0")) for x in supported_release_lines]: + candidates = list( x for x in versions - if minor(x) == recent_line - )) + if minor(x) == line_to_keep + ) + if candidates: + to_keep.add(max(candidates)) for i in sorted(to_keep): logging.info("Saving release '%s'", i) svnmucc_cmd = ['svnmucc', '-m', 'Remove old Subversion releases.\n' + - 'They are still available at ' + - 'https://archive.apache.org/dist/subversion/'] + 'They are still available at ' + dist_archive_url] if (args.username): svnmucc_cmd += ['--username', args.username] for filename in filenames: @@ -938,6 +999,7 @@ def write_news(args): 'version_base' : args.version.base, 'anchor': args.version.get_download_anchor(), 'is_recommended': ezt_bool(args.version.is_recommended()), + 'announcement_url': args.announcement_url, } if args.version.is_prerelease(): @@ -947,38 +1009,53 @@ def write_news(args): template = ezt.Template() template.parse(get_tmplfile(template_filename).read()) - template.generate(sys.stdout, data) + + # Insert the output into an existing file if requested, else print it + if args.edit_html_file: + tmp_name = args.edit_html_file + '.tmp' + with open(args.edit_html_file, 'r') as f, open(tmp_name, 'w') as g: + inserted = False + for line in f: + if not inserted and line.startswith('<div class="h3" id="news-'): + template.generate(g, data) + g.write('\n') + inserted = True + g.write(line) + os.remove(args.edit_html_file) + os.rename(tmp_name, args.edit_html_file) + else: + template.generate(sys.stdout, data) -def get_sha1info(args): - 'Return a list of sha1 info for the release' +def get_fileinfo(args): + 'Return a list of file info (filenames) for the release tarballs' target = get_target(args) - sha1s = glob.glob(os.path.join(target, 'subversion*-%s*.sha1' % args.version)) + files = glob.glob(os.path.join(target, 'subversion*-%s.*.asc' % args.version)) + files.sort() class info(object): pass - sha1info = [] - for s in sha1s: + fileinfo = [] + for f in files: i = info() - # strip ".sha1" - i.filename = os.path.basename(s)[:-5] - i.sha1 = open(s, 'r').read() - sha1info.append(i) + # strip ".asc" + i.filename = os.path.basename(f)[:-4] + fileinfo.append(i) - return sha1info + return fileinfo def write_announcement(args): 'Write the release announcement.' - sha1info = get_sha1info(args) - siginfo = "\n".join(get_siginfo(args, True)) + "\n" + siginfo = get_siginfo(args, True) + if not siginfo: + raise RuntimeError("No signatures found for %s at %s" % (args.version, args.target)) data = { 'version' : str(args.version), - 'sha1info' : sha1info, - 'siginfo' : siginfo, + 'siginfo' : "\n".join(siginfo) + "\n", 'major-minor' : args.version.branch, 'major-minor-patch' : args.version.base, 'anchor' : args.version.get_download_anchor(), @@ -1007,10 +1084,10 @@ def write_announcement(args): def write_downloads(args): 'Output the download section of the website.' - sha1info = get_sha1info(args) + fileinfo = get_fileinfo(args) data = { 'version' : str(args.version), - 'fileinfo' : sha1info, + 'fileinfo' : fileinfo, } template = ezt.Template(compress_whitespace = False) @@ -1046,14 +1123,12 @@ def get_siginfo(args, quiet=False): import security._gnupg as gnupg gpg = gnupg.GPG() - target = get_target(args) - good_sigs = {} fingerprints = {} output = [] - glob_pattern = os.path.join(target, 'subversion*-%s*.asc' % args.version) - for filename in glob.glob(glob_pattern): + for fileinfo in get_fileinfo(args): + filename = os.path.join(get_target(args), fileinfo.filename + '.asc') text = open(filename).read() keys = text.split(key_start) @@ -1174,6 +1249,206 @@ def get_keys(args): fd.seek(0) subprocess.check_call(['gpg', '--import'], stdin=fd) +def add_to_changes_dict(changes_dict, audience, section, change, revision): + # Normalize arguments + if audience: + audience = audience.upper() + if section: + section = section.lower() + change = change.strip() + + if not audience in changes_dict: + changes_dict[audience] = dict() + if not section in changes_dict[audience]: + changes_dict[audience][section] = dict() + + changes = changes_dict[audience][section] + if change in changes: + changes[change].add(revision) + else: + changes[change] = set([revision]) + +def print_section(changes_dict, audience, section, title, mandatory=False): + if audience in changes_dict: + audience_changes = changes_dict[audience] + if mandatory or (section in audience_changes): + if title: + print(' - %s:' % title) + if section in audience_changes: + print_changes(audience_changes[section]) + elif mandatory: + print(' (none)') + +def print_changes(changes): + # Print in alphabetical order, so entries with the same prefix are together + for change in sorted(changes): + revs = changes[change] + rev_string = 'r' + str(min(revs)) + (' et al' if len(revs) > 1 else '') + print(' * %s (%s)' % (change, rev_string)) + +def write_changelog(args): + 'Write changelog, parsed from commit messages' + # Changelog lines are lines with the following format: + # '['[audience[:section]]']' <message> + # or: + # <message> '['[audience[:section]]']' + # where audience = U (User-visible) or D (Developer-visible) + # section = general|major|minor|client|server|clientserver|other|api|bindings + # (section is optional and is treated case-insensitively) + # message = the actual text for CHANGES + # + # This means the "changes label" can be used as prefix or suffix, and it + # can also be left empty (which results in an uncategorized changes entry), + # if the committer isn't sure where the changelog entry belongs. + # + # Putting [skip], [ignore], [c:skip] or [c:ignore] somewhere in the + # log message means this commit must be ignored for Changelog processing + # (ignored even with the --include-unlabeled-summaries option). + # + # If there is no changes label anywhere in the commit message, and the + # --include-unlabeled-summaries option is used, we'll consider the summary + # line of the commit message (= first line except if it starts with a *) + # as an uncategorized changes entry, except if it contains "status", + # "changes", "post-release housekeeping" or "follow-up". + # + # Examples: + # [U:major] Better interactive conflict resolution for tree conflicts + # ra_serf: Adjustments for serf versions with HTTP/2 support [U:minor] + # [U] Fix 'svn diff URL@REV WC' wrongly looks up URL@HEAD (issue #4597) + # Fix bug with canonicalizing Window-specific drive-relative URL [] + # New svn_ra_list() API function [D:api] + # [D:bindings] JavaHL: Allow access to constructors of a couple JavaHL classes + + branch_url = svn_repos + '/' + get_branch_path(args) + previous = svn_repos + '/' + args.previous + include_unlabeled = args.include_unlabeled + separator_line = ('-' * 72) + '\n' + + mergeinfo = subprocess.check_output(['svn', 'mergeinfo', '--show-revs', + 'eligible', '--log', branch_url, previous]) + log_messages_dict = { + # This is a dictionary mapping revision numbers to their respective + # log messages. The expression in the "key:" part of the dict + # comprehension extracts the revision number, as integer, from the + # 'svn log' output. + int(log_message.splitlines()[0].split()[0][1:]): log_message + # The [1:-1] ignores the empty first and last element of the split(). + for log_message in mergeinfo.split(separator_line)[1:-1] + } + mergeinfo = mergeinfo.splitlines() + + separator_pattern = re.compile('^-{72}$') + revline_pattern = re.compile('^r(\d+) \| [^\|]+ \| [^\|]+ \| \d+ lines?$') + changes_prefix_pattern = re.compile(r'^\[(U|D)?:?([^\]]+)?\](.+)$') + changes_suffix_pattern = re.compile(r'^(.+)\[(U|D)?:?([^\]]+)?\]$') + # TODO: push this into backport.status as a library function + auto_merge_pattern = \ + re.compile(r'^Merge (r\d+,? |the r\d+ group |the \S+ branch:)') + + changes_dict = dict() # audience -> (section -> (change -> set(revision))) + revision = -1 + got_firstline = False + unlabeled_summary = None + changes_ignore = False + audience = None + section = None + message = None + + for line in mergeinfo: + if separator_pattern.match(line): + # New revision section. Reset variables. + # If there's an unlabeled summary from a previous section, and + # include_unlabeled is True, put it into uncategorized_changes. + if include_unlabeled and unlabeled_summary and not changes_ignore: + if auto_merge_pattern.match(unlabeled_summary): + # 1. Parse revision numbers from the first line + merged_revisions = [ + int(x) for x in + re.compile(r'(?<=\br)\d+\b').findall(unlabeled_summary) + ] + # TODO pass each revnum in MERGED_REVISIONS through this + # logic, in order to extract CHANGES_PREFIX_PATTERN + # and CHANGES_SUFFIX_PATTERN lines from the trunk log + # message. + + # 2. Parse the STATUS entry + this_log_message = log_messages_dict[revision] + status_paragraph = this_log_message.split('\n\n')[2] + logsummary = \ + backport.status.StatusEntry(status_paragraph).logsummary + add_to_changes_dict(changes_dict, None, None, + ' '.join(logsummary), revision) + else: + add_to_changes_dict(changes_dict, None, None, + unlabeled_summary, revision) + revision = -1 + got_firstline = False + unlabeled_summary = None + changes_ignore = False + audience = None + section = None + message = None + continue + + revmatch = revline_pattern.match(line) + if revmatch and (revision == -1): + # A revision line: get the revision number + revision = int(revmatch.group(1)) + logging.debug('Changelog processing revision r%d' % revision) + continue + + if line.strip() == '': + # Skip empty / whitespace lines + continue + + if not got_firstline: + got_firstline = True + if (not re.search(r'status|changes|post-release housekeeping|follow-up|^\*', + line, re.IGNORECASE) + and not changes_prefix_pattern.match(line) + and not changes_suffix_pattern.match(line)): + unlabeled_summary = line + + if re.search(r'\[(c:)?(skip|ignore)\]', line, re.IGNORECASE): + changes_ignore = True + + prefix_match = changes_prefix_pattern.match(line) + if prefix_match: + audience = prefix_match.group(1) + section = prefix_match.group(2) + message = prefix_match.group(3) + add_to_changes_dict(changes_dict, audience, section, message, revision) + + suffix_match = changes_suffix_pattern.match(line) + if suffix_match: + message = suffix_match.group(1) + audience = suffix_match.group(2) + section = suffix_match.group(3) + add_to_changes_dict(changes_dict, audience, section, message, revision) + + # Output the sorted changelog entries + # 1) Uncategorized changes + print_section(changes_dict, None, None, None) + print + # 2) User-visible changes + print(' User-visible changes:') + print_section(changes_dict, 'U', None, None) + print_section(changes_dict, 'U', 'general', 'General') + print_section(changes_dict, 'U', 'major', 'Major new features') + print_section(changes_dict, 'U', 'minor', 'Minor new features and improvements') + print_section(changes_dict, 'U', 'client', 'Client-side bugfixes', mandatory=True) + print_section(changes_dict, 'U', 'server', 'Server-side bugfixes', mandatory=True) + print_section(changes_dict, 'U', 'clientserver', 'Client-side and server-side bugfixes') + print_section(changes_dict, 'U', 'other', 'Other tool improvements and bugfixes') + print_section(changes_dict, 'U', 'bindings', 'Bindings bugfixes', mandatory=True) + print + # 3) Developer-visible changes + print(' Developer-visible changes:') + print_section(changes_dict, 'D', None, None) + print_section(changes_dict, 'D', 'general', 'General', mandatory=True) + print_section(changes_dict, 'D', 'api', 'API changes', mandatory=True) + print_section(changes_dict, 'D', 'bindings', 'Bindings') + #---------------------------------------------------------------------- # Main entry point for argument parsing and handling @@ -1191,6 +1466,10 @@ def main(): help='''The directory in which to create needed files and folders. The default is the current working directory.''') + parser.add_argument('--branch', + help='''The branch to base the release on, + as a path relative to ^/subversion/. + Default: 'branches/MAJOR.MINOR.x'.''') subparsers = parser.add_subparsers(title='subcommands') # Setup the parser for the build-env subcommand @@ -1216,9 +1495,6 @@ def main(): help='''The release label, such as '1.7.0-alpha1'.''') subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')), help='''The revision number to base the release on.''') - subparser.add_argument('--branch', - help='''The branch to base the release on, - relative to ^/subversion/.''') subparser.add_argument('--patches', help='''The path to the directory containing patches.''') @@ -1231,6 +1507,10 @@ def main(): subparser.add_argument('--target', help='''The full path to the directory containing release artifacts.''') + subparser.add_argument('--userid', + help='''The (optional) USER-ID specifying the key to be + used for signing, such as '110B1C95' (Key-ID). If + omitted, uses the default key.''') # Setup the parser for the post-candidates subcommand subparser = subparsers.add_parser('post-candidates', @@ -1247,25 +1527,36 @@ def main(): # Setup the parser for the create-tag subcommand subparser = subparsers.add_parser('create-tag', - help='''Create the release tag.''') - subparser.set_defaults(func=create_tag) + help='''Create the release tag and, if not a prerelease + version, bump version numbers on the branch.''') + subparser.set_defaults(func=create_tag_and_bump_versions) + subparser.add_argument('version', type=Version, + help='''The release label, such as '1.7.0-alpha1'.''') + subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')), + help='''The revision number to base the release on.''') + subparser.add_argument('--username', + help='''Username for ''' + svn_repos + '''.''') + subparser.add_argument('--target', + help='''The full path to the directory containing + release artifacts.''') + + # Setup the parser for the bump-versions-on-branch subcommand + subparser = subparsers.add_parser('bump-versions-on-branch', + help='''Bump version numbers on branch.''') + subparser.set_defaults(func=bump_versions_on_branch) subparser.add_argument('version', type=Version, help='''The release label, such as '1.7.0-alpha1'.''') subparser.add_argument('revnum', type=lambda arg: int(arg.lstrip('r')), help='''The revision number to base the release on.''') - subparser.add_argument('--branch', - help='''The branch to base the release on, - relative to ^/subversion/.''') subparser.add_argument('--username', - help='''Username for ''' + secure_repos + '''.''') + help='''Username for ''' + svn_repos + '''.''') subparser.add_argument('--target', help='''The full path to the directory containing release artifacts.''') # The clean-dist subcommand subparser = subparsers.add_parser('clean-dist', - help='''Clean the distribution directory (and mirrors) of - all but the most recent MAJOR.MINOR release.''') + help=clean_dist.__doc__.split('\n\n')[0]) subparser.set_defaults(func=clean_dist) subparser.add_argument('--dist-dir', help='''The directory to clean.''') @@ -1288,6 +1579,11 @@ def main(): help='''Output to stdout template text for use in the news section of the Subversion website.''') subparser.set_defaults(func=write_news) + subparser.add_argument('--announcement-url', + help='''The URL to the archived announcement email.''') + subparser.add_argument('--edit-html-file', + help='''Insert the text into this file + news.html, index.html).''') subparser.add_argument('version', type=Version, help='''The release label, such as '1.7.0-alpha1'.''') @@ -1338,6 +1634,25 @@ def main(): separate subcommand.''') subparser.set_defaults(func=cleanup) + # write-changelog + subparser = subparsers.add_parser('write-changelog', + help='''Output to stdout changelog entries parsed from + commit messages, optionally labeled with a category + like [U:client], [D:api], [U], ...''') + subparser.set_defaults(func=write_changelog) + subparser.add_argument('previous', + help='''The "previous" branch or tag, relative to + ^/subversion/, to compare "branch" against.''') + subparser.add_argument('--include-unlabeled-summaries', + dest='include_unlabeled', + action='store_true', default=False, + help='''Include summary lines that do not have a changes + label, unless an explicit [c:skip] or [c:ignore] + is part of the commit message (except if the + summary line contains 'STATUS', 'CHANGES', + 'Post-release housekeeping', 'Follow-up' or starts + with '*').''') + # Parse the arguments args = parser.parse_args() diff --git a/tools/dist/security/parser.py b/tools/dist/security/parser.py index 2f1b883..2827e94 100644 --- a/tools/dist/security/parser.py +++ b/tools/dist/security/parser.py @@ -50,9 +50,16 @@ class Notification(object): CULPRIT_SERVER = 'server' CULPRIT_CLIENT = 'client' - __CULPRITS = ((CULPRIT_SERVER, CULPRIT_CLIENT, - (CULPRIT_SERVER, CULPRIT_CLIENT), - (CULPRIT_CLIENT, CULPRIT_SERVER))) + # For compatibility, 'client' and 'server' may be specified either with + # or without a tuple. + __CULPRITS = ( + CULPRIT_SERVER, + CULPRIT_CLIENT, + (CULPRIT_SERVER,) + (CULPRIT_CLIENT,) + (CULPRIT_SERVER, CULPRIT_CLIENT), + (CULPRIT_CLIENT, CULPRIT_SERVER), + ) def __init__(self, basedir, tracking_id, title, culprit, advisory, patches): diff --git a/tools/dist/templates/download.ezt b/tools/dist/templates/download.ezt index 4c6fda8..19dac3d 100644 --- a/tools/dist/templates/download.ezt +++ b/tools/dist/templates/download.ezt @@ -2,16 +2,14 @@ <table class="centered"> <tr> <th>File</th> - <th>Checksum (SHA1)</th> <th>Checksum (SHA512)</th> <th>Signatures</th> </tr> [for fileinfo]<tr> <td><a href="[[]preferred]subversion/[fileinfo.filename]">[fileinfo.filename]</a></td> - <td class="checksum">[fileinfo.sha1]</td> <!-- The sha512 line does not have a class="checksum" since the link needn't be rendered in monospace. --> - <td>[<a href="http://www.apache.org/dist/subversion/[fileinfo.filename].sha512">SHA-512</a>]</td> - <td>[<a href="http://www.apache.org/dist/subversion/[fileinfo.filename].asc">PGP</a>]</td> + <td>[<a href="https://www.apache.org/dist/subversion/[fileinfo.filename].sha512">SHA-512</a>]</td> + <td>[<a href="https://www.apache.org/dist/subversion/[fileinfo.filename].asc">PGP</a>]</td> </tr>[end] </table> diff --git a/tools/dist/templates/rc-news.ezt b/tools/dist/templates/rc-news.ezt index a645ffa..04f094d 100644 --- a/tools/dist/templates/rc-news.ezt +++ b/tools/dist/templates/rc-news.ezt @@ -8,10 +8,10 @@ release is not intended for production use, but is provided as a milestone to encourage wider testing and feedback from intrepid users and maintainers. Please see the - <a href="">release + <a href="[announcement_url]">release announcement</a> for more information about this release, and the <a href="/docs/release-notes/[major-minor].html">release notes</a> and - <a href="http://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES"> + <a href="https://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES"> change log</a> for information about what will eventually be in the [version_base] release.</p> diff --git a/tools/dist/templates/rc-release-ann.ezt b/tools/dist/templates/rc-release-ann.ezt index ca5f4d0..a8bb0f5 100644 --- a/tools/dist/templates/rc-release-ann.ezt +++ b/tools/dist/templates/rc-release-ann.ezt @@ -1,16 +1,13 @@ From: ...@apache.org To: announce@subversion.apache.org, users@subversion.apache.org, dev@subversion.apache.org, announce@apache.org +Reply-To: users@subversion.apache.org Subject: [[]ANNOUNCE] Apache Subversion [version] released I'm happy to announce the release of Apache Subversion [version]. Please choose the mirror closest to you by visiting: - http://subversion.apache.org/download.cgi#[anchor] + https://subversion.apache.org/download.cgi#[anchor] -The SHA1 checksums are: - -[for sha1info] [sha1info.sha1] [sha1info.filename] -[end] SHA-512 checksums are available at: https://www.apache.org/dist/subversion/subversion-[version].tar.bz2.sha512 @@ -19,9 +16,9 @@ SHA-512 checksums are available at: PGP Signatures are available at: - http://www.apache.org/dist/subversion/subversion-[version].tar.bz2.asc - http://www.apache.org/dist/subversion/subversion-[version].tar.gz.asc - http://www.apache.org/dist/subversion/subversion-[version].zip.asc + https://www.apache.org/dist/subversion/subversion-[version].tar.bz2.asc + https://www.apache.org/dist/subversion/subversion-[version].tar.gz.asc + https://www.apache.org/dist/subversion/subversion-[version].zip.asc For this release, the following people have provided PGP signatures: @@ -57,13 +54,18 @@ end users please. Release notes for the [major-minor].x release series may be found at: - http://subversion.apache.org/docs/release-notes/[major-minor].html + https://subversion.apache.org/docs/release-notes/[major-minor].html You can find the list of changes between [version] and earlier versions at: - http://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES + https://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES Questions, comments, and bug reports to users@subversion.apache.org. Thanks, - The Subversion Team + +-- +To unsubscribe, please see: + + https://subversion.apache.org/mailing-lists.html#unsubscribing diff --git a/tools/dist/templates/stable-news.ezt b/tools/dist/templates/stable-news.ezt index 8fcaae9..4cd82ac 100644 --- a/tools/dist/templates/stable-news.ezt +++ b/tools/dist/templates/stable-news.ezt @@ -10,10 +10,10 @@ [else] This is the most complete release of the [major-minor].x line to date, and we encourage all users to upgrade as soon as reasonable. [end] Please see the - <a href="" + <a href="[announcement_url]" >release announcement</a> and the - <a href="http://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES" - >change log</a> for more information about this release.</p> + <a href="/docs/release-notes/[major-minor]" + >release notes</a> for more information about this release.</p> <p>To get this release from the nearest mirror, please visit our <a href="/download.cgi#[anchor]">download page</a>.</p> diff --git a/tools/dist/templates/stable-release-ann.ezt b/tools/dist/templates/stable-release-ann.ezt index 2aec041..dea8c88 100644 --- a/tools/dist/templates/stable-release-ann.ezt +++ b/tools/dist/templates/stable-release-ann.ezt @@ -1,5 +1,6 @@ From: ...@apache.org To: announce@subversion.apache.org, users@subversion.apache.org, dev@subversion.apache.org, announce@apache.org +Reply-To: users@subversion.apache.org [if-any security]Cc: security@apache.org, oss-security@lists.openwall.com, bugtraq@securityfocus.com [end][if-any security]Subject: [[]SECURITY][[]ANNOUNCE] Apache Subversion [version] released [else]Subject: [[]ANNOUNCE] Apache Subversion [version] released @@ -7,7 +8,7 @@ To: announce@subversion.apache.org, users@subversion.apache.org, dev@subversion. I'm happy to announce the release of Apache Subversion [version]. Please choose the mirror closest to you by visiting: - http://subversion.apache.org/download.cgi#[anchor] + https://subversion.apache.org/download.cgi#[anchor] [if-any dot-zero] This is a stable feature release of the Apache Subversion open source version control system. @@ -18,10 +19,6 @@ open source version control system. This is a stable bugfix release of the Apache Subversion open source version control system. [end][end] -The SHA1 checksums are: - -[for sha1info] [sha1info.sha1] [sha1info.filename] -[end] SHA-512 checksums are available at: https://www.apache.org/dist/subversion/subversion-[version].tar.bz2.sha512 @@ -30,22 +27,27 @@ SHA-512 checksums are available at: PGP Signatures are available at: - http://www.apache.org/dist/subversion/subversion-[version].tar.bz2.asc - http://www.apache.org/dist/subversion/subversion-[version].tar.gz.asc - http://www.apache.org/dist/subversion/subversion-[version].zip.asc + https://www.apache.org/dist/subversion/subversion-[version].tar.bz2.asc + https://www.apache.org/dist/subversion/subversion-[version].tar.gz.asc + https://www.apache.org/dist/subversion/subversion-[version].zip.asc For this release, the following people have provided PGP signatures: [siginfo] Release notes for the [major-minor].x release series may be found at: - http://subversion.apache.org/docs/release-notes/[major-minor].html + https://subversion.apache.org/docs/release-notes/[major-minor].html You can find the list of changes between [version] and earlier versions at: - http://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES + https://svn.apache.org/repos/asf/subversion/tags/[version]/CHANGES Questions, comments, and bug reports to users@subversion.apache.org. Thanks, - The Subversion Team + +-- +To unsubscribe, please see: + + https://subversion.apache.org/mailing-lists.html#unsubscribing |