#! /usr/bin/env bash # # Assembles a draft CHANGES entry out of revisions committed since the last # entry was added. The entry is prepended to the current CHANGES file, and the # user then gets a chance to further edit it in the editor before it gets # committed. # # The script also maintains and updates a VERSION file. # # If the script finds a file called .update-changes.cfg it sources it at the # beginning. That script can define a function "new_version_hook" that will be # called with the new version number. It may use any of the replace_version_* # functions defined below to update other files as necessary. # # If $1 is given, it's interpreted as a release version and a corresponding # tag is created. # # To start using update-changes in a new project, proceed as follows: # # (1) Run "update-changes -I". This will initialize the CHANGES file and, if # needed, establish suitable git tags that update-changes requires in order # to start enumerating commits after a release. You can also prepare the # initial version number in the VERSION file if you prefer that approach. # # (2) If you're planning to use an .update-changes.cfg file, add it as well as # any corresponding changes it requires. Continue regular development, and # when ready, run update-changes to reflect the first actual changeset in # the CHANGES file. # file_changes="CHANGES" # The CHANGES file. file_version="VERSION" # The VERSION file. file_config=".update-changes.cfg" # This will be sourced if available. new_version_hook="new_version_hook" # Function that will be called with new version number. new_commit_msg="Updating CHANGES and VERSION." # Commit message when creating a new commit. init_commit_msg="Starting CHANGES." # Commit message when we initialize CHANGES show_authors=1 # Include author names with commit. # The command line used to generate a revision's version string, such as # v1.0.0-23-gabcdef. This relies on tags to work, which update-changes checks # for. By default this only finds annotated tags; to allow lightweight ones as # well, add --tags. git_describe="git describe --tags" # {rev} will be added. # The command line used to generate a revision's date. The revision will be # appended. Not used with Zeek-style CHANGES file. git_rev_date="git show -s --pretty=tformat:%ci" # The command line used to generate the list of revisions between old and new # state. git_rev_list="git rev-list --topo-order HEAD" # ^{past-rev} will be added. # The command line used to show the one-line summary of a revision before # editing. git_rev_summary="git show -s '--pretty=tformat: %h | %aN | %s'" # {rev} will be added. # The command line used to get a revision's author. git_author="git show -s --pretty=format:%aN" # {rev} will be added. git_author_email="git show -s --pretty=format:%aE" # {rev} will be added. # The command line used to get a revision's message. git_msg=default_format_msg # {rev} will be added. # Portable access to ERE, see e.g. https://unix.stackexchange.com/a/131940 if [ $(uname) == "Linux" ]; then sed="sed -r" else sed="sed -E" fi function usage { echo "usage: $(basename $0) [options]" echo echo " -p Explicitly name the past revision to compare with." echo " -R Tag the current revision as a release. Update VERSION to use that." echo " -B Tag the current revision as a beta release. Update VERSION to use that." echo " -r Tag the current revision as a release, using the next point version as version tag." echo " -I Initialize a new, initially empty CHANGES file." echo " -c Check whether CHANGES is up to date." echo " -n Do not amend the HEAD commit when feasible, create a new one." echo exit 1 } # Takes a version string as input and turns it into a Python-styled one. For # example, input "1.2-23" becomes "1.2.dev23". Other formats remain # unchanged. See: https://peps.python.org/pep-0440/#version-scheme function pythonic_version { echo "$1" | $sed "s#-#.dev#" } ### Functions that can be used to replace version strings in other files. ### To use them, create a file $file_config and define a function ### "new_version_hook" in there that does whatever is necessary, like calling ### any of these. # Function that looks for lines of the form 'VERSION="1.2.3"' in $1. It will # replace the version number with $2 and then git-adds the change. function replace_version_in_script { file=$1 version=$2 cat $file | $sed "s#^([[:blank:]]*VERSION[[:blank:]]*=[[:blank:]]*)\"([0-9.-]+)\"#\1\"$version\"#g" >$file.tmp cat $file.tmp >$file rm -f $file.tmp git add $file } # Function that looks for lines of the form '.. |version| replace:: 0.3' in $1. # It will replace the version number with $2 and then git-adds the change. function replace_version_in_rst { file=$1 version=$2 cat $file | $sed "s#^([[:blank:]]*\.\.[[:blank:]]*\|version\|[[:blank:]]*replace::[[:blank:]]*)([0-9a-zA-Z.-]+)#\1$version#g" >$file.tmp cat $file.tmp >$file rm -f $file.tmp git add $file } # Function that checks file $1 for lines starting with 'version = # ""', where the version string can be of release form (e.g. "1.2.3") # or a development one, expressed as "1.2-23" or the Python-styled "1.2.dev23". # It will replace the version number with a Python-styled form of $2, then # git-add the change. function replace_version_in_setup_py { file=$1 version=$(pythonic_version $2) # The version string can be a sequence of digits and dots, optionally # followed by either "-" or ".dev" plus at least one digit. cat $file | $sed "s#^([[:blank:]]*version[[:blank:]]*=[[:blank:]]*)\"[0-9.]+((-|\.dev)[0-9]+)?\"#\1\"$version\"#g" >$file.tmp cat $file.tmp >$file rm -f $file.tmp git add $file } # Function that checks file $1 for lines starting with '__version__ = # ""', where the version string can be of release form (e.g. "1.2.3") # or a development one, expressed as "1.2-23" or the Python-styled "1.2.dev23". # It will replace the version number with a Python-styled form of $2, then # git-add the change. function replace_version_in_python_package { file=$1 version=$(pythonic_version $2) # The version string can be a sequence of digits and dots, optionally # followed by either "-" or ".dev" plus at least one digit. cat $file | $sed "s#^([[:blank:]]*__version__[[:blank:]]*=[[:blank:]]*)\"[0-9.]+((-|\.dev)[0-9]+)?\"#\1\"$version\"#g" >$file.tmp cat $file.tmp >$file rm -f $file.tmp git add $file } # Function that looks for lines of the form "#define .*VERSION "0.3"", with the # number being "version * 100". It will replace the version with $2 and then # git-adds the change. function replace_version_in_c_header { file=$1 version=$2 cat $file | $sed "s#([[:blank:]]*\#define[[:blank:]]*[_A-Za-z0-9]*_VERSION[[:blank:]]*)\"[0-9.-]+\"#\1\"$version\"#g" >$file.tmp mv $file.tmp $file git add $file } # Default function for preparing commit message. This scans the message for # GitHub issue references to include. function default_format_msg { if command -v gawk &>/dev/null; then # We need gawk for the match(). git show -s --pretty=format:%B $1 | gawk ' match($0, "([Ii]ssue|[Gg][Hh]|#)[ _-]?([0-9]+)", x) { issues[x[2]] = 1; } { msg = msg $0 "\n"; } END { if ( ! match(msg, "^GH-[0-9]+") ) { for ( i in issues ) printf("GH-%s: ", i); } print msg; }' else git show -s --pretty=format:%B $1 fi } ### function version { rev=$1 $git_describe $rev --match "v*" 2>/dev/null | $sed 's/^v//g' | $sed 's/-g.*//g' | $sed 's/-([[:alnum:]]+)-([0-9]+)$/-\1.\2/g' } function start_changes_entry { version=$1 dst=$2 if [ "$zeek_style" == "0" ]; then date=$($git_rev_date HEAD) printf '%s | %s\n' "$version" "$date" >>$dst else date=$(date) printf '%s %s\n' "$version" "$date" >>$dst fi } function add_to_changes_entry { rev=$1 dst=$2 msg=$3 author="" if [ "$msg" == "" ]; then if [ "$show_authors" == "1" ]; then author_email=$($git_author_email $rev) author=$($git_author $rev) if [[ "$author_email" == *@corelight.com ]]; then author=" ($author, Corelight)" else author=" ($author)" fi fi msg=$($git_msg $rev) fi if [ "$msg" == "" ]; then return 1 fi if echo $msg | grep -q "^$new_commit_msg\$"; then # Ignore our own automated commits. return 1 fi if [[ $(git show --no-patch --format='%P' "$rev" | wc -w) -gt 1 ]]; then # Ignore merge commits, i.e., commits with more than one parent. return 1 fi echo >>$dst if [ "$zeek_style" == "0" ]; then bullet=" *" else bullet="-" fi echo -n "$msg" | awk -v bullet="$bullet" -v author="$author" 'NR==1{printf "%s %s%s\n", bullet, $0, author; next }{printf " %s\n", $0}' | $sed 's/[[:blank:]]*$//' >>$dst return 0 } function init_changes { for rev in $(git rev-list HEAD); do version=$(version $rev) [ -n "$version" ] && break done git_version=$version if [ "$version" == "" ] && [ -f $file_version ]; then # git doesn't offer version info, but there's a VERSION file. # Consider it if the user's okay with it. version=$(cat $file_version | head -1) if [ -n "$version" ]; then echo "This git repo doesn't yet offer suitable version tags." read -p "Use '$version' from $file_version? Y/n " -n 1 -r echo if [ -n "$REPLY" ] && [[ $REPLY != [Yy] ]]; then version= fi fi fi if [ "$version" == "" ]; then read -p "No initial version available, please provide one (e.g. 0.1, 1.0.0): " -r version if [[ "$version" == v* ]]; then # We don't need a "v" prefix here, it only exists in the git tags. version=${version:1} fi fi # Subtle: if we're basing this CHANGES intro on a git tag, then we have a # chicken-and-egg problem with correct numbering of the commit introducing # CHANGES. It is itself going to be the _next_ commit. We could increment # the git-derived version number arithmetically, but it's easier to just # commit an empty CHANGES and then augment that below. if [ -n "$git_version" ]; then touch $file_changes git add $file_changes git commit -m "$init_commit_msg" version=$(version HEAD) flags="--amend" fi start_changes_entry $version $file_changes echo >>$file_changes echo " * Starting $file_changes." >>$file_changes git add $file_changes git commit $flags -m "$init_commit_msg" if [ -z "$git_version" ]; then git tag "v$version" fi } function get_last_rev { version=$(cat $file_changes | grep -E '^[0-9a-zA-Z.-]+ *\|' | head -1 | awk '{print $1}') if echo $version | grep -q -- '-'; then # version is now e.g. 1.0.4-14 -- find the revision with that number. for rev in $(git rev-list HEAD); do v=$(version $rev) if [ "$v" == "$version" ]; then echo $rev return fi done echo "Cannot determine revision for version $version." >/dev/stderr exit 1 else # A tag. echo "v$version" fi } function check_release_tag { if [ "$release" != "" ]; then git tag -d $release 2>/dev/null git tag -a $release -m "Version tag" echo "Tagged with new tag $release." echo echo "Push with: " echo echo " git push origin && git push origin $release" echo fi } function check_beta_tag { if [ "$beta" != "" ]; then git tag -d $beta 2>/dev/null git tag -a $beta -m "Beta version tag" echo "Tagged with new tag $beta." echo echo "Push with: " echo echo " git push origin && git push origin $beta" echo fi } function check_submodules { if git submodule status --recursive | grep ^+; then cat <".' exit 1 fi auto_version=$(version HEAD) if [ "$auto_version" == "" ]; then echo "Cannot determine version, checking HEAD did not return anything." exit 1 fi tmp=${file_changes}.$$.tmp trap "rm -f $tmp" EXIT rm -f $tmp found=0 new_version=$auto_version version=$(version $rev) if [ "$version" == "" ]; then echo "Cannot determine version for $rev." exit 1 fi if [ "$release" != "" ]; then new_version=$(echo $release | sed 's/v//g') fi if [ "$beta" != "" ]; then new_version=$(echo $beta | sed 's/v//g') fi if [ "$quiet" != "1" ]; then echo "New version is $new_version." echo "Listing revisions committed since $(version $last_rev) ($last_rev) ... " echo fi start_changes_entry $new_version $tmp for rev in $($git_rev_list ^$last_rev); do version=$(version $rev) if [ "$version" == "" ]; then version="" fi # printf "%15s |" $version if add_to_changes_entry $rev $tmp; then found=1 if [ "$quiet" != "1" ]; then eval "$git_rev_summary $rev | grep -v '^$' | cat" fi fi done if [ "$found" == "0" ]; then if [ "$check" == "1" ]; then echo "CHANGES is up to date." exit 0 fi echo " None." echo if [ "$release" != "" -o "$beta" != "" ]; then add_to_changes_entry head $tmp "Release $new_version." else exit 0 fi fi if [ "$check" == "1" ]; then echo "CHANGES is NOT up to date." exit 1 fi echo >>$tmp cat $file_changes >>$tmp # If we are ahead of origin, we can amend. If not, we need to create a new # commit even if the user wants otherwise. If the user requested -n (no # amendments), we skip all of this. amend=0 if [ $no_amends == "0" ] && git remote | grep -q origin && git rev-list origin/$(git rev-parse --abbrev-ref HEAD)..HEAD | grep -q .; then amend=1 fi echo if [ "$amend" == "0" ]; then echo Update to $file_changes will become a new commit. else echo Update to $file_changes will be amended to last commit. fi echo echo Type Enter to edit new $file_changes, or CTRL-C to abort without any modifications. read # Run editor. if [ -z "$EDITOR" ]; then EDITOR=vi fi eval $EDITOR $tmp # Put changes in place. mv $tmp $file_changes echo "Updated $file_changes." if [ "$file_version" != "" ]; then echo $new_version >$file_version echo "Updated $version to $new_version." fi # Call hook function if it exists. if type $new_version_hook >/dev/null 2>&1; then $new_version_hook $new_version fi # Commit changes. git add $file_changes $file_version if [ "$amend" == "1" ]; then git commit --amend else git commit -m "$new_commit_msg" fi echo "Updates committed." check_release_tag check_beta_tag