Skip to content

Commit

Permalink
Merge pull request #2257 from hoffie/changelog-helper
Browse files Browse the repository at this point in the history
tools: Add ChangeLog helper script
  • Loading branch information
ann0see authored Feb 26, 2022
2 parents 6117258 + 7593156 commit b37732e
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<!-- Thank you for working on Jamulus and opening a Pull Request! Please fill the following to make the review process straight forward -->

**Short description of changes**
<!-- A short description of your changes which might go into the change log -->
<!-- Explain what your PR does -->

CHANGELOG: <!-- Insert a short, end-user understandable sentence in past tense right here, e.g.: Client: Fixed crash when clicking the connect button too fast -->

**Context: Fixes an issue?**
<!-- If this fixes an issue, please write Fixes: <issue number here>; if not, please give your PR a context. -->
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/update-copyright-notices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ jobs:
gh pr comment "${existing_pr}" --body "PR has been updated by tools/update-copyright-notices.sh."
else
body=$'This automated Pull Request updates the copyright notices throughout the source.\n\n'
body="${body}This PR is the result of a tools/update-copyright-notices.sh run."
body="${body}This PR is the result of a tools/update-copyright-notices.sh run."$'\n\n'
body="${body}CHANGELOG: SKIP"
gh pr create --base master --head "${pr_branch}" --title "Update copyright notice(s) for $(date +%Y)" --body "${body}"
fi
Expand Down
283 changes: 283 additions & 0 deletions tools/changelog-helper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
#!/bin/bash
# Requirements: git, Github CLI (gh), jq
set -eu

echo "This tool checks the ChangeLog file and compares its entries for the top-most"
echo "release against the associated Github milestone and the git log."
echo "It will mention any PRs which are not listed in the ChangeLog."
echo "It can optionally pre-fill the ChangeLog with all missing entries"
echo "or sort existing entries. See --help."
echo

# Ensure that we list upstream PRs and not those from the fork:
export GH_REPO=jamulussoftware/jamulus

PR_LIST_LIMIT=300
TRANSLATION_ENTRY_TEXT="GUI: Translations have been updated:"
declare -A LANGS=(
[de_DE]="German"
[fr_FR]="French"
[it_IT]="Italian"
[nl_NL]="Dutch"
[pl_PL]="Polish"
[pt_BR]="Portuguese Brazilian"
[pt_PT]="Portuguese European"
[sk_SK]="Slovak"
[es_ES]="Spanish"
[sv_SE]="Swedish"
[zh_CN]="Simplified Chinese"
)

find_or_add_missing_entries() {
local changelog=$(sed -rne '/^###.*'"${target_release//./\.}"'\b/,/^### '"${prev_release//./\.}"'\b/p' ChangeLog)
local changelog_begin_position=$(grep -nP '^### .*\d+\.\d+\.\d+\b' ChangeLog | head -n1 | cut -d: -f1)

echo "Checking if all merged Github PRs since ${prev_release} are included for ${target_release}..."
for id in $(gh pr list --limit "${PR_LIST_LIMIT}" --search 'milestone:"Release '"${target_release}"'"' --state merged | awk '{print $1}'); do
check_or_add_pr "$id"
done

target_ref=origin/master
if git tag | grep -qxF "${target_release_tag}"; then
# already released, use this
target_ref="${target_release_tag}"
fi
echo
echo "Checking if all PR or references in git log since ${prev_release_tag} are included for ${target_release} based on ref ${target_ref}..."
for id in $(git log "${prev_release_tag}..master" | grep -oP '#\K(\d+)'); do
gh pr view "${id}" --json title &>/dev/null || continue # Skip non-PRs
check_or_add_pr "${id}"
done

echo
echo "Done."
if [[ "${ACTION}" == "find-missing-entries" ]]; then
echo "You can re-run this script with add-missing-entries to fill the ChangeLog automatically."
fi
}

