#!/bin/bash

# git-debpush -- create & push a git tag with metadata for an ftp-master upload
#
# Copyright (C) 2019 Sean Whitton
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

set -e$DGIT_TEST_DEBPUSH_DEBUG
set -o pipefail

# DESIGN PRINCIPLES
#
# - do not invoke dgit, do anything involving any tarballs, no network
#   access except `git push` right at the end
#
# - do not look at the working tree, like `git push` `git tag`
#
# - we are always in split brain mode, because that fits this workflow,
#   and avoids pushes failing just because dgit in the intermediary
#   service wants to append commits
#
# - if there is no previous tag created by this script, require a quilt
#   mode; if there is a previous tag, and no quilt mode provided, assume
#   same quilt mode as in previous tag created by this script

# ---- Helper functions and variables

us="$(basename $0)"

cleanup() {
    if [ -d "$temp" ]; then
        rm -rf "$temp"
    fi
}

fail () {
    echo >&2 "$us: $*";
    exit 127;
}

badusage () {
    fail "bad usage: $*";
}

get_file_from_ref () {
    local path=$1

    if git ls-tree --name-only -r "$branch" \
            | grep -Eq "^$path$"; then
        git cat-file blob $branch:$path
    fi
}

failed_check=false
fail_check () {
    if $force; then
        echo >&2 "$us: warning: $*"
    else
        echo >&2 "$us: $*"
        failed_check=true
    fi
}

find_last_tag () {
    local prefix=$1

    set +o pipefail             # perl will SIGPIPE git-log(1) here
    git log --pretty=format:'%D' --decorate=full "$branch" \
        | perl -wne 'use Dpkg::Version;
            @pieces = split /, /, $_;
            @debian_tag_vs = sort { version_compare($b, $a) }
                map { m|tag: refs/tags/'"$prefix"'(.+)| ? $1 : () } @pieces;
            if (@debian_tag_vs) { print "'"$prefix"'$debian_tag_vs[0]\n"; exit }'
    set -o pipefail
}

# ---- Parse command line

getopt=$(getopt -s bash -o 'nfu:' \
              -l 'no-push,force,branch:,remote:,distro:,upstream:,quilt:,gbp,dpm,\
baredebian,baredebian+git,baredebian+tarball' \
              -n "$us" -- "$@")
eval "set - $getopt"
set -e$DGIT_TEST_DEBPUSH_DEBUG

git_tag_opts=()
pushing=true
force=false
distro=debian
quilt_mode=""
branch="HEAD"

while true; do
    case "$1" in
        '-n'|'--no-push') pushing=false;           shift;   continue ;;
	'-u')             git_tag_opts+=(-u "$2"); shift 2; continue ;;
        '-f'|'--force')   force=true;              shift;   continue ;;
        '--gbp')          quilt_mode='gbp';        shift;   continue ;;
        '--dpm')          quilt_mode='dpm';        shift;   continue ;;
        '--branch')       branch=$2;               shift 2; continue ;;
        '--remote')       remote=$2;               shift 2; continue ;;
        '--distro')       distro=$2;               shift 2; continue ;;
        '--quilt')        quilt_mode=$2;           shift 2; continue ;;
        '--upstream')     upstream_tag=$2;         shift 2; continue ;;

        '--baredebian'|'--baredebian+git')
            quilt_mode=baredebian;         shift; continue ;;
        '--baredebian+tarball')
            fail "--baredebian+tarball quilt mode not supported"
            ;;

        '--') shift; break ;;
	*) badusage "unknown option $1" ;;
    esac
done

if [ $# != 0 ]; then
    badusage 'no positional arguments allowed'
fi

case "$quilt_mode" in
    linear|auto|smash|nofix|gbp|dpm|unapplied|baredebian|'') ;;
    baredebian+git) quilt_mode="baredebian" ;;
    baredebian+tarball) fail "--baredebian+tarball quilt mode not supported" ;;
    *) badusage "invalid quilt mode: $quilt_mode" ;;
esac

# ---- Gather git information

remoteconfigs=()
to_push=()

# Maybe $branch is a symbolic ref.  If so, resolve it
branchref="$(git symbolic-ref -q $branch || test $? = 1)"
if [ "x$branchref" != "x" ]; then
   branch="$branchref"
