Skip to content

Instantly share code, notes, and snippets.

@gburgett
Last active January 5, 2022 15:59
Show Gist options
  • Save gburgett/17d00da4cbd018c52978f209cb3235e2 to your computer and use it in GitHub Desktop.
Save gburgett/17d00da4cbd018c52978f209cb3235e2 to your computer and use it in GitHub Desktop.
generates a changelog from git history, grabbing PRs from github
#! /bin/bash
COLOR_NC='\033[0m' # No Color
COLOR_LIGHT_GREEN='\033[1;32m'
COLOR_GRAY='\033[1;30m'
COLOR_RED='\033[0;31m'
logv() {
[[ ! -z "$VERBOSE" ]] && >&2 echo -e "${COLOR_GRAY}$*${COLOR_NC}"
}
logerr() {
>&2 echo -e "${COLOR_RED}$*${COLOR_NC}"
exit -1
}
usage() {
echo "$0 <from> <to>
Parse the git history to create a changelog from the latest release.
FROM: the initial commit from which to generate a changelog.
If 'all', then this will create a complete changelog with
changes grouped by 'Release*' tag.
If not given, the merge-base between HEAD and master is assumed.
TO: The end commit until which to generate a changelog.
If not given, 'HEAD' is assumed.
" && \
grep " .)\ #" $0; exit 0;
}
while getopts ":hvm" arg; do
case $arg in
v) # Verbose mode - extra output
VERBOSE=true
FLAGS="$FLAGS -v"
;;
m) # Output in markdown format
MARKDOWN=true
;;
h | *) # Display help.
usage
exit 0
;;
esac
done
shift $(($OPTIND - 1))
[[ -z "$GITHUB_TOKEN" ]] && logerr "Please set Github API token in GITHUB_TOKEN environment variable"
command -v jq >/dev/null 2>&1 || logerr "I require 'jq' but it's not installed."
inside_git_repo="$(git rev-parse --is-inside-work-tree 2>/dev/null)"
[ ! "$inside_git_repo" ] && logerr "fatal: Not a git repository"
TO=$2
[[ -z "$TO" ]] && TO=$(git rev-parse HEAD)
FROM=$1
if [[ -z "$FROM" ]]; then
mains=("master" "main")
main=""
for m in "${mains[@]}"; do
if git rev-parse --quiet --verify "origin/$m" > /dev/null; then
main="$m"
fi
done
[[ -z "$main" ]] && logv "no main branch found" && return -1;
FROM=$(git merge-base "$TO" "origin/$main")
fi
if [[ "$FROM" == "all" ]]; then
TAGS=$(git tag --sort=-version:refname -l 'Release*')
LAST=''
while read -r line; do
$0 $FLAGS $line $LAST
[[ $? -ne 0 ]] && exit -1
LAST=$line
echo ""
done <<< "${TAGS}"
INITIAL=$(git log --reverse --oneline | head -n 1 | awk '{ print $1 }')
$0 $FLAGS $INITIAL $LAST
exit $?
fi
FROM=$(git describe --exact-match --tags $FROM 2>/dev/null || echo "$FROM")
TO=$(git describe --exact-match --tags $TO 2>/dev/null || echo "$TO")
logv "git log ${FROM}..${TO}"
PRS=$(git log ${FROM}..${TO} --merges --oneline --grep '^Merge pull request')
logv "$PRS"
[[ -z "$PRS" ]] && exit 0
PROJECT=$(git config --get remote.origin.url | sed -E 's/.*github\.com.(.*)(\.git)?/\1/')
PROJECT=${PROJECT%.git}
logv "Project is ${PROJECT}"
[[ "$PROJECT" == $(git config --get remote.origin.url) ]] && logerr "remote $PROJECT is not a github repo"
ISSUES_FIXED=()
echo "# Changelog From ${FROM} To ${TO}"
while read -r line; do
PR_NUM=$(echo $line | sed 's/.*\#\([0-9]*\).*/\1/')
logv "curl https://api.github.com/repos/${PROJECT}/pulls/${PR_NUM}"
PR_INFO=$(curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${PROJECT}/pulls/${PR_NUM} 2>/dev/null)
PR_TITLE=$(echo "${PR_INFO}" | jq -r '.title')
logv $PR_TITLE
if [[ $(echo "$PR_TITLE" | grep -i '^[[:space:]]*release') ]]; then
logv "Release PR - skipping"
continue
fi
PR_BODY=$(echo "${PR_INFO}" | jq -r '.body' | sed 's/\\n/\
/g')
PR_USER=$(echo "${PR_INFO}" | jq -r '.user.login')
PR_MERGED_AT=$(echo "${PR_INFO}" | jq -r '.merged_at')
if [[ $(echo "$PR_USER" | grep -i '^dependabot') ]]; then
logv "Dependabot PR - skipping body"
PR_BODY=''
fi
PR_DATE=$(date -jf '%FT%TZ' "${PR_MERGED_AT}" +"%Y-%m-%d")
ISSUES_FIXED+=($(echo "$PR_BODY" | grep -i 'fixes\|closes\|refs #' | awk '{print $2}'))
echo "<details>"
echo "<summary>#${PR_NUM} ${PR_TITLE} @${PR_USER} merged ${PR_DATE}</summary>"
echo ""
echo "${PR_BODY}"
echo ""
echo "</details>"
done <<< "${PRS}"
scratch=$(mktemp)
function finish {
logv rm -rf "$scratch"
rm -rf "$scratch"
}
trap finish EXIT
for issue in "${ISSUES_FIXED[@]}"
do
logv "issue: ${issue}"
ISSUE_NUM=$(echo $issue | sed 's/.*\#\([0-9]*\).*/\1/')
logv "curl https://api.github.com/repos/${PROJECT}/issues/${ISSUE_NUM}"
ISSUE_INFO=$(curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${PROJECT}/issues/${ISSUE_NUM} 2>/dev/null)
[[ -z "$ISSUE_INFO" ]] && continue
RESP_MESSAGE=$(echo "$ISSUE_INFO" | jq -r .message)
[[ "$RESP_MESSAGE" == "Not Found" ]] && continue
logv $ISSUE_INFO
echo "$ISSUE_INFO" >> $scratch
done
BY_MILESTONE=$(jq --slurp -c 'group_by(.milestone.title) | .[]' $scratch)
echo ""
echo "## Issues closed in this release:"
while IFS= read -r milestone ; do
milestone_items=$(echo "$milestone" | jq -c '.[]')
CURRENT_MILESTONE=
while IFS= read -r ISSUE_INFO ; do
MILESTONE_URL=$(echo "${ISSUE_INFO}" | jq -r '.milestone.url')
if [[ "$CURRENT_MILESTONE" != "$MILESTONE_URL" ]]; then
logv "changing milestone to $MILESTONE_URL"
CURRENT_MILESTONE="$MILESTONE_URL"
if [[ "null" == "$MILESTONE_URL" ]]; then
echo "### No Milestone"
else
MILESTONE_TEXT=$(echo "${ISSUE_INFO}" | jq -r '.milestone.title')
MILESTONE_HTML_URL=$(echo "${ISSUE_INFO}" | jq -r '.milestone.html_url')
echo "### Milestone: [$MILESTONE_TEXT]($MILESTONE_HTML_URL)"
fi
fi
ISSUE_TITLE=$(echo "$ISSUE_INFO" | jq -r .title)
ISSUE_USER=$(echo "${ISSUE_INFO}" | jq -r '.user.login')
ISSUE_NUMBER=$(echo "$ISSUE_INFO" | jq -r .number)
echo "* #${ISSUE_NUMBER} ${ISSUE_TITLE}"
echo " - created by @${ISSUE_USER}"
done <<< "$milestone_items"
done <<< "$BY_MILESTONE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment