624 lines
17 KiB
Bash
Executable File
624 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
PROG=semtag
|
|
PROG_VERSION="v0.1.0"
|
|
|
|
SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"
|
|
IDENTIFIER_REGEX="^\-([0-9A-Za-z-]+)\.([0-9A-Za-z-]+)*$"
|
|
|
|
# Global variables
|
|
FIRST_VERSION="v0.0.0"
|
|
finalversion=$FIRST_VERSION
|
|
lastversion=$FIRST_VERSION
|
|
hasversiontag="false"
|
|
scope="patch"
|
|
displayonly="false"
|
|
forcetag="false"
|
|
forcedversion=
|
|
versionname=
|
|
identifier=
|
|
|
|
HELP="\
|
|
Usage:
|
|
$PROG
|
|
$PROG getlast
|
|
$PROG getfinal
|
|
$PROG (final|alpha|beta|candidate) [-s <scope> (major|minor|patch|auto) | -o]
|
|
$PROG --help
|
|
$PROG --version
|
|
Options:
|
|
-s The scope that must be increased, can be major, minor or patch.
|
|
The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD)
|
|
where X, Y and Z are positive integers, PRERELEASE is an optionnal
|
|
string composed of alphanumeric characters describing if the build is
|
|
a release candidate, alpha or beta version, with a number.
|
|
BUILD is also an optional string composed of alphanumeric
|
|
characters and hyphens.
|
|
Setting the scope as 'auto', the script will chose the scope between
|
|
'minor' and 'patch', depending on the amount of lines added (<10% will
|
|
choose patch).
|
|
-v Specifies manually the version to be tagged, must be a valid semantic version
|
|
in the format X.Y.Z where X, Y and Z are positive integers.
|
|
-o Output the version only, shows the bumped version, but doesn't tag.
|
|
-f Forces to tag, even if there are unstaged or uncommited changes.
|
|
Commands:
|
|
--help Print this help message.
|
|
--version Prints the program's version.
|
|
get Returns both current final version and last tagged version.
|
|
getlast Returns the latest tagged version.
|
|
getfinal Returns the latest tagged final version.
|
|
getcurrent Returns the current version, based on the latest one, if there are uncommited or
|
|
unstaged changes, they will be reflected in the version, adding the number of
|
|
pending commits, current branch and commit hash.
|
|
final Tags the current build as a final version, this only can be done on the master branch.
|
|
candidate Tags the current build as a release candidate, the tag will contain all
|
|
the commits from the last final version.
|
|
alpha Tags the current build as an alpha version, the tag will contain all
|
|
the commits from the last final version.
|
|
beta Tags the current build as a beta version, the tag will contain all
|
|
the commits from the last final version."
|
|
|
|
# Commands and options
|
|
ACTION="getlast"
|
|
ACTION="$1"
|
|
shift
|
|
|
|
# We get the parameters
|
|
while getopts "v:s:of" opt; do
|
|
case $opt in
|
|
v)
|
|
forcedversion="$OPTARG"
|
|
;;
|
|
s)
|
|
scope="$OPTARG"
|
|
;;
|
|
o)
|
|
displayonly="true"
|
|
;;
|
|
f)
|
|
forcetag="true"
|
|
;;
|
|
\?)
|
|
echo "Invalid option: -$OPTARG" >&2
|
|
exit 1
|
|
;;
|
|
:)
|
|
echo "Option -$OPTARG requires an argument." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version
|
|
# $1 The string containing the version in semantic format
|
|
# $2 The variable to store the result array:
|
|
# position 0: major number
|
|
# position 1: minor number
|
|
# position 2: patch number
|
|
# position 3: identifier (or prerelease identifier)
|
|
# position 4: build info
|
|
function explode_version {
|
|
local __version=$1
|
|
local __result=$2
|
|
if [[ $__version =~ $SEMVER_REGEX ]] ; then
|
|
local __major=${BASH_REMATCH[1]}
|
|
local __minor=${BASH_REMATCH[2]}
|
|
local __patch=${BASH_REMATCH[3]}
|
|
local __prere=${BASH_REMATCH[4]}
|
|
local __build=${BASH_REMATCH[5]}
|
|
eval "$__result=(\"$__major\" \"$__minor\" \"$__patch\" \"$__prere\" \"$__build\")"
|
|
else
|
|
eval "$__result="
|
|
fi
|
|
}
|
|
|
|
# Compare two versions and returns -1, 0 or 1
|
|
# $1 The first version to compare
|
|
# $2 The second version to compare
|
|
# $3 The variable where to store the result
|
|
function compare_versions {
|
|
local __first
|
|
local __second
|
|
explode_version $1 __first
|
|
explode_version $2 __second
|
|
local lv=$3
|
|
|
|
# Compares MAJOR, MINOR and PATCH
|
|
for i in 0 1 2; do
|
|
local __numberfirst=${__first[$i]}
|
|
local __numbersecond=${__second[$i]}
|
|
case $(($__numberfirst - $__numbersecond)) in
|
|
0)
|
|
;;
|
|
-[0-9]*)
|
|
eval "$lv=-1"
|
|
return 0
|
|
;;
|
|
[0-9]*)
|
|
eval "$lv=1"
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Identifiers should compare with the ASCII order.
|
|
local __identifierfirst=${__first[3]}
|
|
local __identifiersecond=${__second[3]}
|
|
if [[ -n "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
|
|
if [[ "$__identifierfirst" > "$__identifiersecond" ]]; then
|
|
eval "$lv=1"
|
|
return 0
|
|
elif [[ "$__identifierfirst" < "$__identifiersecond" ]]; then
|
|
eval "$lv=-1"
|
|
return 0
|
|
fi
|
|
elif [[ -z "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
|
|
eval "$lv=1"
|
|
return 0
|
|
elif [[ -n "$__identifierfirst" ]] && [[ -z "$__identifiersecond" ]]; then
|
|
eval "$lv=-1"
|
|
return 0
|
|
fi
|
|
|
|
eval "$lv=0"
|
|
}
|
|
|
|
# Returns the last version of two
|
|
# $1 The first version to compare
|
|
# $2 The second version to compare
|
|
# $3 The variable where to store the last one
|
|
function get_latest_of_two {
|
|
local __first=$1
|
|
local __second=$2
|
|
local __result
|
|
local __latest=$3
|
|
compare_versions $__first $__second __result
|
|
case $__result in
|
|
0)
|
|
eval "$__latest=$__second"
|
|
;;
|
|
-1)
|
|
eval "$__latest=$__second"
|
|
;;
|
|
1)
|
|
eval "$__latest=$__first"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1
|
|
# $1 The identifier in the format -id.#
|
|
# $2 The vferiable where to store the 2 size array
|
|
function explode_identifier {
|
|
local __identifier=$1
|
|
local __result=$2
|
|
if [[ $__identifier =~ $IDENTIFIER_REGEX ]] ; then
|
|
local __id=${BASH_REMATCH[1]}
|
|
local __number=${BASH_REMATCH[2]}
|
|
if [[ -z "$__number" ]]; then
|
|
__number=1
|
|
fi
|
|
eval "$__result=(\"$__id\" \"$__number\")"
|
|
else
|
|
eval "$__result="
|
|
fi
|
|
}
|
|
|
|
# Gets a list of tags and assigns the base and latest versions
|
|
# Receives an array with the tags containing the versions
|
|
# Assigns to the global variables finalversion and lastversion the final version and the latest version
|
|
function get_latest {
|
|
local __taglist=("$@")
|
|
local __tagsnumber=${#__taglist[@]}
|
|
local __current
|
|
case $__tagsnumber in
|
|
0)
|
|
finalversion=$FIRST_VERSION
|
|
lastversion=$FIRST_VERSION
|
|
;;
|
|
1)
|
|
__current=${__taglist[0]}
|
|
explode_version $__current ver
|
|
if [ -n "$ver" ]; then
|
|
if [ -n "${ver[3]}" ]; then
|
|
finalversion=$FIRST_VERSION
|
|
else
|
|
finalversion=$__current
|
|
fi
|
|
lastversion=$__current
|
|
else
|
|
finalversion=$FIRST_VERSION
|
|
lastversion=$FIRST_VERSION
|
|
fi
|
|
;;
|
|
*)
|
|
local __lastpos=$(($__tagsnumber-1))
|
|
for i in $(seq 0 $__lastpos)
|
|
do
|
|
__current=${__taglist[i]}
|
|
explode_version ${__taglist[i]} ver
|
|
if [ -n "$ver" ]; then
|
|
if [ -z "${ver[3]}" ]; then
|
|
get_latest_of_two $finalversion $__current finalversion
|
|
get_latest_of_two $lastversion $finalversion lastversion
|
|
else
|
|
get_latest_of_two $lastversion $__current lastversion
|
|
fi
|
|
fi
|
|
done
|
|
;;
|
|
esac
|
|
|
|
if git rev-parse -q --verify "refs/tags/$lastversion" >/dev/null; then
|
|
hasversiontag="true"
|
|
else
|
|
hasversiontag="false"
|
|
fi
|
|
}
|
|
|
|
# Gets the next version given the provided scope
|
|
# $1 The version that is going to be bumped
|
|
# $2 The scope to bump
|
|
# $3 The variable where to stoer the result
|
|
function get_next_version {
|
|
local __exploded
|
|
local __fromversion=$1
|
|
local __scope=$2
|
|
local __result=$3
|
|
explode_version $__fromversion __exploded
|
|
case $__scope in
|
|
major)
|
|
__exploded[0]=$((${__exploded[0]}+1))
|
|
__exploded[1]=0
|
|
__exploded[2]=0
|
|
;;
|
|
minor)
|
|
__exploded[1]=$((${__exploded[1]}+1))
|
|
__exploded[2]=0
|
|
;;
|
|
patch)
|
|
__exploded[2]=$((${__exploded[2]}+1))
|
|
;;
|
|
esac
|
|
|
|
eval "$__result=v${__exploded[0]}.${__exploded[1]}.${__exploded[2]}"
|
|
}
|
|
|
|
function bump_version {
|
|
## First we try to get the next version based on the existing last one
|
|
if [ "$scope" == "auto" ]; then
|
|
get_scope_auto scope
|
|
fi
|
|
|
|
local __candidatefromlast=$FIRST_VERSION
|
|
local __explodedlast
|
|
explode_version $lastversion __explodedlast
|
|
if [[ -n "${__explodedlast[3]}" ]]; then
|
|
# Last version is not final
|
|
local __idlast
|
|
explode_identifier ${__explodedlast[3]} __idlast
|
|
|
|
# We get the last, given the desired id based on the scope
|
|
__candidatefromlast="v${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}"
|
|
if [[ -n "$identifier" ]]; then
|
|
local __nextid="$identifier.1"
|
|
if [ "$identifier" == "${__idlast[0]}" ]; then
|
|
# We target the same identifier as the last so we increase one
|
|
__nextid="$identifier.$(( ${__idlast[1]}+1 ))"
|
|
__candidatefromlast="$__candidatefromlast-$__nextid"
|
|
else
|
|
# Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version
|
|
__candidatefromlast="$__candidatefromlast-$__nextid"
|
|
local __comparedwithlast
|
|
compare_versions $__candidatefromlast $lastversion __comparedwithlast
|
|
if [ "$__comparedwithlast" == -1 ]; then
|
|
get_next_version $__candidatefromlast $scope __candidatefromlast
|
|
__candidatefromlast="$__candidatefromlast-$__nextid"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Then we try to get the version based on the latest final one
|
|
local __candidatefromfinal=$FIRST_VERSION
|
|
get_next_version $finalversion $scope __candidatefromfinal
|
|
if [[ -n "$identifier" ]]; then
|
|
__candidatefromfinal="$__candidatefromfinal-$identifier.1"
|
|
fi
|
|
|
|
# Finally we compare both candidates
|
|
local __resultversion
|
|
local __result
|
|
compare_versions $__candidatefromlast $__candidatefromfinal __result
|
|
case $__result in
|
|
0)
|
|
__resultversion=$__candidatefromlast
|
|
;;
|
|
-1)
|
|
__resultversion="$__candidatefromfinal"
|
|
;;
|
|
1)
|
|
__resultversion=$__candidatefromlast
|
|
;;
|
|
esac
|
|
|
|
eval "$1=$__resultversion"
|
|
}
|
|
|
|
function increase_version {
|
|
local __version=
|
|
|
|
if [ -z $forcedversion ]; then
|
|
bump_version __version
|
|
else
|
|
if [[ $forcedversion =~ $SEMVER_REGEX ]] ; then
|
|
compare_versions $forcedversion $lastversion __result
|
|
if [ $__result -le 0 ]; then
|
|
echo "Version can't be lower than last version: $lastversion"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Non valid version to bump"
|
|
exit 1
|
|
fi
|
|
__version=$forcedversion
|
|
fi
|
|
|
|
if [ "$displayonly" == "true" ]; then
|
|
echo "$__version"
|
|
else
|
|
if [ "$forcetag" == "false" ]; then
|
|
check_git_dirty_status
|
|
fi
|
|
local __commitlist
|
|
if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then
|
|
__commitlist="$(git log --pretty=oneline | cat)"
|
|
else
|
|
__commitlist="$(git log --pretty=oneline $finalversion... | cat)"
|
|
fi
|
|
|
|
# If we are forcing a bump, we add bump to the commit list
|
|
if [[ -z $__commitlist && "$forcetag" == "true" ]]; then
|
|
__commitlist="bump"
|
|
fi
|
|
|
|
if [[ -z $__commitlist ]]; then
|
|
echo "No commits since the last final version, not bumping version"
|
|
else
|
|
if [[ -z $versionname ]]; then
|
|
versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
fi
|
|
local __message="$versionname
|
|
$__commitlist"
|
|
|
|
# We check we have info on the user
|
|
local __username=$(git config user.name)
|
|
if [ -z "$__username" ]; then
|
|
__username=$(id -u -n)
|
|
git config user.name $__username
|
|
fi
|
|
local __useremail=$(git config user.email)
|
|
if [ -z "$__useremail" ]; then
|
|
__useremail=$(hostname)
|
|
git config user.email "$__username@$__useremail"
|
|
fi
|
|
|
|
git tag -a $__version -m "$__message"
|
|
|
|
# If we have a remote, we push there
|
|
local __remote=${SEMTAG_REMOTE:-$(git remote)}
|
|
if [[ -n $__remote ]]; then
|
|
git push $__remote $__version > /dev/null
|
|
if [ $? -eq 0 ]; then
|
|
echo "$__version"
|
|
else
|
|
echo "Error pushing the tag $__version to $__remote"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "$__version"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function check_git_dirty_status {
|
|
local __repostatus=
|
|
get_work_tree_status __repostatus
|
|
|
|
if [ "$__repostatus" == "uncommitted" ]; then
|
|
echo "ERROR: You have uncommitted changes"
|
|
git status --porcelain
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$__repostatus" == "unstaged" ]; then
|
|
echo "ERROR: You have unstaged changes"
|
|
git status --porcelain
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Get the total amount of lines of code in the repo
|
|
function get_total_lines {
|
|
local __empty_id="$(git hash-object -t tree /dev/null)"
|
|
local __changes="$(git diff --numstat $__empty_id | cat)"
|
|
local __added_deleted=$1
|
|
get_changed_lines "$__changes" $__added_deleted
|
|
}
|
|
|
|
# Get the total amount of lines of code since the provided tag
|
|
function get_sincetag_lines {
|
|
local __sincetag=$1
|
|
local __changes="$(git diff --numstat $__sincetag | cat)"
|
|
local __added_deleted=$2
|
|
get_changed_lines "$__changes" $__added_deleted
|
|
}
|
|
|
|
function get_changed_lines {
|
|
local __changes_numstat=$1
|
|
local __result=$2
|
|
IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat"
|
|
local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$"
|
|
|
|
local __total_added=0
|
|
local __total_deleted=0
|
|
for i in "${__changes_array[@]}"
|
|
do
|
|
if [[ $i =~ $__diff_regex ]] ; then
|
|
local __added=${BASH_REMATCH[1]}
|
|
local __deleted=${BASH_REMATCH[2]}
|
|
__total_added=$(( $__total_added+$__added ))
|
|
__total_deleted=$(( $__total_deleted+$__deleted ))
|
|
fi
|
|
done
|
|
eval "$2=( $__total_added $__total_deleted )"
|
|
}
|
|
|
|
function get_scope_auto {
|
|
local __verbose=$2
|
|
local __total=0
|
|
local __since=0
|
|
local __scope=
|
|
|
|
get_total_lines __total
|
|
get_sincetag_lines $finalversion __since
|
|
|
|
local __percentage=0
|
|
if [ "$__total" != "0" ]; then
|
|
local __percentage=$(( 100*$__since/$__total ))
|
|
if [ $__percentage -gt "10" ]; then
|
|
__scope="minor"
|
|
else
|
|
__scope="patch"
|
|
fi
|
|
fi
|
|
|
|
eval "$1=$__scope"
|
|
if [[ -n "$__verbose" ]]; then
|
|
echo "[Auto Scope] Percentage of lines changed: $__percentage"
|
|
echo "[Auto Scope] : $__scope"
|
|
fi
|
|
}
|
|
|
|
function get_work_tree_status {
|
|
# Update the index
|
|
git update-index -q --ignore-submodules --refresh > /dev/null
|
|
eval "$1="
|
|
|
|
if ! git diff-files --quiet --ignore-submodules -- > /dev/null
|
|
then
|
|
eval "$1=unstaged"
|
|
fi
|
|
|
|
if ! git diff-index --cached --quiet HEAD --ignore-submodules -- > /dev/null
|
|
then
|
|
eval "$1=uncommitted"
|
|
fi
|
|
}
|
|
|
|
function get_current {
|
|
if [ "$hasversiontag" == "true" ]; then
|
|
local __commitcount="$(git rev-list $lastversion.. --count)"
|
|
else
|
|
local __commitcount="$(git rev-list --count HEAD)"
|
|
fi
|
|
local __status=
|
|
get_work_tree_status __status
|
|
|
|
if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then
|
|
eval "$1=$lastversion"
|
|
else
|
|
local __buildinfo="$(git rev-parse --short HEAD)"
|
|
local __currentbranch="$(git rev-parse --abbrev-ref HEAD)"
|
|
if [ "$__currentbranch" != "master" ]; then
|
|
__buildinfo="$__currentbranch.$__buildinfo"
|
|
fi
|
|
|
|
local __suffix=
|
|
if [ "$__commitcount" != "0" ]; then
|
|
if [ -n "$__suffix" ]; then
|
|
__suffix="$__suffix."
|
|
fi
|
|
__suffix="$__suffix$__commitcount"
|
|
fi
|
|
if [ -n "$__status" ]; then
|
|
if [ -n "$__suffix" ]; then
|
|
__suffix="$__suffix."
|
|
fi
|
|
__suffix="$__suffix$__status"
|
|
fi
|
|
|
|
__suffix="$__suffix+$__buildinfo"
|
|
if [ "$lastversion" == "$finalversion" ]; then
|
|
scope="patch"
|
|
identifier=
|
|
local __bumped=
|
|
bump_version __bumped
|
|
eval "$1=$__bumped-dev.$__suffix"
|
|
else
|
|
eval "$1=$lastversion.$__suffix"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function init {
|
|
git fetch > /dev/null
|
|
TAGS="$(git tag)"
|
|
IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS"
|
|
|
|
get_latest ${TAG_ARRAY[@]}
|
|
currentbranch="$(git rev-parse --abbrev-ref HEAD)"
|
|
}
|
|
|
|
case $ACTION in
|
|
--help)
|
|
echo -e "$HELP"
|
|
;;
|
|
--version)
|
|
echo -e "${PROG}: $PROG_VERSION"
|
|
;;
|
|
final)
|
|
init
|
|
diff=$(git diff master | cat)
|
|
if [ "$forcetag" == "false" ]; then
|
|
if [ -n "$diff" ]; then
|
|
echo "ERROR: Branch must be updated with master for final versions"
|
|
exit 1
|
|
fi
|
|
fi
|
|
increase_version
|
|
;;
|
|
alpha|beta)
|
|
init
|
|
identifier="$ACTION"
|
|
increase_version
|
|
;;
|
|
candidate)
|
|
init
|
|
identifier="rc"
|
|
increase_version
|
|
;;
|
|
getlast)
|
|
init
|
|
echo "$lastversion"
|
|
;;
|
|
getfinal)
|
|
init
|
|
echo "$finalversion"
|
|
;;
|
|
getcurrent)
|
|
init
|
|
get_current current
|
|
echo "$current"
|
|
;;
|
|
get)
|
|
init
|
|
echo "Current final version: $finalversion"
|
|
echo "Last tagged version: $lastversion"
|
|
;;
|
|
*)
|
|
echo "'$ACTION' is not a valid command, see --help for available commands."
|
|
;;
|
|
esac
|