fi
# If $branch is the name of a branch but it does not start with
# 'refs/heads/', prepend 'refs/heads/', so that we can know later
# whether we are tagging a branch or some other kind of committish
case "$branch" in
    refs/heads/*) ;;
    *)
        branchref="$(git for-each-ref --format='%(objectname)' \
                         '[r]efs/heads/$branch')"
        if [ "x$branchref" != "x" ]; then
            branch="refs/heads/$branch"
        fi
        ;;
esac

# If our tag will point at a branch, push that branch, and add its
# pushRemote and remote to the things we'll check if the user didn't
# supply a remote
case "$branch" in
    refs/heads/*)
        b=${branch#refs/heads/}
        to_push+=("$b")
        remoteconfigs+=( branch.$b.pushRemote branch.$b.remote )
        ;;
esac

# also check, if the branch does not have its own pushRemote or
# remote, whether there's a default push remote configured
remoteconfigs+=(remote.pushDefault)

if $pushing && [ "x$remote" = "x" ]; then
    for c in "${remoteconfigs[@]}"; do
	remote=$(git config "$c" || test $? = 1)
	if [ "x$remote" != "x" ]; then break; fi
    done
    if [ "x$remote" = "x" ]; then
	fail "pushing, but could not determine remote, so need --remote="
    fi
fi

# ---- Gather source package information

temp=$(mktemp -d)
trap cleanup EXIT
mkdir "$temp/debian"
git cat-file blob "$branch":debian/changelog >"$temp/debian/changelog"
version=$(cd $temp; dpkg-parsechangelog -SVersion)
source=$(cd $temp; dpkg-parsechangelog -SSource)
target=$(cd $temp; dpkg-parsechangelog -SDistribution)
rm -rf "$temp"
trap - EXIT

format="$(get_file_from_ref debian/source/format)"
case "$format" in
    '3.0 (quilt)')  upstream=true ;;
    '3.0 (native)') upstream=false ;;
    '1.0'|'')
	if get_file_from_ref debian/source/options | grep '^-sn *$'; then
	    upstream=false
        elif get_file_from_ref debian/source/options | grep '^-sk *$'; then
	    upstream=true
	else
	    fail 'please see "SOURCE FORMAT 1.0" in git-debpush(1)'
	fi
	;;
    *)
	fail "unsupported debian/source/format $format"
	;;
esac

# ---- Gather git history information

last_debian_tag=$(find_last_tag "debian/")
last_archive_tag=$(find_last_tag "archive/debian/")

upstream_info=""
if $upstream; then
    if [ "x$upstream_tag" = x ]; then
	upstream_tag=$(
	    set +e
	    git deborig --just-print --version="$version" \
			   | head -n1
	    ps="${PIPESTATUS[*]}"
	    set -e
	    case "$ps" in
		"0 0"|"141 0") ;; # ok or SIGPIPE
		*" 0")
		    echo >&2 \
 "$us: git-deborig failed; maybe try $us --upstream=TAG"
		    exit 0
		    ;;
		*) exit 127; # presumably head will have complained
	    esac
	)
	if [ "x$upstream_tag" = x ]; then exit 127; fi
    fi
    upstream_committish=$(git rev-parse "refs/tags/${upstream_tag}"^{})
    upstream_info=" upstream-tag=$upstream_tag upstream=$upstream_committish"
    to_push+=("$upstream_tag")
fi

# ---- Useful sanity checks

if [ "$target" = "UNRELEASED" ]; then
    fail_check "UNRELEASED changelog"
fi

if ! [ "x$last_debian_tag" = "x" ] && ! [ "x$last_archive_tag" = "x" ]; then
    last_debian_tag_c=$(git rev-parse "$last_debian_tag"^{})
    last_archive_tag_c=$(git rev-parse "$last_archive_tag"^{})
    if ! [ "$last_debian_tag_c" = "$last_archive_tag_c" ] \
            && git merge-base --is-ancestor \
                   "$last_debian_tag" "$last_archive_tag"; then
        fail_check \
"looks like you might be trying to push the dgit view to the maintainer branch?"
    fi
fi

if ! [ "x$last_debian_tag" = "x" ]; then
    temp=$(mktemp -d)
    trap cleanup EXIT
    mkdir "$temp/debian"
    git cat-file blob "$last_debian_tag":debian/changelog >"$temp/debian/changelog"
    prev_target=$(cd $temp; dpkg-parsechangelog -SDistribution)
    rm -rf "$temp"
    trap - EXIT

    if ! [ "$prev_target" = "$target" ] && ! [ "$target" = "UNRELEASED" ]; then
        fail_check \
"last upload targeted $prev_target, now targeting $target; might be a mistake?"
    fi
fi

if ! [ "x$upstream_tag" = "x" ] \
        && ! git merge-base --is-ancestor "$upstream_tag" "$branch" \
        && ! [ "$quilt_mode" = "baredebian" ]; then
    fail_check \
 "upstream tag $upstream_tag is not an ancestor of $branch; probably a mistake"
fi


if ! $force && $failed_check; then
    fail "some checks failed; you can override with --force"
fi

# ---- Create the git tag

# convert according to DEP-14 rules
git_version=$(echo $version | tr ':~' '%_' | sed 's/\.(?=\.|$|lock$)/.#/g')

debian_tag="$distro/$git_version"
to_push+=("$debian_tag")

# If the user didn't supply a quilt mode, look for it in a previous
# tag made by this script
if [ "x$quilt_mode" = "x" ] && [ "$format" = "3.0 (quilt)" ]; then
    set +o pipefail             # perl will SIGPIPE git-cat-file(1) here
    if [ "x$last_debian_tag" != "x" ]; then
        quilt_mode=$(git cat-file -p $(git rev-parse "$last_debian_tag") \
                         | perl -wne \
                                'm/^\[dgit.*--quilt=([a-z+]+).*\]$/;
                                 if ($1) { print "$1\n"; exit }')
    fi
    set -o pipefail
fi

quilt_mode_text=""
if [ "$format" = "3.0 (quilt)" ]; then
    if [ "x$quilt_mode" = "x" ]; then
        echo >&2 "$us: could not determine the git branch layout"
        echo >&2 "$us: please supply a --quilt= argument"
        exit 1
    else
        quilt_mode_text=" --quilt=$quilt_mode"
    fi
fi

git tag "${git_tag_opts[@]}" -s -F- "$debian_tag" "$branch" <<EOF
$source release $version for $target

[dgit distro=$distro split$quilt_mode_text]
[dgit please-upload$upstream_info]
EOF

# ---- Do a git push

if $pushing; then
    git push "$remote" "${to_push[@]}"
fi
