#!/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 .
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)"
git_playtree_setup=git-playtree-setup ###substituted###
git_playtree_setup=${DEBPUSH_GIT_PLAYTREE_SETUP-$git_playtree_setup}
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 () {
local check=$1; shift
local check_is_forced=false
case ",$force," in
*",$check,"*) check_is_forced=true ;;
esac
if $force_all || $check_is_forced; then
echo >&2 "$us: warning: $* ('$check' check)"
else
echo >&2 "$us: $* ('$check' check)"
failed_check=true
fi
}
fail_check_upstream_nonidentical () {
fail_check upstream-nonidentical \
"the upstream source in tag $upstream_tag is not identical to the upstream source in $branch"
}
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
}
check_treesame () {
local first=$1
local second=$2
shift 2
set +e
git diff --quiet --exit-code "$first".."$second" -- . "$@"
git_diff_rc=$?
set -e
# show the user what the difference was
if [ $git_diff_rc = 1 ]; then
git diff --compact-summary "$first".."$second" -- . "$@"
fi
if [ $git_diff_rc -le 1 ]; then
return $git_diff_rc
else
fail "'git diff' exited with unexpected code $git_diff_rc"
fi
}
check_patches_apply () {
local should_match_branch="$1"
local playground="$(git rev-parse --git-dir)/gdp"
local playtree="$playground/apply-patches"
local git_apply_rc=0
rm -rf "$playground"
mkdir -p "$playtree"
local pwd="$(pwd)"
cd "$playtree"
"$git_playtree_setup" .
# checking out the upstream source and then d/patches on top
# ensures this check will work for a variety of quilt modes
git checkout -b upstream "$upstream_committish"
git checkout "$branch_commit" -- debian
if [ -s "debian/patches/series" ]; then
while read patch; do
shopt -s extglob; patch="${patch%%?( )#*}"; shopt -u extglob
if [ -z "$patch" ]; then continue; fi
set +e
git apply --index "debian/patches/$patch"
git_apply_rc=$?
set -e
if ! [ $git_apply_rc = 0 ]; then
fail_check patches-applicable \
"'git apply' failed to apply patch $patch"
break
fi
done /dev/null; then
fail_check detached \
"HEAD is detached; you probably don't want to debpush it"
fi
# **** 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
# resolve $branch to a commit
branch_commit="$(git rev-parse --verify ${branch}^{commit})"
# 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 -q '^-sn *$'; then
upstream=false
elif get_file_from_ref debian/source/options | grep -q '^-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 ****
# ---- UNRELEASED suite
if [ "$target" = "UNRELEASED" ]; then
fail_check unreleased "UNRELEASED changelog"
fi
# ---- Pushing dgit view to maintainer view
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 dgit-view \
"looks like you might be trying to push the dgit view to the maintainer branch?"
fi
fi
# ---- Targeting different suite
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 suite \
"last upload targeted $prev_target, now targeting $target; might be a mistake?"
fi
fi
# ---- Upstream tag is not ancestor of $branch
if ! [ "x$upstream_tag" = "x" ] \
&& ! git merge-base --is-ancestor "$upstream_tag" "$branch" \
&& ! [ "$quilt_mode" = "baredebian" ]; then
fail_check upstream-nonancestor \
"upstream tag $upstream_tag is not an ancestor of $branch; probably a mistake"
fi
# ---- Quilt mode-specific checks
case "$quilt_mode" in
gbp)
check_treesame "$upstream_tag" "$branch" ':!debian' ':!**.gitignore' \
|| fail_check_upstream_nonidentical
check_patches_apply false
;;
unapplied)
check_treesame "$upstream_tag" "$branch" ':!debian' \
|| fail_check_upstream_nonidentical
check_patches_apply false
;;
baredebian)
check_patches_apply false
;;
dpm|nofix)
check_patches_apply true
;;
esac
# ---- git-debrebase branch format checks
# only check branches, since you can't run `git debrebase conclude` on
# non-branches
case "$branch" in
refs/heads/*)
# see "STITCHING, PSEUDO-MERGES, FFQ RECORD" in git-debrebase(5)
ffq_prev_ref="refs/ffq-prev/${branch#refs/}"
if git show-ref --quiet --verify "$ffq_prev_ref"; then
fail_check unstitched \
"this looks like an unstitched git-debrebase branch, which should not be pushed"
fi
esac
# ---- Summary
if $failed_check; then
# We don't mention the --force=check options here as those are
# mainly for use by scripts, or when you already know what check
# is going to fail before you invoke git-debpush. Keep the
# script's terminal output as simple as possible. No "see the
# manpage"!
fail "some check(s) failed; you can pass --force to ignore them"
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
tagmessage="$source release $version for $target
[dgit distro=$distro split$quilt_mode_text]
[dgit please-upload$upstream_info]
"
git tag "${git_tag_opts[@]}" -s -m "$tagmessage" "$debian_tag" "$branch"
# **** Do a git push ****
if $pushing; then
git push "$remote" "${to_push[@]}"
fi