#!/bin/sh
# This file is part of Checkbox.
#
# Copyright 2014 Canonical Ltd.
# Written by:
#   Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# Checkbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.

# Release management script
# =========================
#
# This script is indented to run in a directory with lp:checkbox checked-out
# (with bzr) as 'trunk'. Every change made by this script is local. The
# resulting branches are intended to be proposed back into trunk and merged
# with the CI system. 


assert() {
    if ! "$@"; then
        echo "E: assertion failed: $@" >&2
        exit 1
    fi
}


pep386_parse() {
    # Parse a human friendly version string into PEP386 canonical version
    #
    # Arguments:
    #   1 (version):
    #   The human-friendly version string
    # Return:
    #   canonical representation of that version
    if [ -z "$1" ]; then
        echo "E: pep386_parse: missing argument: version">&2
        exit 1
    fi
    echo "D: parsing version: $1" >&2
    local version=$(echo "$1" | awk -- '{ gsub(/a/, "alpha"); gsub(/b/, "beta"); gsub(/c|rc/, "candidate"); gsub(/[a-z]+/, ".&."); print; }')
    echo "D: after awk processing: $version" >&2
    local major=
    local minor=
    local micro=
    local releaselevel=final
    local serial=
    local rest=$version
    local next=
    while test -n "$rest"; do
        if echo "$rest" | grep -F . -q; then
            next=$(echo "$rest" | cut -d . -f 1)
            rest=$(echo "$rest" | cut -d . -f 2-)
        else
            next="$rest"
            rest=''
        fi
        case "$next" in
            dev|alpha|beta|candidate|final)
                releaselevel=$next
                test -z "$major" && major=0
                test -z "$minor" && minor=0
                test -z "$micro" && micro=0
                ;;
            *)
                if [ -z "$major" ]; then
                    major=$next
                elif [ -z "$minor" ]; then
                    minor=$next
                elif [ -z "$micro" ]; then
                    micro=$next
                elif [ -z "$serial" ]; then
                    serial=$next
                fi
                ;;
        esac
    done
    test -z "$major" && major=0
    test -z "$minor" && minor=0
    test -z "$micro" && micro=0
    test -z "$serial" && serial=0
    echo "D: after shell split/reassembly: $major.$minor.$micro.$releaselevel.$serial" >&2
    echo "$major.$minor.$micro.$releaselevel.$serial"
}


pep386_unparse() {
    # Reverse the pep386_parse() operation
    #
    # Arguments:
    #   1 (canonical_version):
    #   The canonical PEP386 version string
    # Return:
    #   human friendly version string without needless zeros, etc
    if [ -z "$1" ]; then
        echo "E: pep386_parse: missing argument: canonical_version">&2
        exit 1
    fi
    echo "D: unparsing version: $1" >&2
    local major=$(echo "$1" | cut -d . -f 1)
    local minor=$(echo "$1" | cut -d . -f 2)
    local micro=$(echo "$1" | cut -d . -f 3)
    local releaselevel=$(echo "$1" | cut -d . -f 4)
    local serial=$(echo "$1" | cut -d . -f 5)
    local version="$major.$minor"
    if [ "$micro" -ne 0 ]; then
        version="$version.$micro"
    fi
    case "$releaselevel" in 
        dev)
            if [ "$serial" -ne 0 ]; then
                version="${version}.dev.${serial}"
            else
                version="${version}.dev"
            fi
            ;;
        alpha)
            version="${version}a${serial}"
            ;;
        beta)
            version="${version}b${serial}"
            ;;
        candidate)
            version="${version}c${serial}"
            ;;
        final)
            ;;
    esac
    echo "D: unparsing: $version" >&2
    echo "$version"
}


pep386_as_tuple() {
    # Convert PEP386 version to python tuple sometimes found in __version__ 
    #
    # Arguments:
    #   1 (canonical_version):
    #   The canonical PEP386 version string
    # Return:
    #   Python tuple with the same data 
    if [ -z "$1" ]; then
        echo "E: pep386_as_tuple: missing argument: canonical_version">&2
        exit 1
    fi
    echo "D: canonical version: $1" >&2
    local major=$(echo "$1" | cut -d . -f 1)
    local minor=$(echo "$1" | cut -d . -f 2)
    local micro=$(echo "$1" | cut -d . -f 3)
    local releaselevel=$(echo "$1" | cut -d . -f 4)
    local serial=$(echo "$1" | cut -d . -f 5)
    local version="$major"
    local tuple="($major, $minor, $micro, \"$releaselevel\", $serial)"
    echo "D: python tuple: $tuple" >&2
    echo "$tuple"
}


