|  | #!/bin/bash | 
|  |  | 
|  | # Copyright 2015 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # Licensed 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 -eu | 
|  |  | 
|  | # Generate the release notes from the git history. | 
|  |  | 
|  | RELNOTES_SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) | 
|  | source ${RELNOTES_SCRIPT_DIR}/common.sh | 
|  |  | 
|  | # It uses the RELNOTES tag in the history to knows the important changes to | 
|  | # report: | 
|  | #   RELNOTES: indicates a change important the user. | 
|  | #   RELNOTES[NEW]: introduces a new feature. | 
|  | #   RELNOTES[INC]: indicates an incompatible change. | 
|  | # The previous releases base is detected using the CHANGELOG file from the | 
|  | # repository. | 
|  | RELNOTES_TYPES=("INC" "NEW" "") | 
|  | RELNOTES_DESC=("Incompatible changes" "New features" "Important changes") | 
|  |  | 
|  | # Get the baseline version and cherry-picks of the previous release | 
|  | #  Parameter: $1 is the path to the changelog file | 
|  | #  Output: "${BASELINE} ${CHERRYPICKS}" | 
|  | #    BASELINE is the hash of the baseline commit of the latest release | 
|  | #    CHERRYPICKS is the list of hash of cherry-picked commits of the latest release | 
|  | #  return 1 if there is no initial release | 
|  | function __get_last_release() { | 
|  | local changelog=$1 | 
|  | [ -f "$changelog" ] || return 1  # No changelog = initial release | 
|  | local BASELINE_LINE=$(grep -m 1 -n '^Baseline: ' "$changelog") || return 1 | 
|  | [ -n "${BASELINE_LINE}" ] || return 1  # No baseline = initial release | 
|  | local BASELINE_LINENB=$(echo "${BASELINE_LINE}" | cut -d ":" -f 1) | 
|  | BASELINE=$(echo "${BASELINE_LINE}" | cut -d " " -f 2) | 
|  | local CHERRYPICK_LINE=$(($BASELINE_LINENB + 3)) | 
|  | # grep -B999 looks for all lines before the empty line and after that we | 
|  | # restrict to only lines with the cherry picked hash then finally we cut | 
|  | # the hash. | 
|  | local CHERRY_PICKS=$(tail -n +${CHERRYPICK_LINE} "$changelog" \ | 
|  | | grep -m 1 "^$" -B999 \ | 
|  | | grep -E '^   \+ [a-z0-9]+:' \ | 
|  | | cut -d ":" -f 1 | cut -d "+" -f 2) | 
|  | echo $BASELINE $CHERRY_PICKS | 
|  | return 0 | 
|  | } | 
|  |  | 
|  | # Now get the list of commit with a RELNOTES since latest release baseline ($1) | 
|  | # discarding cherry_picks ($2..) and rollbacks. The returned list of commits is | 
|  | # from the oldest to the newest | 
|  | function __get_release_notes_commits() { | 
|  | local baseline=$1 | 
|  | shift | 
|  | local cherry_picks="$@" | 
|  | local rollback_commits=$(git log --oneline -E --grep='^Rollback of commit [a-z0-9]+.$' ${baseline}.. \ | 
|  | | grep -E '^[a-z0-9]+ Rollback of commit [a-z0-9]+.$' || true) | 
|  | local rollback_hashes=$(echo "$rollback_commits" | cut -d " " -f 1) | 
|  | local rolledback_hashes=$(echo "$rollback_commits" | cut -d " " -f 5 | sed -E 's/^(.......).*$/\1/') | 
|  | local exclude_hashes=$(echo DUMMY $cherry_picks $rollback_hashes $rolledback_hashes | xargs echo | sed 's/ /|/g') | 
|  | git log --reverse --pretty=format:%H ${baseline}.. -E --grep='^RELNOTES(\[[^\]+\])?:' \ | 
|  | | grep -Ev "^(${exclude_hashes})" || true | 
|  | } | 
|  |  | 
|  | # Extract the release note from a commit hash ($1). It extracts | 
|  | # the RELNOTES([??]): lines. A new empty line ends the relnotes tag. | 
|  | # It adds the relnotes, if not "None" ("None.") or "n/a" ("n/a.") to | 
|  | # the correct array: | 
|  | #   RELNOTES_INC for incompatible changes | 
|  | #   RELNOTES_NEW for new features changes | 
|  | #   RELNOTES for other changes | 
|  | function __extract_release_note() { | 
|  | local find_relnote_awk_script=" | 
|  | BEGIN { in_relnote = 0 } | 
|  | /^$/ { in_relnote = 0 } | 
|  | /^PiperOrigin-RevId:.*$/ { in_relnote = 0 } | 
|  | /^RELNOTES(\[[^\]]+\])?:/ { in_relnote = 1 } | 
|  | { if (in_relnote) { print } }" | 
|  | local relnote="$(git show -s $1 --pretty=format:%B | awk "${find_relnote_awk_script}")" | 
|  | local regex="^RELNOTES(\[([a-zA-Z]*)\])?:[[:space:]]*([^[:space:]].*[^[:space:]])[[:space:]]*$" | 
|  | if [[ "$relnote" =~ $regex ]]; then | 
|  | local relnote_kind=${BASH_REMATCH[2]} | 
|  | local relnote_text="${BASH_REMATCH[3]}" | 
|  | if [[ ! "$(echo $relnote_text | awk '{print tolower($0)}')" =~ ^((none|n/a|no)(\.( .*)?)?|\.)$ ]]; then | 
|  | eval "RELNOTES_${relnote_kind}+=(\"\${relnote_text}\")" | 
|  | fi | 
|  | fi | 
|  | } | 
|  |  | 
|  | # Build release notes arrays from a list of commits ($@) and return the release | 
|  | # note in an array of array. | 
|  | function __generate_release_notes() { | 
|  | for i in "${RELNOTES_TYPES[@]}"; do | 
|  | eval "RELNOTES_${i}=()" | 
|  | done | 
|  | for commit in $@; do | 
|  | __extract_release_note "${commit}" | 
|  | done | 
|  | } | 
|  |  | 
|  | # Returns the list of release notes in arguments into a list of points in | 
|  | # a markdown list. The release notes are wrapped to 70 characters so it | 
|  | # displays nicely in a git history. | 
|  | function __format_release_notes() { | 
|  | local i | 
|  | for (( i=1; $i <= $#; i=$i+1 )); do | 
|  | local relnote="${!i}" | 
|  | local lines=$(echo "$relnote" | wrap_text 66)  # wrap to 70 counting the 4 leading spaces. | 
|  | echo "  - $lines" | head -1 | 
|  | echo "$lines" | tail -n +2 | sed 's/^/    /' | 
|  | done | 
|  | } | 
|  |  | 
|  | # Create the release notes since commit $1 ($2...${[#]} are the cherry-picks, | 
|  | # so the commits to ignore. | 
|  | function __release_notes() { | 
|  | local last_release=$1 | 
|  | local i | 
|  | local commits=$(__get_release_notes_commits $last_release) | 
|  | local length="${#RELNOTES_TYPES[@]}" | 
|  | __generate_release_notes "$commits" | 
|  | for (( i=0; $i < $length; i=$i+1 )); do | 
|  | local relnotes_title="${RELNOTES_DESC[$i]}" | 
|  | local relnotes_type=${RELNOTES_TYPES[$i]} | 
|  | local relnotes="RELNOTES_${relnotes_type}[@]" | 
|  | local nb_relnotes=$(eval "echo \${#$relnotes}") | 
|  | if (( "${nb_relnotes}" > 0 )); then | 
|  | echo "${relnotes_title}:" | 
|  | echo | 
|  | __format_release_notes "${!relnotes}" | 
|  | echo | 
|  | fi | 
|  | done | 
|  |  | 
|  | # Add a list of contributors to thank. | 
|  | # Stages: | 
|  | #   1. Get the list of authors from the last release til now, both name and | 
|  | #     email. | 
|  | #   2. Sort and uniqify. | 
|  | #   3. Remove googlers. (This is why the email is needed) | 
|  | #   4. Cut the email address, leaving only the name. | 
|  | #   5-n. Remove trailing spaces and newlines, substituting with a comman and a | 
|  | #     space, removing any trailing spaces again. | 
|  | local external_authors=$(git log $last_release..HEAD --format="%aN <%aE>" \ | 
|  | | sort \ | 
|  | | uniq \ | 
|  | | grep -v "google.com" \ | 
|  | | cut -d'<' -f 1 \ | 
|  | | sed -e 's/[[:space:]]$//' \ | 
|  | | tr '\n' ',' \ | 
|  | | sed -e 's/,$/\n/' \ | 
|  | | sed -e 's/,/, /g') | 
|  | echo "This release contains contributions from many people at Google, as well as ${external_authors}." | 
|  | } | 
|  |  | 
|  | # A wrapper around all the previous function, using the CHANGELOG.md | 
|  | # file in $1 to compute the last release commit hash. | 
|  | function create_release_notes() { | 
|  | local last_release=$(__get_last_release "$1") || \ | 
|  | { echo "Initial release."; return 0; } | 
|  | [ -n "${last_release}" ] || { echo "Initial release."; return 0; } | 
|  | __release_notes ${last_release} | 
|  | } | 
|  |  | 
|  | # Trim empty lines at the beginning and the end of the buffer. | 
|  | function __trim_empty_lines() { | 
|  | # Replace all new line by a linefeed, then using sed, remove the leading | 
|  | # and trailing linefeeds and convert them back to newline | 
|  | tr '\n' '\f' | sed -e "s/^\f*//" -e "s/\f*$//" | tr '\f' '\n' | 
|  | } | 
|  |  | 
|  | # Launch the editor and return the edited release notes. | 
|  | function __release_note_processor() { | 
|  | local tmpfile="$1" | 
|  |  | 
|  | # Strip the release notes. | 
|  | local relnotes="$(cat ${tmpfile} | grep -v '^#' | __trim_empty_lines)" | 
|  | if [ -z "${relnotes}" ]; then | 
|  | echo "Release notes are empty, cancelling release creation..." >&2 | 
|  | return 1 | 
|  | fi | 
|  |  | 
|  | echo "${relnotes}" > "${tmpfile}" | 
|  | } | 
|  |  | 
|  | # Create the revision information given a list of commits. The first | 
|  | # commit should be the baseline, and the other ones are the cherry-picks. | 
|  | # The result is of the form: | 
|  | # Baseline: BASELINE_COMMIT | 
|  | # | 
|  | # Cherry picks: | 
|  | # | 
|  | #    + CHERRY_PICK1: commit message summary of the CHERRY_PICK1. This | 
|  | #                    message will be wrapped into 70 columns. | 
|  | #    + CHERRY_PICK2: commit message summary of the CHERRY_PICK2. | 
|  | function __create_revision_information() { | 
|  | echo "Baseline: $(__git_commit_hash "${1}")" | 
|  | local first=1 | 
|  | shift | 
|  | while [ -n "${1-}" ]; do | 
|  | if [[ "$first" -eq 1 ]]; then | 
|  | echo -e "\nCherry picks:" | 
|  | echo | 
|  | first=0 | 
|  | fi | 
|  | local hash="$(__git_commit_hash "${1}")" | 
|  | local subject="$(__git_commit_subject $hash)" | 
|  | local lines=$(echo "$subject" | wrap_text 65)  # 5 leading spaces. | 
|  | echo "   + $hash:" | 
|  | echo "$lines" | sed 's/^/     /' | 
|  | shift | 
|  | done | 
|  | } | 
|  |  | 
|  | # Get the baseline of master. | 
|  | # Args: $1: release branch (or HEAD) | 
|  | # TODO(philwo) this gives the wrong baseline when HEAD == release == master. | 
|  | function get_release_baseline() { | 
|  | git merge-base master "$1" | 
|  | } | 
|  |  | 
|  | # Get the list of cherry-picks since master | 
|  | # Args: | 
|  | #   $1: branch, default to HEAD | 
|  | #   $2: baseline change, default to $(get_release_baseline $1) | 
|  | function get_cherrypicks() { | 
|  | local branch="${1:-HEAD}" | 
|  | local baseline="${2:-$(get_release_baseline "${branch}")}" | 
|  | # List of changes since the baseline on the release branch | 
|  | local changes="$(git_log_hash "${baseline}" "${branch}" --reverse)" | 
|  | # List of changes since the baseline on the master branch, and their patch-id | 
|  | local master_changes="$(git_log_hash "${baseline}" master | xargs git show | git patch-id)" | 
|  | # Now for each changes on the release branch | 
|  | for i in ${changes}; do | 
|  | # Find the change with the same patch-id on the master branch if the note is not present | 
|  | hash=$(echo "${master_changes}" \ | 
|  | | grep "^$(git show "$i" | git patch-id | cut -d " " -f 1)" \ | 
|  | | cut -d " " -f 2) | 
|  | if [ -z "${hash}" ]; then | 
|  | # We don't know which cherry-pick it is coming from, fall back to the new commit hash. | 
|  | echo "$i" | 
|  | else | 
|  | echo "${hash}" | 
|  | fi | 
|  | done | 
|  | } | 
|  |  | 
|  | # Generate the title of the release with the date from the release name ($1). | 
|  | function get_release_title() { | 
|  | echo "Release ${1} ($(date +%Y-%m-%d))" | 
|  | } | 
|  |  | 
|  | # Generate the release message to be added to the changelog | 
|  | # from the release notes for release $1 | 
|  | # Args: | 
|  | #   $1: release name | 
|  | #   $2: release ref (default HEAD) | 
|  | #   $3: delimiter around the revision information (default none) | 
|  | function generate_release_message() { | 
|  | local release_name="$1" | 
|  | local branch="${2:-HEAD}" | 
|  | local delimiter="${3-}" | 
|  | local baseline="$(get_release_baseline "${branch}")" | 
|  | local cherrypicks="$(get_cherrypicks "${branch}" "${baseline}")" | 
|  |  | 
|  | get_release_title "$release_name" | 
|  | echo | 
|  |  | 
|  | if [ -n "${delimiter}" ]; then | 
|  | echo "${delimiter}" | 
|  | fi | 
|  | __create_revision_information $baseline $cherrypicks | 
|  | if [ -n "${delimiter}" ]; then | 
|  | echo "${delimiter}" | 
|  | fi | 
|  |  | 
|  | echo | 
|  |  | 
|  | # Generate the release notes | 
|  | local tmpfile=$(mktemp --tmpdir relnotes-XXXXXXXX) | 
|  | trap "rm -f ${tmpfile}" EXIT | 
|  |  | 
|  | # Save the changelog so we compute the relnotes against HEAD. | 
|  | git show master:CHANGELOG.md > "${tmpfile}" | 
|  |  | 
|  | local relnotes="$(create_release_notes "${tmpfile}" "${baseline}" ${cherrypicks})" | 
|  | echo "${relnotes}" > "${tmpfile}" | 
|  |  | 
|  | __release_note_processor "${tmpfile}" || return 1 | 
|  | relnotes="$(cat ${tmpfile})" | 
|  |  | 
|  | cat "${tmpfile}" | 
|  | } | 
|  |  | 
|  | # Returns the release notes for the CHANGELOG.md taken from either from | 
|  | # the notes for a release candidate or from the commit message for a | 
|  | # full release. | 
|  | function get_full_release_notes() { | 
|  | local release_name="$(get_full_release_name "$@")" | 
|  |  | 
|  | if [[ "${release_name}" =~ rc[0-9]+$ ]]; then | 
|  | # Release candidate, we need to generate from the notes | 
|  | generate_release_message "${release_name}" "$@" | 
|  | else | 
|  | # Full release, returns the commit message | 
|  | git_commit_msg "$@" | 
|  | fi | 
|  | } | 
|  |  |