chiark / gitweb /
git-debpush: check_treesame: Show diffstat when there is a diff
[dgit.git] / git-debpush
1 #!/bin/bash
2
3 # git-debpush -- create & push a git tag with metadata for an ftp-master upload
4 #
5 # Copyright (C) 2019 Sean Whitton
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 set -e$DGIT_TEST_DEBPUSH_DEBUG
21 set -o pipefail
22
23 # DESIGN PRINCIPLES
24 #
25 # - do not invoke dgit, do anything involving any tarballs, no network
26 #   access except `git push` right at the end
27 #
28 # - do not look at the working tree, like `git push` `git tag`
29 #
30 # - we are always in split brain mode, because that fits this workflow,
31 #   and avoids pushes failing just because dgit in the intermediary
32 #   service wants to append commits
33 #
34 # - if there is no previous tag created by this script, require a quilt
35 #   mode; if there is a previous tag, and no quilt mode provided, assume
36 #   same quilt mode as in previous tag created by this script
37
38 # **** Helper functions and variables ****
39
40 us="$(basename $0)"
41 git_playtree_setup=git-playtree-setup ###substituted###
42 git_playtree_setup=${DEBPUSH_GIT_PLAYTREE_SETUP-$git_playtree_setup}
43
44 cleanup() {
45     if [ -d "$temp" ]; then
46         rm -rf "$temp"
47     fi
48 }
49
50 fail () {
51     echo >&2 "$us: $*";
52     exit 127;
53 }
54
55 badusage () {
56     fail "bad usage: $*";
57 }
58
59 get_file_from_ref () {
60     local path=$1
61
62     if git ls-tree --name-only -r "$branch" \
63             | grep -Eq "^$path$"; then
64         git cat-file blob $branch:$path
65     fi
66 }
67
68 failed_check=false
69 fail_check () {
70     local check=$1; shift
71     local check_is_forced=false
72
73     case ",$force," in
74         *",$check,"*) check_is_forced=true ;;
75     esac
76     if $force_all || $check_is_forced; then
77         echo >&2 "$us: warning: $* ('$check' check)"
78     else
79         echo >&2 "$us: $* ('$check' check)"
80         failed_check=true
81     fi
82 }
83
84 fail_check_upstream_nonidentical () {
85     fail_check upstream-nonidentical \
86  "the upstream source in tag $upstream_tag is not identical to the upstream source in $branch"
87 }
88
89 find_last_tag () {
90     local prefix=$1
91
92     set +o pipefail             # perl will SIGPIPE git-log(1) here
93     git log --pretty=format:'%D' --decorate=full "$branch" \
94         | perl -wne 'use Dpkg::Version;
95             @pieces = split /, /, $_;
96             @debian_tag_vs = sort { version_compare($b, $a) }
97                 map { m|tag: refs/tags/'"$prefix"'(.+)| ? $1 : () } @pieces;
98             if (@debian_tag_vs) { print "'"$prefix"'$debian_tag_vs[0]\n"; exit }'
99     set -o pipefail
100 }
101
102 check_treesame () {
103     local first=$1
104     local second=$2
105     shift 2
106
107     set +e
108     git diff --quiet --exit-code "$first".."$second" -- . "$@"
109     git_diff_rc=$?
110     set -e
111
112     # show the user what the difference was
113     if [ $git_diff_rc = 1 ]; then
114         git diff --compact-summary "$first".."$second" -- . "$@"
115     fi
116
117     if [ $git_diff_rc -le 1 ]; then
118         return $git_diff_rc
119     else
120         fail "'git diff' exited with unexpected code $git_diff_rc"
121     fi
122 }
123
124 # **** Parse command line ****
125
126 getopt=$(getopt -s bash -o 'nfu:' \
127               -l 'no-push,force::,branch:,remote:,distro:,upstream:,quilt:,gbp,dpm,\
128 baredebian,baredebian+git,baredebian+tarball' \
129               -n "$us" -- "$@")
130 eval "set - $getopt"
131 set -e$DGIT_TEST_DEBPUSH_DEBUG
132
133 git_tag_opts=()
134 pushing=true
135 force_all=false
136 force=""
137 distro=debian
138 quilt_mode=""
139 branch="HEAD"
140
141 while true; do
142     case "$1" in
143         '-n'|'--no-push') pushing=false;           shift;   continue ;;
144         '-u')             git_tag_opts+=(-u "$2"); shift 2; continue ;;
145         '-f')             force_all=true;          shift;   continue ;;
146         '--gbp')          quilt_mode='gbp';        shift;   continue ;;
147         '--dpm')          quilt_mode='dpm';        shift;   continue ;;
148         '--branch')       branch=$2;               shift 2; continue ;;
149         '--remote')       remote=$2;               shift 2; continue ;;
150         '--distro')       distro=$2;               shift 2; continue ;;
151         '--quilt')        quilt_mode=$2;           shift 2; continue ;;
152         '--upstream')     upstream_tag=$2;         shift 2; continue ;;
153
154         '--baredebian'|'--baredebian+git')
155             quilt_mode=baredebian;         shift; continue ;;
156         '--baredebian+tarball')
157             fail "--baredebian+tarball quilt mode not supported"
158             ;;
159
160         # we require the long form of the option to skip individual
161         # checks, not permitting `-f check`, to avoid problems if we
162         # later want to introduce positional args
163         '--force')
164             case "$2" in
165                 '')
166                     force_all=true                         ;;
167                 *)
168                     force="$force,$2"                      ;;
169             esac
170             shift 2; continue ;;
171
172         '--') shift; break ;;
173         *) badusage "unknown option $1" ;;
174     esac
175 done
176
177 if [ $# != 0 ]; then
178     badusage 'no positional arguments allowed'
179 fi
180
181 case "$quilt_mode" in
182     linear|auto|smash|nofix|gbp|dpm|unapplied|baredebian|'') ;;
183     baredebian+git) quilt_mode="baredebian" ;;
184     baredebian+tarball) fail "--baredebian+tarball quilt mode not supported" ;;
185     *) badusage "invalid quilt mode: $quilt_mode" ;;
186 esac
187
188 # **** Gather git information ****
189
190 remoteconfigs=()
191 to_push=()
192
193 # Maybe $branch is a symbolic ref.  If so, resolve it
194 branchref="$(git symbolic-ref -q $branch || test $? = 1)"
195 if [ "x$branchref" != "x" ]; then
196    branch="$branchref"
197 fi
198 # If $branch is the name of a branch but it does not start with
199 # 'refs/heads/', prepend 'refs/heads/', so that we can know later
200 # whether we are tagging a branch or some other kind of committish
201 case "$branch" in
202     refs/heads/*) ;;
203     *)
204         branchref="$(git for-each-ref --format='%(objectname)' \
205                          '[r]efs/heads/$branch')"
206         if [ "x$branchref" != "x" ]; then
207             branch="refs/heads/$branch"
208         fi
209         ;;
210 esac
211
212 # If our tag will point at a branch, push that branch, and add its
213 # pushRemote and remote to the things we'll check if the user didn't
214 # supply a remote
215 case "$branch" in
216     refs/heads/*)
217         b=${branch#refs/heads/}
218         to_push+=("$b")
219         remoteconfigs+=( branch.$b.pushRemote branch.$b.remote )
220         ;;
221 esac
222
223 # also check, if the branch does not have its own pushRemote or
224 # remote, whether there's a default push remote configured
225 remoteconfigs+=(remote.pushDefault)
226
227 if $pushing && [ "x$remote" = "x" ]; then
228     for c in "${remoteconfigs[@]}"; do
229         remote=$(git config "$c" || test $? = 1)
230         if [ "x$remote" != "x" ]; then break; fi
231     done
232     if [ "x$remote" = "x" ]; then
233         fail "pushing, but could not determine remote, so need --remote="
234     fi
235 fi
236
237 # **** Gather source package information ****
238
239 temp=$(mktemp -d)
240 trap cleanup EXIT
241 mkdir "$temp/debian"
242 git cat-file blob "$branch":debian/changelog >"$temp/debian/changelog"
243 version=$(cd $temp; dpkg-parsechangelog -SVersion)
244 source=$(cd $temp; dpkg-parsechangelog -SSource)
245 target=$(cd $temp; dpkg-parsechangelog -SDistribution)
246 rm -rf "$temp"
247 trap - EXIT
248
249 format="$(get_file_from_ref debian/source/format)"
250 case "$format" in
251     '3.0 (quilt)')  upstream=true ;;
252     '3.0 (native)') upstream=false ;;
253     '1.0'|'')
254         if get_file_from_ref debian/source/options | grep -q '^-sn *$'; then
255             upstream=false
256         elif get_file_from_ref debian/source/options | grep -q '^-sk *$'; then
257             upstream=true
258         else
259             fail 'please see "SOURCE FORMAT 1.0" in git-debpush(1)'
260         fi
261         ;;
262     *)
263         fail "unsupported debian/source/format $format"
264         ;;
265 esac
266
267 # **** Gather git history information ****
268
269 last_debian_tag=$(find_last_tag "debian/")
270 last_archive_tag=$(find_last_tag "archive/debian/")
271
272 upstream_info=""
273 if $upstream; then
274     if [ "x$upstream_tag" = x ]; then
275         upstream_tag=$(
276             set +e
277             git deborig --just-print --version="$version" \
278                            | head -n1
279             ps="${PIPESTATUS[*]}"
280             set -e
281             case "$ps" in
282                 "0 0"|"141 0") ;; # ok or SIGPIPE
283                 *" 0")
284                     echo >&2 \
285  "$us: git-deborig failed; maybe try $us --upstream=TAG"
286                     exit 0
287                     ;;
288                 *) exit 127; # presumably head will have complained
289             esac
290         )
291         if [ "x$upstream_tag" = x ]; then exit 127; fi
292     fi
293     upstream_committish=$(git rev-parse "refs/tags/${upstream_tag}"^{})
294     upstream_info=" upstream-tag=$upstream_tag upstream=$upstream_committish"
295     to_push+=("$upstream_tag")
296 fi
297
298 # **** Useful sanity checks ****
299
300 # ---- UNRELEASED suite
301
302 if [ "$target" = "UNRELEASED" ]; then
303     fail_check unreleased "UNRELEASED changelog"
304 fi
305
306 # ---- Pushing dgit view to maintainer view
307
308 if ! [ "x$last_debian_tag" = "x" ] && ! [ "x$last_archive_tag" = "x" ]; then
309     last_debian_tag_c=$(git rev-parse "$last_debian_tag"^{})
310     last_archive_tag_c=$(git rev-parse "$last_archive_tag"^{})
311     if ! [ "$last_debian_tag_c" = "$last_archive_tag_c" ] \
312             && git merge-base --is-ancestor \
313                    "$last_debian_tag" "$last_archive_tag"; then
314         fail_check dgit-view \
315 "looks like you might be trying to push the dgit view to the maintainer branch?"
316     fi
317 fi
318
319 # ---- Targeting different suite
320
321 if ! [ "x$last_debian_tag" = "x" ]; then
322     temp=$(mktemp -d)
323     trap cleanup EXIT
324     mkdir "$temp/debian"
325     git cat-file blob "$last_debian_tag":debian/changelog >"$temp/debian/changelog"
326     prev_target=$(cd $temp; dpkg-parsechangelog -SDistribution)
327     rm -rf "$temp"
328     trap - EXIT
329
330     if ! [ "$prev_target" = "$target" ] && ! [ "$target" = "UNRELEASED" ]; then
331         fail_check suite \
332 "last upload targeted $prev_target, now targeting $target; might be a mistake?"
333     fi
334 fi
335
336 # ---- Upstream tag is not ancestor of $branch
337
338 if ! [ "x$upstream_tag" = "x" ] \
339         && ! git merge-base --is-ancestor "$upstream_tag" "$branch" \
340         && ! [ "$quilt_mode" = "baredebian" ]; then
341     fail_check upstream-nonancestor \
342  "upstream tag $upstream_tag is not an ancestor of $branch; probably a mistake"
343 fi
344
345 # ---- Upstream tag tree nonidentical
346
347 case "$quilt_mode" in
348     gbp)
349         check_treesame "$upstream_tag" "$branch" ':!debian' ':!**.gitignore' \
350             || fail_check_upstream_nonidentical
351         ;;
352     unapplied)
353         check_treesame "$upstream_tag" "$branch" ':!debian' \
354             || fail_check_upstream_nonidentical
355         ;;
356 esac
357
358 # ---- Summary
359
360 if $failed_check; then
361     # We don't mention the --force=check options here as those are
362     # mainly for use by scripts, or when you already know what check
363     # is going to fail before you invoke git-debpush.  Keep the
364     # script's terminal output as simple as possible.  No "see the
365     # manpage"!
366     fail "some check(s) failed; you can pass --force to ignore them"
367 fi
368
369 # **** Create the git tag ****
370
371 # convert according to DEP-14 rules
372 git_version=$(echo $version | tr ':~' '%_' | sed 's/\.(?=\.|$|lock$)/.#/g')
373
374 debian_tag="$distro/$git_version"
375 to_push+=("$debian_tag")
376
377 # If the user didn't supply a quilt mode, look for it in a previous
378 # tag made by this script
379 if [ "x$quilt_mode" = "x" ] && [ "$format" = "3.0 (quilt)" ]; then
380     set +o pipefail             # perl will SIGPIPE git-cat-file(1) here
381     if [ "x$last_debian_tag" != "x" ]; then
382         quilt_mode=$(git cat-file -p $(git rev-parse "$last_debian_tag") \
383                          | perl -wne \
384                                 'm/^\[dgit.*--quilt=([a-z+]+).*\]$/;
385                                  if ($1) { print "$1\n"; exit }')
386     fi
387     set -o pipefail
388 fi
389
390 quilt_mode_text=""
391 if [ "$format" = "3.0 (quilt)" ]; then
392     if [ "x$quilt_mode" = "x" ]; then
393         echo >&2 "$us: could not determine the git branch layout"
394         echo >&2 "$us: please supply a --quilt= argument"
395         exit 1
396     else
397         quilt_mode_text=" --quilt=$quilt_mode"
398     fi
399 fi
400
401 tagmessage="$source release $version for $target
402
403 [dgit distro=$distro split$quilt_mode_text]
404 [dgit please-upload$upstream_info]
405 "
406
407 git tag "${git_tag_opts[@]}" -s -m "$tagmessage" "$debian_tag" "$branch"
408
409 # **** Do a git push ****
410
411 if $pushing; then
412     git push "$remote" "${to_push[@]}"
413 fi