#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

if [ $# -eq 1 -a "$1" == --nutools-completion ]; then
    echo '
function __prel_completion() {
    local cur
    _get_comp_words_by_ref cur
    COMPREPLY=($(compgen -W "$(__prel_branches)" "$cur"))
}
complete -F __prel_completion prel
'
    exit 0
fi

source "$(dirname "$0")/lib/ulib/ulib" || exit 1
urequire DEFAULTS ptools

# XXX Ajouter une option pour fusionner les modifications d'une branche de
# pré-release dans develop

function display_help() {
    uecho "$scriptname: basculer sur une branche de release

USAGE
    $scriptname -u [SOURCE]
    $scriptname -c [RELEASE [SOURCE]]
    $scriptname -m|-l|-d [RELEASE]

- Vérifier s'il n'y a pas de modifications locales. Sinon, proposer de faire un
  commit ou un stash.
- Avec l'option -c, s'il existe une branche de release, proposer de basculer
  vers elle ou sur la branche master. Sinon, basculer sur la branche master.
- Avec l'option -u, proposer ou fixer une branche de release à créer. Si elle
  existe déjà, basculer vers elle. Sinon, la créer en la basant sur SOURCE, qui
  vaut par défaut develop

OPTIONS
    -C, --projdir PROJDIR
        Spécifier le répertoire de base du projet qui est dans git. Par défaut,
        on travaille dans le répertoire courant et on laisse git trouver le
        répertoire de base du projet. Avec cette option, le répertoire courant
        est modifié avant de lancer les commandes git.
    -O, --origin ORIGIN
        Spécifier le nom de l'origine. Par défaut, utiliser 'origin'
    -o, --offline
        En cas de création d'une branche, ne pas pousser vers l'origine; ne pas
        tenter le cas échéant de traquer la branche dans l'origine; ne pas
        supprimer une branche dans l'origine. Cette option est automatiquement
        activée si la variable UTOOLS_VCS_OFFLINE est définie.
    --online
        Annuler l'effet de la variable UTOOLS_VCS_OFFLINE: forcer le mode online

    -c, --checkout
        Basculer vers une branche de release existante. C'est l'option par
        défaut. Si aucune branche de release n'existe, basculer vers master
    -u, --update
        Préparer une nouvelle release. Utiliser une des options -x, -z ou -p
        pour spécifier le type de release à préparer. Si la branche qui serait
        créée pour le type de release existe déjà, basculer vers cette branche.
        S'il faut la créer, la baser sur la branche SOURCE, qui vaut par défaut
        develop
    --menu
    -x, --major
    -z, --minor
    -p, --patchlevel
        Utilisé avec l'option -u, soit afficher un menu pour choisir la version
        de la nouvelle release (par défaut), soit préparer respectivement une
        release majeure, mineure, ou pour correction de bug.
    -v-OPT
        Avec l'option -u, spécifier une option de pver permettant de choisir la
        version de la nouvelle release. Les options supportées sont -v, -l, -a,
        -b, -r et -R. Par exemple, si la version actuelle sur la branche master
        est 0.2.3, les options '-uz -v-lbeta' permettent de préparer la release
        0.3.0-beta
        En principe, cette option n'a pas à être utilisée, puisque dans une
        branche de release, on peut faire vivre les versions de pré-release
        jusqu'à la release finale. Ainsi, la branche de release est nommée
        d'après la version finale, mais le projet peut recevoir une version de
        pré-release incrémentale.
    -w, --write
        Si une nouvelle branche est créée avec -u, mettre à jour le fichier
        VERSION.txt avec pver. C'est l'option par défaut.
    -n, --no-write
        Si une nouvelle branche est créée avec -u, NE PAS mettre à jour le
        fichier VERSION.txt avec pver. Utiliser cette option si la mise à jour
        du numéro de version doit être faite d'une manière particulière.
    -e, --edit
        Editer le fichier CHANGES.md autogénéré par les options -u -w ou un
        fichier CHANGES.txt existant. Cette option est surtout utile si -m est
        utilisé avec -u, pour donner la possibilité de corriger la liste des
        modifications avant leur enregistrement définitif.

    -m, --merge
        Si la branche actuelle est une branche de release, ou s'il existe une
        branche de release, la merger dans master, puis dans develop, puis la
        supprimer. A l'issu de cette opération, rester sur la branche develop.
        S'il n'existe pas de branche de release, proposer de fusionner les
        modifications de la branche develop dans la branche master, sans
        préparer de branche de release au préalable.
    --delete
        Supprimer une branche de release, à condition qu'elle aie déjà été
        entièrement fusionnée dans la branch master
    --force-delete
        Supprimer une branche de release, même si elle n'a pas encore été
        fusionnée dans la branche master

    -s, --summary
        Afficher la liste des différences entre la branche develop et la branche
        master, comme elle serait générée par les options -u -w pour le fichier
        CHANGES.txt (la syntaxe pour CHANGES.md est légèrement différente)
    -l, --log
        Afficher les modifications actuellement effectuée dans la branche de
        release par rapport à develop.
    -d, --diff
        Afficher les modifications actuellement effectuée dans la branche de
        release par rapport à develop, sous forme de diff.

OPTIONS AVANCEES
    --uc, --upgrade-changes
        Convertir un fichier CHANGES.txt en CHANGES.md"
}

function show_summary() {
    git log --oneline --graph "$@" |
        grep -vF '|\' | grep -vF '|/' | sed 's/\* //; s/^  /+ /' |
        grep -v "Intégration de la branche release-" |
        grep -v "Branche develop en version .*-SNAPSHOT"
}

function format_md() {
    awk '
$0 == "" || $0 ~ /^#/ { print; next }
$1 == "+" {
  $1 = "*"
  $2 = "`" $2 "`"
  print; next
}
$1 == "|" {
  $1 = "  *"
  $2 = "`" $2 "`"
  print; next
}
{
  $1 = "* `" $1 "`"
  print; next
}
'
}

function __get_newver_from_release() {
    local relver filever
    is_release_branch "$release" && relver="${release#release-}"
    setx filever=pver --sw "" --allow-empty
    if [ -n "$relver" -a "$filever" != "$relver" ]; then
        newver="$filever"
        ewarn "La version de la branche de release est différente de la version dans le fichier"
        enote "La version effectivement sélectionnée est $newver"
    elif [ -n "$filever" ]; then
        newver="$filever"
    elif [ -n "$relver" ]; then
        newver="$relver"
    fi
}

projdir=
origin=origin
action=auto
checkout=
update=
incversion=
pver_opts=()
write=1
edit_changes=
merge=
force_delete=
log=
diff=
parse_opts "${PRETTYOPTS[@]}" \
    --help '$exit_with display_help' \
    -C:,--projdir: projdir= \
    -O:,--origin: origin= \
    -o,--offline UTOOLS_VCS_OFFLINE=1 \
    --online UTOOLS_VCS_OFFLINE= \
    -c,--checkout checkout=1 \
    -u,--update '$update=1; [ -z "$incversion" ] && incversion=auto' \
    --menu '$update=1; incversion=menu' \
    -x,--major '$update=1; incversion=major' \
    -z,--minor '$update=1; incversion=minor' \
    -p,--patchlevel '$update=1; incversion=patchlevel' \
    -v: '$update=1; add@ pver_opts' \
    -w,--write write=1 \
    -n,--no-write write= \
    -e,--edit edit_changes=1 \
    -m,--merge merge=1 \
    --delete action=delete \
    --force-delete '$action=delete; force_delete=1' \
    -s,--summary action=summary \
    -l,--log '$action=diff; log=1' \
    -d,--diff '$action=diff; diff=1' \
    --upgrade-changes,--uc action=upgrade-changes \
    @ args -- "$@" && set -- "${args[@]}" || die "$args"

if [ "$action" == auto ]; then
    if [ -n "$update" ]; then
        action=update
    elif [ -n "$merge" ]; then
        action=merge
    elif [ -n "$checkout" ]; then
        action=checkout
    else
        # checkout par défaut
        action=checkout
    fi
fi

if [ -n "$projdir" ]; then
    cd "$projdir" || die
fi

if [ "$action" == upgrade-changes ]; then
    [ -f CHANGES.txt ] || die "CHANGES.txt: fichier introuvable"
    format_md <CHANGES.txt >CHANGES.md || die
    rm CHANGES.txt
    enote "Conversion CHANGES.txt --> CHANGES.md effectuée"
    exit 0
fi

git_ensure_gitvcs

if [ -f .prel-noauto -a -n "$update" -a -n "$merge" ]; then
    die "Vous ne pouvez pas faire de release automatique sur ce dépôt. Vous devez utiliser -u et -m séparement."
fi

setx vertype=pver --sw "" --show-source
[[ "$vertype" == pom* ]] && maven_update=1 || maven_update=

push_branches=()
push_tags=()
push_deferred=
[ -n "$UTOOLS_VCS_OFFLINE" ] && push_deferred= || push_deferred=1
[ -n "$update" -a -n "$merge" ] && UTOOLS_VCS_OFFLINE=1

setx branch=git_get_branch

update_opt=
if [ "$action" == update ]; then
    git_ensure_cleancheckout

    if [ -n "$maven_update" ]; then
        setx oldver=pver --gw develop:
        enote "La version de la branche develop est $oldver"
    else
        setx oldver=pver --gw master:
    fi
    newver=

    if [ "$incversion" == auto ]; then
        if [ ${#pver_opts[*]} -gt 0 ]; then
            # des options ont été spécifiées, les honorer
            setx newver=pver -s "$oldver" ${maven_update:+--maven-update -R} "${pver_opts[@]}"
            release="release-$newver"
        else
            # sinon, prendre une décision en fonction des branches de release
            # qui existent déjà
            setx -a branches=list_release_branches "$origin"
            if [ ${#branches[*]} -eq 0 ]; then
                # en l'absence de branche de release, proposer d'en créer une
                incversion=menu
            elif [ ${#branches[*]} -eq 1 ]; then
                # s'il n'y en a qu'une, la prendre
                release="${branches[0]}"
            else
                # sinon, donner le choix dans un menu
                array_add branches master
                default_branch="$branch"
                array_contains branches "$default_branch" || default_branch="${branches[0]}"
                simple_menu release branches -d "$default_branch" \
                    -t "Basculer vers une branche de release" \
                    -m "Veuillez choisir la branche vers laquelle basculer"
            fi
        fi
    fi
    pver_opts=(${maven_update:+--maven-update -R} "${pver_opts[@]}")
    case "$incversion" in
    menu)
        setx majorv=pver -s "$oldver" -ux "${pver_opts[@]}"
        setx minorv=pver -s "$oldver" -uz "${pver_opts[@]}"
        setx patchlevelv=pver -s "$oldver" -up "${pver_opts[@]}"
        if [ -z "$maven_update" ]; then
            release="release-$minorv"
        else
            release="release-$patchlevelv"
        fi
        branches=("release-$majorv" "release-$minorv" "release-$patchlevelv" master)
        simple_menu release branches \
            -t "Basculer vers une nouvelle branche de release" \
            -m "Veuillez choisir la branche à créer"
        [ "$release" != master ] && newver="${release#release-}"
        if [ "$release" == "release-$majorv" ]; then
            update_opt=-x
        elif [ "$release" == "release-$minorv" ]; then
            update_opt=-z
        elif [ "$release" == "release-$patchlevelv" ]; then
            update_opt=-p
        fi
        ;;
    major)
        setx newver=pver -s "$oldver" -ux "${pver_opts[@]}"
        release="release-$newver"
        update_opt=-x
        ;;
    minor)
        setx newver=pver -s "$oldver" -uz "${pver_opts[@]}"
        release="release-$newver"
        update_opt=-z
        ;;
    patchlevel)
        setx newver=pver -s "$oldver" -up "${pver_opts[@]}"
        release="release-$newver"
        update_opt=-p
        ;;
    esac

    set -- "$release" "$1"
    action=checkout
fi

if [ "$action" == checkout ]; then
    release="$1"
    source="${2:-develop}"

    if [ -z "$release" ]; then
        setx -a branches=list_release_branches "$origin"

        if [ ${#branches[*]} -eq 0 ]; then
            # en l'absence de branche de release, basculer sur master
            release=master
        elif [ ${#branches[*]} -eq 1 ]; then
            # s'il n'y en a qu'une, la prendre
            release="${branches[0]}"
        else
            # sinon, donner le choix dans un menu
            array_add branches master
            default_branch="$branch"
            array_contains branches "$default_branch" || default_branch="${branches[0]}"
            simple_menu release branches -d "$default_branch" \
                -t "Basculer vers une branche de release" \
                -m "Veuillez choisir la branche vers laquelle basculer"
        fi
    fi

    # On est peut-être déjà sur la bonne branche
    if git_is_branch "$release"; then
        if [ -z "$UTOOLS_VCS_OFFLINE" ]; then
            git_track_branch "$release" "$origin"
        fi
        git_fast_forward "$release" "" "$origin"
        exit 0
    fi

    # Créer/basculer vers une release branch
    git_ensure_cleancheckout
    is_any_branch "$release" master release || die "$release: ce n'est pas une branche de release"
    r=0
    newbranch=
    if git_have_branch "$release"; then
        git checkout "$release"; r=$?
    elif git_have_rbranch "$release"; then
        git checkout "$release"; r=$?
    else
        estepn "\
Vous allez créer la nouvelle branche de release ${COULEUR_VERTE}$release${COULEUR_NORMALE}
à partir de la branche source ${COULEUR_BLEUE}$source${COULEUR_NORMALE}"
        ask_yesno "Voulez-vous continuer?" O || die

        newbranch=1
        git_ensure_branch "$release" "$source" "$origin"
        [ $? -eq 2 ] && die "Impossible de créer la branche $release. Veuillez vérifier que la branche $source existe"
        git checkout "$release"; r=$?

        if [ -z "$newver" ]; then
            # le cas échéant, tenter de calculer la version en fonction de la release
            __get_newver_from_release
        fi

        if [ "$r" -eq 0 -a -n "$write" ]; then
            set cwd=pwd
            setx workdir=git rev-parse --show-toplevel
            [ -d "$workdir" ] && cd "$workdir"

            commitmsg="Init changelog"
            if [ -n "$newver" ]; then
                pver -uv "$newver" && commitmsg="Init changelog & version $newver"
            fi

            version="$newver"
            [ -n "$version" ] || version="$release"

            changelog="## Version $version du $(date +%d/%m/%Y-%H:%M)"
            setx mergebase=git merge-base master "$release"
            tmpcmd=(show_summary "$mergebase..$release")
            if [ -f CHANGES.txt ]; then
                changes=CHANGES.txt
            else
                changes=CHANGES.md
                array_add tmpcmd // format_md
            fi
            setxp modifs "${tmpcmd[@]}"
            [ -n "$modifs" ] && changelog="$changelog

$modifs"

            ac_set_tmpfile tmpchanges
            echo "$changelog" >>"$tmpchanges"

            if [ -n "$edit_changes" ]; then
                estep "Lancement d'un éditeur pour vérifier la liste des modifications"
                "${EDITOR:-vi}" "$tmpchanges"
            fi

            if [ -f "$changes" ]; then
                echo >>"$tmpchanges"
                cat "$changes" >>"$tmpchanges"
            fi
            cat "$tmpchanges" >"$changes"
            ac_clean "$tmpchanges"

            git add -A
            git commit -m "$commitmsg"

            cd "$cwd"
        fi
    fi
    if [ "$r" -eq 0 ]; then
        # éventuellement fast-forwarder automatiquement
        git_fast_forward "$release" "" "$origin"
    fi

    set -- "$release"
    if [ "$r" -eq 0 -a -n "$merge" ]; then
        # mettre à jour la branche sur laquelle on se trouve
        setx branch=git_get_branch
        action=merge
    elif [ "$r" -eq 0 -a -n "$newbranch" ]; then
        action=push
        array_addu push_branches "$release"
    else
        exit "$r"
    fi
fi

setx -a branches=list_release_branches
setb have_release_branches=[ ${#branches[*]} -gt 0 ]

release="$1"
if [ -n "$release" ]; then
    if [ -n "$have_release_branches" ]; then
        is_release_branch "$release" || die "$release: ce n'est pas une branche de release"
    elif ! is_develop_branch "$release"; then
        die "Aucune branche de release n'existe, vous devez fusionner à partir de develop"
    fi
    git_have_branch "$release" || die "$release: branche introuvable"
elif is_release_branch "$branch"; then
    release="$branch"
fi

final_branch= # branche sur laquelle se placer après avoir fusionné la branche de release
if [ "$action" == merge ]; then
    confirm=

    if [ -z "$release" ]; then
        if [ ${#branches[*]} -eq 0 ]; then
            ewarn "Aucune branche de release n'a été préparée."
            ewarn "La branche develop sera fusionnée directement dans master."
            release=develop
            confirm=-y
        elif [ ${#branches[*]} -eq 1 ]; then
            release="${branches[0]}"
            estepn "Autosélection de $release"
        else
            default_release="$branch"
            array_contains branches "$default_release" || default_release="${branches[0]}"
            simple_menu release branches -d "$default_release" \
                -t "Choix de la branche de release" \
                -m "Veuillez choisir la branche"
        fi
    fi

    estepn "Intégration ${COULEUR_VERTE}$release${COULEUR_NORMALE} --> ${COULEUR_BLEUE}master${COULEUR_NORMALE}"
    ask_yesno $confirm "Voulez-vous continuer?" O || die

    git checkout master || die
    git merge "$release" -m "Intégration de la branche $release" --no-ff || die

    if [ -z "$newver" ]; then
        # le cas échéant, tenter de calculer la version en fonction de la release
        __get_newver_from_release
    fi

    if [ -n "$newver" ]; then
        estepn "Création du tag $newver"
        array_addu push_tags "$newver"
        git tag --force "$newver" || die
        if git_have_remote "$origin"; then
            if [ -z "$UTOOLS_VCS_OFFLINE" ]; then
                git push "$origin" tag "$newver"
            elif [ -z "$push_deferred" ]; then
                eimportant "Le tag $newver n'a pas été poussé vers l'origine.
Il faudra le faire manuellement avec la commande suivante:
    $(qvals git push "$origin" tag "$newver")
ou celle-ci pour pour pousser TOUS les tags:
    $(qvals git push "$origin" --tags)"
            fi
        fi
    fi

    if [ "$release" != develop ]; then
        estepn "Intégration ${COULEUR_VERTE}$release${COULEUR_NORMALE} --> ${COULEUR_BLEUE}develop${COULEUR_NORMALE}"
        git checkout develop
        git merge "$release" -m "Intégration de la branche $release" --no-ff || die

        if [ -n "$maven_update" ]; then
            [ -n "$update_opt" ] || update_opt=-p
            pver -u $update_opt -S
            git add -A
            git commit -m "Branche develop en version $(pver --show)" || die
        fi
    fi

    # mettre à jour la branche sur laquelle on se trouve
    git checkout master || die
    setx branch=git_get_branch

    action=delete
    final_branch=develop
fi

if [ "$action" == delete -a "$release" != develop ]; then
    if [ -z "$force_delete" ]; then
        # vérifier que la branche a été fusionnée
        git_is_merged "$release" master || die "Refus de supprimer la branche $release: elle n'a pas été fusionnée dans master"
        git_is_merged "$release" develop || die "Refus de supprimer la branche $release: elle n'a pas été fusionnée dans develop"
    fi

    if [ "$branch" == "$release" ]; then
        # si on est sur la branche en question, en sortir pour pouvoir la
        # supprimer
        git checkout master || die
    fi

    estepi "Suppression de la branche locale"
    git branch -D "$release"

    if git_have_remote "$origin"; then
        if [ -z "$UTOOLS_VCS_OFFLINE" ]; then
            estepi "Suppression de la branche distante"
            git push "$origin" ":$release"
        elif git_have_rbranch "$release" "$origin"; then
            eimportant "\
La branche $origin/$release n'a plus lieu d'être, mais la configuration actuelle interdit de la supprimer.
Veuillez le faire manuellement avec la commande suivante:
    $(qvals git push "$origin" ":$release")"
        fi
    fi

    action=push
    array_addu push_branches master
    array_addu push_branches develop
fi

if [ "$action" == push -a -n "$push_deferred" ]; then
    if git_have_remote "$origin"; then
        estepi "Mise à jour de l'origine"
        for tag in "${push_tags[@]}"; do
            git push "$origin" tag "$tag"
        done
        for branch in "${push_branches[@]}"; do
            setx rbranch=git_get_branch_rbranch "$branch" "$origin"
            rbranch="${rbranch#refs/remotes/$origin/}"
            git push "$origin" "$branch:$rbranch"
        done
    fi
fi

if [ "$action" == summary ]; then
    setx mergebase=git merge-base master develop
    show_summary "$mergebase..develop"

elif [ "$action" == diff ]; then
    if [ -n "$log" ]; then
        if [ -n "$release" ]; then
            git log ${diff:+-p} master.."$release"
        else
            git log ${diff:+-p} master..develop
        fi
    elif [ -n "$diff" ]; then
        if [ -n "$release" ]; then
            git diff master.."$release"
        else
            git diff master..develop
        fi
    fi
fi

# en fin de traitement, revenir le cas échéant sur $final_branch
if [ -n "$final_branch" ]; then
    git checkout "$final_branch" || die
fi