group_entries() {
local changelog_begin_position=$(grep -nP '^### .*\d+\.\d+\.\d+\b' ChangeLog | head -n1 | cut -d: -f1)
local changelog_prev_release_position=$(grep -nP '^### .*\d+\.\d+\.\d+\b' ChangeLog | head -n2 | tail -n1 | cut -d: -f1)

# Save everything before the actual release changelog content:
local changelog_header=$(head -n "${changelog_begin_position}" ChangeLog)

# Save everything after the actual release changelog content:
local changelog_prev_releases=$(tail -n "+${changelog_prev_release_position}" ChangeLog)

# Save the current release's changelog content:
local changelog=$(sed -rne '/^###.*'"${target_release//./\.}"'\b/,/^### '"${prev_release//./\.}"'\b/p' ChangeLog | tail -n +2 | head -n -1)

# Remove trailing whitespace on all lines of the current changelog:
changelog=$(sed -re 's/\s+$//' <<<"$changelog")

# Prepend a number to known categories in order to make their sorting position consistent:
category_order=(
"Client"
"GUI"
"$TRANSLATION_ENTRY_TEXT"
"Server"
"Recorder"
"Performance"
"CLI"
"Bug Fix"
"Windows"
"Installer"
"Linux"
"Mac"
"Android"
"iOS"
"Translation"
"Doc"
"Website"
"Github"
"Build"
"Autobuild"
"Code"
"Internal"
)
local index=0
for category in "${category_order[@]}"; do
changelog=$(sed -re 's/^(- '"${category}"')/'"${index}"' \1/' <<<"${changelog}")
index=$(($index+1))
done

# Reduce blocks ("entries") to a single line by replacing \n with \v.
# `sort` then works on those reduced lines and sorts them by the category (e.g. Server:)
# Afterwards, convert \v to \n again:
changelog=$(
sed -r ':r;/(^|\n)$/!{$!{N;br}};s/\n/\v/g' <<<"$changelog" |
LC_ALL=C sort --stable --numeric-sort --field-separator=':' -k1,1 |
sed 's/\v/\n/g'
)

# Remove temporary sorting indices at line start again:
changelog=$(sed -re 's/^[0-9]+ (- )/\1/' <<<"$changelog")

# Rebuild the changelog and write back to file:
(echo "$changelog_header"; echo "$changelog"; echo; echo; echo "$changelog_prev_releases") > ChangeLog
}

declare -A checked_ids=()
check_or_add_pr() {
local id=$1
if [[ "${checked_ids[$id]+exists}" ]]; then
return
fi
checked_ids[$id]=1
local json=$(gh pr view "${id/#/}" --json title,author)
local title=$(jq -r .title <<<"${json}" | sanitize_title)
local author=$(jq -r .author.login <<<"${json}")
if grep -qF "#$id" <<<"$changelog"; then
return
fi
local title_suggestion_in_pr=$(gh pr view "$id" --json body,comments,reviews --jq '(.body), (.comments[] .body), (.reviews[] .body)' | grep -oP '\bCHANGELOG:\s*\K([^\\]{5,})' | tail -n1 | sanitize_title)
if [[ "${title_suggestion_in_pr}" ]]; then
title="${title_suggestion_in_pr}"
if [[ "${title_suggestion_in_pr}" == "SKIP" ]]; then
return
fi
fi
echo -n "-> Missing PR #${id} (${title}, @${author})"
if [[ "${ACTION}" != add-missing-entries ]]; then
echo
return
fi
echo ", adding new entry"
local new_entry=""
local lang=$(grep -oP 'Updated? \K(\S+)(?= app translations? for )' <<<"$title" || true)
if [[ "${lang}" ]]; then
# Note: This creates a top-level entry for each language.
# group-entries can merge those to a single one.
local full_lang="${LANGS[$lang]-${lang}}"
add_translation_pr "${lang}" "${author}" "${id}"
return
fi
new_entry=$(
echo "- ${title} (#${id})."
echo " (contributed by @${author})"
)
local changelog_before=$(head -n "${changelog_begin_position}" ChangeLog)
local changelog_after=$(tail -n "+$((${changelog_begin_position}+1))" ChangeLog)
(echo "$changelog_before"; echo; echo "$new_entry"; echo "$changelog_after") > ChangeLog
}