pep386_attr() {
    # Access fields of pep386 canonical version
    #
    # Arguments:
    #   1 (canonical_version):
    #   The canonical PEP386 version string
    #   2+ (operation):
    #   Operations to perform. For getters they have a form of --field where
    #   field is one of the fields defined by the canonical version (major,
    #   minor, micro, releaselevel, serial). For setters the syntax is
    #   --field=value. Any number of setters can be used together. The first
    #   getter terminates the procedure. 
    #
    # Return:
    #   If a getter was used, the field that was referred to. Otherwise the
    #   canonical version is returned (after being modified by any of the
    #   setters).
    if [ -z "$1" ]; then
        echo "E: pep386_attr: missing argument: canonical_version">&2
        exit 1
    fi
    local major=$(echo "$1" | cut -d . -f 1)
    local minor=$(echo "$1" | cut -d . -f 2)
    local micro=$(echo "$1" | cut -d . -f 3)
    local releaselevel=$(echo "$1" | cut -d . -f 4)
    local serial=$(echo "$1" | cut -d . -f 5)
    shift
    while test -n "$1"; do
        case "$1" in
            --major)
                echo "$major"
                return
                ;;
            --major=*)
                major=$(echo "$1" | cut -d = -f 2)
                ;;
            --minor)
                echo "$minor"
                return
                ;;
            --minor=*)
                minor=$(echo "$1" | cut -d = -f 2)
                ;;
            --micro)
                echo "$micro"
                return
                ;;
            --micro=*)
                micro=$(echo "$1" | cut -d = -f 2)
                ;;
            --releaselevel)
                echo "$releaselevel"
                return
                ;;
            --releaselevel=*)
                releaselevel=$(echo "$1" | cut -d = -f 2)
                ;;
            --serial)
                echo "$serial"
                return
                ;;
            --serial=*)
                serial=$(echo "$1" | cut -d = -f 2)
                ;;
            *)
                echo "E: pep386_attr: bad argument: $1" >&2
                exit 1
                ;;
        esac
        shift
    done
    echo "$major.$minor.$micro.$releaselevel.$serial"
}


release_component() {
    if [ -z "$1" ]; then
        echo "E: release_component: missing argument: component_path" >&2
        exit 1
    fi
    # path to the top-level directory of the component relative to tree root
    local component_path=$1; shift

    # origin and target branches
    local origin=trunk
    local target=
    # clobber origin branch directly
    local in_place=

    # Component name and version
    local name=
    local version=
    
    # name of the component's top-level python package
    local component_python_package=
    # name of the component as it shows up in the commit message
    local component_commit_name=
    # name of the component as it shows up in tags
    local component_tag_name=
    # release policy (micro/minor/major/rc)
    local bump_version=
    local bump_level=
    # tagging policy (yes|no|auto*)
    local tagging_policy=

    # Parse keyword arguments
    while test -n "$1"; do
        if echo "$1" | grep -F -q =; then
            local value=$(echo $1 | cut -d = -f 2-)
        else
            local value=
        fi
        case "$1" in
            --origin=*)
                origin=$value
                ;;
            --target=*)
                target=$value
                ;;
            --in-place=*)
                in_place=$value
                ;;
            --python-pkg=*)
                component_python_package=$value
                ;;
            --tag-name=*)
                component_tag_name=$value
                ;;
            --commit-name=*)
                component_commit_name=$value
                ;;
            --bump-version=*)
                bump_version=$value
                ;;
            --bump-level=*)
                bump_level=$value
                ;;
            --tagging-policy=*)
                tagging_policy=$value
                ;;
            *)
                echo "E: release_component: bad argument: $1" >&2
                exit 1
                ;;
        esac
        shift
    done

    # Ensure that origin branch exist
    assert test -d "$origin"

    echo "I: inspecting component..."
    # Interrogate name/version
    name=$($origin/$component_path/setup.py --name)
    version=$($origin/$component_path/setup.py --version)
    echo "I: component is $name-$version"

    # set defaults for various name-like variabls
    test -z "$component_python_package" && component_python_package="$name"
    test -z "$component_commit_name" && component_commit_name="$name"
    test -z "$component_tag_name" && component_tag_name="$name"

    # Parse version
    local canonical_version=$(pep386_parse "$version")
    echo "D: canonical version is $canonical_version"

    # Compute next version (each release increments version)
    echo "D: computing next version..."
    echo "D: bump_level: $bump_level"
    echo "D: bump_version: $bump_version"
    local canonical_next_version=$canonical_version
    case "$bump_version" in
        major)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --major=$(expr $(pep386_attr "$canonical_next_version" --major) + 1) --minor=0 --micro=0) 
            ;;
        minor)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --minor=$(expr $(pep386_attr "$canonical_next_version" --minor) + 1) --micro=0) 
            ;;
        micro)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --micro=$(expr $(pep386_attr "$canonical_next_version" --micro) + 1)) 
            ;;
    esac
    case "$bump_level" in
        dev)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=dev --serial=0)
            ;;
        alpha)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=alpha --serial=1)
            ;;
        beta)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=beta --serial=1)
            ;;
        candidate)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=candidate --serial=1)
            ;;
        final)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=final --serial=0)
            ;;
        next-level)
            case "$(pep386_attr \"$canonical_version\" --releaselevel)" in
                dev)
                    canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=alpha --serial=1)
                    ;;
                alpha)
                    canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=beta --serial=1)
                    ;;
                beta)
                    canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=candidate --serial=1)
                    ;;
                candidate)
                    canonical_next_version=$(pep386_attr "$canonical_next_version" --releaselevel=final --serial=0)
                    ;;
            esac
            ;;
        next-serial)
            canonical_next_version=$(pep386_attr "$canonical_next_version" --serial=$(expr $(pep386_attr "$canonical_next_version" --serial) + 1)) 
            ;;
    esac
    echo "D: canonical next version is $canonical_next_version"
    local next_version=$(pep386_unparse "$canonical_next_version")
    echo "I: next version is $next_version"

    # Determine tag name and tagging policy
    local tag_name="$component_tag_name-v$next_version"
    local should_tag=
    case "$tagging_policy" in
        force)
            should_tag=yes
            echo "I: will apply tag: $tag_name (due to --tagging-policy=force)"
            ;;
        never)
            should_tag=no
            echo "I: will not apply tag (due to --tagging-policy=never)"
            ;;
        auto|'')
            case "$(pep386_attr \"$canonical_next_version\" --releaselevel)" in
                dev)
                    should_tag=no
                    echo "I: will not apply tag (dev version)"
                    ;;
                *)
                    should_tag=yes
                    echo "I: will apply tag: $tag_name"
                    ;;
            esac
            ;;
    esac

    if [ -z "$dry_run" ]; then
        if [ "$in_place" = "yes" ]; then
            target="$origin"
        elif [ -z "$target" ]; then
            target="$name-$next_version"
            echo "I: derived --target=$target from component name/version"
        fi
        if [ "$target" != "$origin" ]; then
            echo "I: branching $origin to ${target}..."
            assert test ! -e "$target"
            bzr branch "$origin" "$target" --quiet
        else
            echo "I: not branching $origin (will apply changes in-place)"
        fi
        # Apply the new version
        echo "I: patching setup.py..."
        sed -i -e "s!$version!$next_version!g" "$target/$component_path/setup.py"
        echo "I: patching $name/__init__.py..."
        sed -i -e "s!__version__ = \(.*\)!__version__ = $(pep386_as_tuple $canonical_next_version)!g" "$target/$component_path/$component_python_package/__init__.py"
        echo "I: commiting version bump"
        # NOTE: no way to bzr commit has no '-d'
        (cd $target && bzr commit -m "$component_commit_name: increment version to $next_version" --quiet)
        # Apply the tag if required
        if [ "$should_tag" = yes ]; then
            echo "I: applying tag: $tag_name"
            bzr tag -d "$target" "$tag_name" --quiet
        fi
        echo "I: component $name is ready in $target"
    else
        echo "I: not doing anything in dry-run mode"
    fi
}


