blob: b0182f2fb5f0c6552e98878e6c98a1bb8d56b946 [file] [log] [blame]
Philipp Wollermanna5afe952016-06-21 14:58:09 +00001#!/bin/bash
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +00002
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00003# Copyright 2015 The Bazel Authors. All rights reserved.
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +00004#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
Philipp Wollermanna5afe952016-06-21 14:58:09 +000017set -eu
18
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000019# Generate the release notes from the git history.
20
Yun Peng123f2b92020-02-12 04:34:00 -080021RELNOTES_SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
22source ${RELNOTES_SCRIPT_DIR}/common.sh
Philipp Wollermann5fabb432018-03-27 04:37:23 -070023
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000024# It uses the RELNOTES tag in the history to knows the important changes to
25# report:
26# RELNOTES: indicates a change important the user.
27# RELNOTES[NEW]: introduces a new feature.
28# RELNOTES[INC]: indicates an incompatible change.
29# The previous releases base is detected using the CHANGELOG file from the
30# repository.
31RELNOTES_TYPES=("INC" "NEW" "")
32RELNOTES_DESC=("Incompatible changes" "New features" "Important changes")
33
34# Get the baseline version and cherry-picks of the previous release
35# Parameter: $1 is the path to the changelog file
36# Output: "${BASELINE} ${CHERRYPICKS}"
37# BASELINE is the hash of the baseline commit of the latest release
38# CHERRYPICKS is the list of hash of cherry-picked commits of the latest release
39# return 1 if there is no initial release
Philipp Wollermann5fabb432018-03-27 04:37:23 -070040function __get_last_release() {
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000041 local changelog=$1
42 [ -f "$changelog" ] || return 1 # No changelog = initial release
43 local BASELINE_LINE=$(grep -m 1 -n '^Baseline: ' "$changelog") || return 1
44 [ -n "${BASELINE_LINE}" ] || return 1 # No baseline = initial release
45 local BASELINE_LINENB=$(echo "${BASELINE_LINE}" | cut -d ":" -f 1)
46 BASELINE=$(echo "${BASELINE_LINE}" | cut -d " " -f 2)
Kristina Chodorowe7be8392016-04-22 20:34:37 +000047 local CHERRYPICK_LINE=$(($BASELINE_LINENB + 3))
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000048 # grep -B999 looks for all lines before the empty line and after that we
49 # restrict to only lines with the cherry picked hash then finally we cut
50 # the hash.
51 local CHERRY_PICKS=$(tail -n +${CHERRYPICK_LINE} "$changelog" \
52 | grep -m 1 "^$" -B999 \
53 | grep -E '^ \+ [a-z0-9]+:' \
54 | cut -d ":" -f 1 | cut -d "+" -f 2)
55 echo $BASELINE $CHERRY_PICKS
56 return 0
57}
58
59# Now get the list of commit with a RELNOTES since latest release baseline ($1)
60# discarding cherry_picks ($2..) and rollbacks. The returned list of commits is
61# from the oldest to the newest
Philipp Wollermann5fabb432018-03-27 04:37:23 -070062function __get_release_notes_commits() {
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000063 local baseline=$1
64 shift
65 local cherry_picks="$@"
66 local rollback_commits=$(git log --oneline -E --grep='^Rollback of commit [a-z0-9]+.$' ${baseline}.. \
Damien Martin-Guillerezb62f7172017-10-16 15:19:27 +020067 | grep -E '^[a-z0-9]+ Rollback of commit [a-z0-9]+.$' || true)
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000068 local rollback_hashes=$(echo "$rollback_commits" | cut -d " " -f 1)
69 local rolledback_hashes=$(echo "$rollback_commits" | cut -d " " -f 5 | sed -E 's/^(.......).*$/\1/')
Damien Martin-Guillereza97f9142017-10-16 16:20:20 +020070 local exclude_hashes=$(echo DUMMY $cherry_picks $rollback_hashes $rolledback_hashes | xargs echo | sed 's/ /|/g')
Damien Martin-Guillerezdbdaf832017-01-26 14:41:04 +000071 git log --reverse --pretty=format:%H ${baseline}.. -E --grep='^RELNOTES(\[[^\]+\])?:' \
Damien Martin-Guillerezb62f7172017-10-16 15:19:27 +020072 | grep -Ev "^(${exclude_hashes})" || true
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000073}
74
75# Extract the release note from a commit hash ($1). It extracts
76# the RELNOTES([??]): lines. A new empty line ends the relnotes tag.
77# It adds the relnotes, if not "None" ("None.") or "n/a" ("n/a.") to
78# the correct array:
79# RELNOTES_INC for incompatible changes
80# RELNOTES_NEW for new features changes
81# RELNOTES for other changes
Philipp Wollermann5fabb432018-03-27 04:37:23 -070082function __extract_release_note() {
Marcel Hlopkodb136252017-05-04 16:04:23 +020083 local find_relnote_awk_script="
84 BEGIN { in_relnote = 0 }
85 /^$/ { in_relnote = 0 }
86 /^PiperOrigin-RevId:.*$/ { in_relnote = 0 }
87 /^RELNOTES(\[[^\]]+\])?:/ { in_relnote = 1 }
88 { if (in_relnote) { print } }"
89 local relnote="$(git show -s $1 --pretty=format:%B | awk "${find_relnote_awk_script}")"
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000090 local regex="^RELNOTES(\[([a-zA-Z]*)\])?:[[:space:]]*([^[:space:]].*[^[:space:]])[[:space:]]*$"
91 if [[ "$relnote" =~ $regex ]]; then
92 local relnote_kind=${BASH_REMATCH[2]}
93 local relnote_text="${BASH_REMATCH[3]}"
Damien Martin-Guillerez445a8792017-10-17 14:06:58 +020094 if [[ ! "$(echo $relnote_text | awk '{print tolower($0)}')" =~ ^((none|n/a|no)(\.( .*)?)?|\.)$ ]]; then
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +000095 eval "RELNOTES_${relnote_kind}+=(\"\${relnote_text}\")"
96 fi
97 fi
98}
99
100# Build release notes arrays from a list of commits ($@) and return the release
101# note in an array of array.
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700102function __generate_release_notes() {
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000103 for i in "${RELNOTES_TYPES[@]}"; do
104 eval "RELNOTES_${i}=()"
105 done
Androbin9c78a792017-11-29 01:31:47 -0800106 for commit in $@; do
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700107 __extract_release_note "${commit}"
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000108 done
109}
110
111# Returns the list of release notes in arguments into a list of points in
112# a markdown list. The release notes are wrapped to 70 characters so it
113# displays nicely in a git history.
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700114function __format_release_notes() {
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000115 local i
116 for (( i=1; $i <= $#; i=$i+1 )); do
117 local relnote="${!i}"
Damien Martin-Guillerez3b61b2c2015-07-27 10:05:43 +0000118 local lines=$(echo "$relnote" | wrap_text 66) # wrap to 70 counting the 4 leading spaces.
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000119 echo " - $lines" | head -1
120 echo "$lines" | tail -n +2 | sed 's/^/ /'
121 done
122}
123
124# Create the release notes since commit $1 ($2...${[#]} are the cherry-picks,
125# so the commits to ignore.
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700126function __release_notes() {
John Cater9225a132018-10-31 10:38:42 -0700127 local last_release=$1
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000128 local i
John Cater9225a132018-10-31 10:38:42 -0700129 local commits=$(__get_release_notes_commits $last_release)
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000130 local length="${#RELNOTES_TYPES[@]}"
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700131 __generate_release_notes "$commits"
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000132 for (( i=0; $i < $length; i=$i+1 )); do
133 local relnotes_title="${RELNOTES_DESC[$i]}"
134 local relnotes_type=${RELNOTES_TYPES[$i]}
135 local relnotes="RELNOTES_${relnotes_type}[@]"
136 local nb_relnotes=$(eval "echo \${#$relnotes}")
137 if (( "${nb_relnotes}" > 0 )); then
138 echo "${relnotes_title}:"
139 echo
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700140 __format_release_notes "${!relnotes}"
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000141 echo
142 fi
143 done
John Cater9225a132018-10-31 10:38:42 -0700144
145 # Add a list of contributors to thank.
John Caterf3fa8762018-11-26 11:55:43 -0800146 # Stages:
147 # 1. Get the list of authors from the last release til now, both name and
148 # email.
149 # 2. Sort and uniqify.
150 # 3. Remove googlers. (This is why the email is needed)
151 # 4. Cut the email address, leaving only the name.
152 # 5-n. Remove trailing spaces and newlines, substituting with a comman and a
153 # space, removing any trailing spaces again.
John Cater9225a132018-10-31 10:38:42 -0700154 local external_authors=$(git log $last_release..HEAD --format="%aN <%aE>" \
155 | sort \
156 | uniq \
157 | grep -v "google.com" \
John Caterf3fa8762018-11-26 11:55:43 -0800158 | cut -d'<' -f 1 \
John Cater9225a132018-10-31 10:38:42 -0700159 | sed -e 's/[[:space:]]$//' \
160 | tr '\n' ',' \
161 | sed -e 's/,$/\n/' \
162 | sed -e 's/,/, /g')
163 echo "This release contains contributions from many people at Google, as well as ${external_authors}."
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000164}
165
166# A wrapper around all the previous function, using the CHANGELOG.md
167# file in $1 to compute the last release commit hash.
168function create_release_notes() {
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700169 local last_release=$(__get_last_release "$1") || \
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000170 { echo "Initial release."; return 0; }
171 [ -n "${last_release}" ] || { echo "Initial release."; return 0; }
Philipp Wollermann5fabb432018-03-27 04:37:23 -0700172 __release_notes ${last_release}
Damien Martin-Guillerezd019eea2015-07-24 12:40:48 +0000173}
Yun Peng123f2b92020-02-12 04:34:00 -0800174
175# Trim empty lines at the beginning and the end of the buffer.
176function __trim_empty_lines() {
177 # Replace all new line by a linefeed, then using sed, remove the leading
178 # and trailing linefeeds and convert them back to newline
179 tr '\n' '\f' | sed -e "s/^\f*//" -e "s/\f*$//" | tr '\f' '\n'
180}
181
182# Launch the editor and return the edited release notes.
183function __release_note_processor() {
184 local tmpfile="$1"
185
186 # Strip the release notes.
187 local relnotes="$(cat ${tmpfile} | grep -v '^#' | __trim_empty_lines)"
188 if [ -z "${relnotes}" ]; then
189 echo "Release notes are empty, cancelling release creation..." >&2
190 return 1
191 fi
192
193 echo "${relnotes}" > "${tmpfile}"
194}
195
196# Create the revision information given a list of commits. The first
197# commit should be the baseline, and the other ones are the cherry-picks.
198# The result is of the form:
199# Baseline: BASELINE_COMMIT
200#
201# Cherry picks:
202#
203# + CHERRY_PICK1: commit message summary of the CHERRY_PICK1. This
204# message will be wrapped into 70 columns.
205# + CHERRY_PICK2: commit message summary of the CHERRY_PICK2.
206function __create_revision_information() {
207 echo "Baseline: $(__git_commit_hash "${1}")"
208 local first=1
209 shift
210 while [ -n "${1-}" ]; do
211 if [[ "$first" -eq 1 ]]; then
212 echo -e "\nCherry picks:"
213 echo
214 first=0
215 fi
216 local hash="$(__git_commit_hash "${1}")"
217 local subject="$(__git_commit_subject $hash)"
218 local lines=$(echo "$subject" | wrap_text 65) # 5 leading spaces.
219 echo " + $hash:"
220 echo "$lines" | sed 's/^/ /'
221 shift
222 done
223}
224
225# Get the baseline of master.
226# Args: $1: release branch (or HEAD)
227# TODO(philwo) this gives the wrong baseline when HEAD == release == master.
228function get_release_baseline() {
229 git merge-base master "$1"
230}
231
232# Get the list of cherry-picks since master
233# Args:
234# $1: branch, default to HEAD
235# $2: baseline change, default to $(get_release_baseline $1)
236function get_cherrypicks() {
237 local branch="${1:-HEAD}"
238 local baseline="${2:-$(get_release_baseline "${branch}")}"
239 # List of changes since the baseline on the release branch
240 local changes="$(git_log_hash "${baseline}" "${branch}" --reverse)"
241 # List of changes since the baseline on the master branch, and their patch-id
242 local master_changes="$(git_log_hash "${baseline}" master | xargs git show | git patch-id)"
243 # Now for each changes on the release branch
244 for i in ${changes}; do
245 # Find the change with the same patch-id on the master branch if the note is not present
246 hash=$(echo "${master_changes}" \
247 | grep "^$(git show "$i" | git patch-id | cut -d " " -f 1)" \
248 | cut -d " " -f 2)
249 if [ -z "${hash}" ]; then
250 # We don't know which cherry-pick it is coming from, fall back to the new commit hash.
251 echo "$i"
252 else
253 echo "${hash}"
254 fi
255 done
256}
257
258# Generate the title of the release with the date from the release name ($1).
259function get_release_title() {
260 echo "Release ${1} ($(date +%Y-%m-%d))"
261}
262
263# Generate the release message to be added to the changelog
264# from the release notes for release $1
265# Args:
266# $1: release name
267# $2: release ref (default HEAD)
268# $3: delimiter around the revision information (default none)
269function generate_release_message() {
270 local release_name="$1"
271 local branch="${2:-HEAD}"
272 local delimiter="${3-}"
273 local baseline="$(get_release_baseline "${branch}")"
274 local cherrypicks="$(get_cherrypicks "${branch}" "${baseline}")"
275
276 get_release_title "$release_name"
277 echo
278
279 if [ -n "${delimiter}" ]; then
280 echo "${delimiter}"
281 fi
282 __create_revision_information $baseline $cherrypicks
283 if [ -n "${delimiter}" ]; then
284 echo "${delimiter}"
285 fi
286
287 echo
288
289 # Generate the release notes
290 local tmpfile=$(mktemp --tmpdir relnotes-XXXXXXXX)
291 trap "rm -f ${tmpfile}" EXIT
292
293 # Save the changelog so we compute the relnotes against HEAD.
294 git show master:CHANGELOG.md > "${tmpfile}"
295
296 local relnotes="$(create_release_notes "${tmpfile}" "${baseline}" ${cherrypicks})"
297 echo "${relnotes}" > "${tmpfile}"
298
299 __release_note_processor "${tmpfile}" || return 1
300 relnotes="$(cat ${tmpfile})"
301
302 cat "${tmpfile}"
303}
304
305# Returns the release notes for the CHANGELOG.md taken from either from
306# the notes for a release candidate or from the commit message for a
307# full release.
308function get_full_release_notes() {
309 local release_name="$(get_full_release_name "$@")"
310
311 if [[ "${release_name}" =~ rc[0-9]+$ ]]; then
312 # Release candidate, we need to generate from the notes
313 generate_release_message "${release_name}" "$@"
314 else
315 # Full release, returns the commit message
316 git_commit_msg "$@"
317 fi
318}
319