add_translation_pr() {
local lang="${1}"
local author="${2}"
local id="${3}"
local changelog_begin_position=$(grep -nP '^### .*\d+\.\d+\.\d+\b' ChangeLog | head -n1 | cut -d: -f1)
local changelog_prev_release_position=$(grep -nP '^### .*\d+\.\d+\.\d+\b' ChangeLog | head -n2 | tail -n1 | cut -d: -f1)

# Save everything before the actual release changelog content:
local changelog_header=$(head -n "${changelog_begin_position}" ChangeLog)

# Save everything after the actual release changelog content:
local changelog_prev_releases=$(tail -n "+${changelog_prev_release_position}" ChangeLog)

# Save the current release's changelog content:
local changelog=$(sed -rne '/^###.*'"${target_release//./\.}"'\b/,/^### '"${prev_release//./\.}"'\b/p' ChangeLog | tail -n +2 | head -n -1)
local changelog_orig="${changelog}"

# Is there an existing entry for this language already?
changelog=$(sed -re "s/^( \* ${full_lang}, by .+ \(.*)\)/\1, #${id})/" <<<"${changelog}")
if [[ "${changelog}" == "${changelog_orig}" ]]; then
# No existing language entry. Check for an existing translation entry.
changelog=$(sed -re "s/^(- ${TRANSLATION_ENTRY_TEXT}.*)/\1\n * ${full_lang}, by @${author} (#${id})/" <<<"${changelog}")
if [[ "${changelog}" == "${changelog_orig}" ]]; then
# No existing translation entry at all. Add a new one.
changelog="${changelog}$(
echo
echo
echo "- ${TRANSLATION_ENTRY_TEXT}"
echo " * ${full_lang}, by @${author} (#${id})"
)"
else
# Existing translation entries, so sort them:
local changelog_before_translations=""
local changelog_translations=""
local changelog_after_translations=""
local changelog_translations_pos=before # before|block|after
while IFS= read -r line; do
if [[ "${changelog_translations_pos}" == "before" ]]; then
if [[ "${line}" == "- ${TRANSLATION_ENTRY_TEXT}" ]]; then
changelog_translations_pos=block
fi
changelog_before_translations="${changelog_before_translations}${line}"$'\n'
continue
fi
if [[ "${changelog_translations_pos}" == "block" ]]; then
if [[ "${line}" == "" ]]; then
changelog_translations_pos=after
# fallthrough
else
changelog_translations="${changelog_translations}${line}"$'\n'
continue
fi
fi
if [[ "${changelog_translations_pos}" == "after" ]]; then
changelog_after_translations="${changelog_after_translations}${line}"$'\n'
fi
done <<< "${changelog}"
changelog="$(
# echo -n strips whitespace. we need that here.
echo -n "${changelog_before_translations}"
echo -n "$(grep -vP '^$' <<< "${changelog_translations}" | sort)"
echo -n "${changelog_after_translations}"
)"
fi
fi
# Rebuild the changelog and write back to file:
(echo "$changelog_header"; echo "$changelog"; echo; echo "$changelog_prev_releases") > ChangeLog
}

sanitize_title() {
sed \
-re 's/^\s+//' \
-re 's/\s{2,}/ /' \
-re 's/\s*\.?\s*$//' \
-re 's/\b((Add)|(Updat|Enhanc|Improv|Remov)e)\b/\2\3ed/i'
}

case "${1:-1}" in
find-missing-entries)
ACTION=find-missing-entries
;;
add-missing-entries)
ACTION=add-missing-entries
;;
group-entries)
ACTION=group-entries
;;
--help)
echo "Usage: $0 ACTION"
echo " Supported actions:"
echo " * find-missing-entries: Prints a list"
echo " * add-missing-entries: Inserts missing entries into the file"
echo " * group-entries: Groups existing entries by prefix"
echo
exit
;;
*)
echo "ERROR: Bad invocation, see --help"
exit 1
esac

target_release=$(grep -oP '^### .*\K(\d+\.\d+\.\d+)\b' ChangeLog | head -n1)
prev_release=$(grep -oP '^### .*\K(\d+\.\d+\.\d+)\b' ChangeLog | head -n2 | tail -n1)
target_release_tag=r${target_release//./_}
prev_release_tag=r${prev_release//./_}

echo "Auto-detected target release: ${target_release}"
echo "Auto-detected previous release: ${prev_release}"
echo

case "$ACTION" in
find-missing-entries|add-missing-entries)
find_or_add_missing_entries
;;
group-entries)
group_entries
;;
esac

0 comments on commit b37732e

Please sign in to comment.