help() {
    echo "usage: releasectl (checkbox|checkbox-ng|plainbox) [options]" >&2
    echo
    echo "This script is indented to run in a directory with lp:checkbox"
    echo "branched (with bzr) as 'trunk'. Every change made is local."
    echo
    echo "component:"
    echo "  checkbox        release checkbox-old"
    echo "  checkbox-ng     release checkbox-ng"
    echo "  plainbox        release plainbox"
    echo
    echo "general options"
    echo "  -n|--dry-run"
    echo "    don't prepare branch with version changes"
    echo
    echo "branch selection options"
    echo "  --origin=BRANCH"
    echo "    use BRANCH as origin branch (default=trunk)"
    echo "  --target=BRANCH"
    echo "    use BRANCH as target branch (default=derived from name and version)"
    echo "  --in-place"
    echo "    modify origin branch in-place (ignores --target)"
    echo
    echo "component version options"
    echo "  --bump-version={major,minor,micro}"
    echo "    increment version component"
    echo "  --major|--minor|--micro"
    echo "    same as --bump-version=..."
    echo
    echo "component release level options"
    echo "  --bump-level={dev,alpha,beta,candidate,final,next-level,next-serial}"
    echo "    set/increment release level and serial"
    echo "  --dev|--alpha|--beta|--candidate|--final"
    echo "    same as --bump-level=..."
    echo "  --rc"
    echo "    same as --candidate"
    echo 
    echo "NOTE: If any version change options are used without --bump-level"
    echo "      then the program behaves as if --bump-level=dev was passed"
    echo "NOTE: Using: --bump-level=next-level resets serial to 1"
    echo "      (except for final, where it must be 0)"
    echo "NOTE: Using: --bump-level=next-level never goes past final"
    echo
    echo "WARNING: Using: --bump-level=next-serial just increments the serial number"
    echo "      That is *incorrect* if current level is 'dev' or 'final'"
}


main() {
    local origin=
    local target=
    local in_place=no
    local component=
    local bump_version=
    local bump_level=
    local dry_run=
    # Parse keyword arguments
    while test -n "$1"; do
        if echo "$1" | grep -F -q =; then 
            local value=$(echo $1 | cut -d = -f 2-)
        else
            local value=
        fi
        case $1 in
            checkbox)
                component=checkbox
                ;;
            checkbox-ng)
                component=checkbox-ng
                ;;
            plainbox)
                component=plainbox
                ;;
            -n|--dry-run)
                dry_run=1
                ;;
            --origin=*)
                origin=$value
                ;;
            --target=*)
                target=$value
                ;;
            --in-place)
                in_place=yes
                ;;
            --bump-version=*)
                case $value in
                    major|minor|micro)
                        bump_version=$value
                        ;;
                    *)
                        echo "E: releasectl: bad value for --bump-version: $value" >&2
                        exit 1
                        ;;
                esac
                ;;
            --major)
                bump_version=major
                ;;
            --minor)
                bump_version=minor
                ;;
            --micro)
                bump_version=micro
                ;;
            --bump-level=*)
                case $value in
                    dev|alpha|beta|candidate|final|next-level|next-serial)
                        bump_level=$value
                        ;;
                    *)
                        echo "E: releasectl: bad value for --bump-level: $value" >&2
                        exit 1
                        ;;
                esac
                ;;
            --dev)
                bump_level=dev
                ;;
            --alpha)
                bump_level=alpha
                ;;
            --beta)
                bump_level=beta
                ;;
            --candidate|--rc)
                bump_level=candidate
                ;;
            --final)
                bump_level=final
                ;;
            --help)
                help
                exit 0
                ;;
            *)
                echo "E: releasectl: bad argument: $1" >&2
                exit 1
                ;;
        esac
        shift
    done
    if [ -z "$component" ]; then
        echo "E: releasectl: component name required"
        echo
        help
        exit 1
    fi
    if [ -z "$bump_level" ] && [ -z "$bump_version" ]; then
        echo "E: releasectl: expected at least one of --bump-level=... or --bump-version=..."
        echo
        help
        exit 1
    fi
    if [ -n "$bump_version" ] && [ -z "$bump_level" ]; then
        echo "I: assuming --bump-level=dev"
        bump_level=dev
    fi
    if [ -z "$origin" ]; then
        echo "W: assuming --origin=trunk"
        origin=trunk
    fi
    case "$component" in
        checkbox)
            release_component checkbox-old \
                --origin=$origin \
                --target=$target \
                --in-place=$in_place \
                --tag-name=checkbox \
                --commit-name=checkbox-old \
                --python-pkg=checkbox \
                --bump-version=$bump_version \
                --bump-level=$bump_level
            ;;
        checkbox-ng)
            release_component checkbox-ng \
                --origin=$origin \
                --target=$target \
                --in-place=$in_place \
                --python-pkg=checkbox_ng \
                --bump-version=$bump_version \
                --bump-level=$bump_level
            ;;
        plainbox)
            release_component plainbox \
                --origin=$origin \
                --target=$target \
                --in-place=$in_place \
                --bump-version=$bump_version \
                --bump-level=$bump_level
            ;;
    esac
}


main "$@"
