chiark / gitweb /
git-debpush: avoid a pipefail problem in get_file_from_ref
[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     # redirect to /dev/null instead of using `grep -Eq` to avoid grep
63     # SIGPIPEing git-ls-tree
64     if git ls-tree --name-only -r "$branch" \
65             | grep -E "^$path$" >/dev/null; then
66         git cat-file blob $branch:$path
67     fi
68 }
69
70 failed_check=false
71 fail_check () {
72     local check=$1; shift
73     local check_is_forced=false
74
75     case ",$force," in
76         *",$check,"*) check_is_forced=true ;;
77     esac
78     if $force_all || $check_is_forced; then
79         echo >&2 "$us: warning: $* ('$check' check)"
80     else
81         echo >&2 "$us: $* ('$check' check)"
82         failed_check=true
83     fi
84 }
85
86 fail_check_upstream_nonidentical () {
87     fail_check upstream-nonidentical \
88  "the upstream source in tag $upstream_tag is not identical to the upstream source in $branch"
89 }
90
91 find_last_tag () {
92     local prefix=$1
93
94     set +o pipefail             # perl will SIGPIPE git-log(1) here
95     git log --pretty=format:'%D' --decorate=full "$branch" \
96         | perl -wne 'use Dpkg::Version;
97             @pieces = split /, /, $_;
98             @debian_tag_vs = sort { version_compare($b, $a) }
99                 map { m|tag: refs/tags/'"$prefix"'(.+)| ? $1 : () } @pieces;
100             if (@debian_tag_vs) { print "'"$prefix"'$debian_tag_vs[0]\n"; exit }'
101     set -o pipefail
102 }
103
104 check_treesame () {
105     local first=$1
106     local second=$2
107     shift 2
108
109     set +e
110     git diff --quiet --exit-code "$first".."$second" -- . "$@"
111     git_diff_rc=$?
112     set -e
113
114     # show the user what the difference was
115     if [ $git_diff_rc = 1 ]; then
116         git diff --compact-summary "$first".."$second" -- . "$@"
117     fi
118
119     if [ $git_diff_rc -le 1 ]; then
120         return $git_diff_rc
121     else
122         fail "'git diff' exited with unexpected code $git_diff_rc"
123     fi
124 }
125
126 check_patches_apply () {
127     local should_match_branch="$1"
128
129     local playground="$(git rev-parse --git-dir)/gdp"
130     local playtree="$playground/apply-patches"
131     local git_apply_rc=0
132
133     rm -rf "$playground"
134     mkdir -p "$playtree"
135     local pwd="$(pwd)"
136     cd "$playtree"
137     "$git_playtree_setup" .
138
139     # checking out the upstream source and then d/patches on top
140     # ensures this check will work for a variety of quilt modes
141     git checkout -b upstream "$upstream_committish"
142     git checkout "$branch_commit" -- debian
143
144     if [ -s "debian/patches/series" ]; then
145         while read patch; do
146             shopt -s extglob; patch="${patch%%?( )#*}"; shopt -u extglob
147             if [ -z "$patch" ]; then continue; fi
148             set +e
149             git apply --index "debian/patches/$patch"
150             git_apply_rc=$?
151             set -e
152             if ! [ $git_apply_rc = 0 ]; then
153                 fail_check patches-applicable \
154                            "'git apply' failed to apply patch $patch"
155                 break
156             fi
157         done <debian/patches/series
158
159         if $should_match_branch && [ $git_apply_rc = 0 ]; then
160             git commit -q -a -m"commit result of applying all patches"
161             check_treesame HEAD "$branch_commit" ':!debian' \
162                 || fail_check patches-applicable \
163                               "applying all patches does not yield $branch"
164         fi
165     fi
166
167     cd "$pwd"
168     rm -rf "$playground"
169 }
170
171 # **** Parse command line ****
172
173 getopt=$(getopt -s bash -o 'nfu:' \
174               -l 'no-push,force::,branch:,remote:,distro:,upstream:,quilt:,gbp,dpm,\
175 baredebian,baredebian+git,baredebian+tarball' \
176               -n "$us" -- "$@")
177 eval "set - $getopt"
178 set -e$DGIT_TEST_DEBPUSH_DEBUG
179
180 git_tag_opts=()
181 pushing=true
182 force_all=false
183 force=""
184 distro=debian
185 quilt_mode=""
186 branch="HEAD"
187
188 while true; do
189     case "$1" in
190         '-n'|'--no-push') pushing=false;           shift;   continue ;;
191         '-u')             git_tag_opts+=(-u "$2"); shift 2; continue ;;
192         '-f')             force_all=true;          shift;   continue ;;
193         '--gbp')          quilt_mode='gbp';        shift;   continue ;;
194         '--dpm')          quilt_mode='dpm';        shift;   continue ;;
195         '--branch')       branch=$2;               shift 2; continue ;;
196         '--remote')       remote=$2;               shift 2; continue ;;
197         '--distro')       distro=$2;               shift 2; continue ;;
198         '--quilt')        quilt_mode=$2;           shift 2; continue ;;
199         '--upstream')     upstream_tag=$2;         shift 2; continue ;;
200
201         '--baredebian'|'--baredebian+git')
202             quilt_mode=baredebian;         shift; continue ;;
203         '--baredebian+tarball')
204             fail "--baredebian+tarball quilt mode not supported"
205             ;;
206
207         # we require the long form of the option to skip individual
208         # checks, not permitting `-f check`, to avoid problems if we
209         # later want to introduce positional args
210         '--force')
211             case "$2" in
212                 '')
213                     force_all=true                         ;;
214                 *)
215                     force="$force,$2"                      ;;
216             esac
217             shift 2; continue ;;
218
219         '--') shift; break ;;
220         *) badusage "unknown option $1" ;;
221     esac
222 done
223
224 if [ $# != 0 ]; then
225     badusage 'no positional arguments allowed'
226 fi
227
228 case "$quilt_mode" in
229     linear|auto|smash|nofix|gbp|dpm|unapplied|baredebian|'') ;;
230     baredebian+git) quilt_mode="baredebian" ;;
231     baredebian+tarball) fail "--baredebian+tarball quilt mode not supported" ;;
232     *) badusage "invalid quilt mode: $quilt_mode" ;;
233 esac
234
235 # **** Early sanity check ****
236
237 if [ "$branch" = "HEAD" ] \
238        && ! git symbolic-ref --quiet HEAD >/dev/null; then
239     fail_check detached \
240                "HEAD is detached; you probably don't want to debpush it"
241 fi
242
243 # **** Gather git information ****
244
245 remoteconfigs=()
246 to_push=()
247
248 # Maybe $branch is a symbolic ref.  If so, resolve it
249 branchref="$(git symbolic-ref -q $branch || test $? = 1)"
250 if [ "x$branchref" != "x" ]; then
251    branch="$branchref"
252 fi
253 # If $branch is the name of a branch but it does not start with
254 # 'refs/heads/', prepend 'refs/heads/', so that we can know later
255 # whether we are tagging a branch or some other kind of committish
256 case "$branch" in
257     refs/heads/*) ;;
258     *)
259         branchref="$(git for-each-ref --format='%(objectname)' \
260                          '[r]efs/heads/$branch')"
261         if [ "x$branchref" != "x" ]; then
262             branch="refs/heads/$branch"
263         fi
264         ;;
265 esac
266
267 # If our tag will point at a branch, push that branch, and add its
268 # pushRemote and remote to the things we'll check if the user didn't
269 # supply a remote
270 case "$branch" in
271     refs/heads/*)
272         b=${branch#refs/heads/}
273         to_push+=("$b")
274         remoteconfigs+=( branch.$b.pushRemote branch.$b.remote )
275         ;;
276 esac
277
278 # resolve $branch to a commit
279 branch_commit="$(git rev-parse --verify ${branch}^{commit})"
280
281 # also check, if the branch does not have its own pushRemote or
282 # remote, whether there's a default push remote configured
283 remoteconfigs+=(remote.pushDefault)
284
285 if $pushing && [ "x$remote" = "x" ]; then
286     for c in "${remoteconfigs[@]}"; do
287         remote=$(git config "$c" || test $? = 1)
288         if [ "x$remote" != "x" ]; then break; fi
289     done
290     if [ "x$remote" = "x" ]; then
291         fail "pushing, but could not determine remote, so need --remote="
292     fi
293 fi
294
295 # **** Gather source package information ****
296
297 temp=$(mktemp -d)
298 trap cleanup EXIT
299 mkdir "$temp/debian"
300 git cat-file blob "$branch":debian/changelog >"$temp/debian/changelog"
301 version=$(cd $temp; dpkg-parsechangelog -SVersion)
302 source=$(cd $temp; dpkg-parsechangelog -SSource)
303 target=$(cd $temp; dpkg-parsechangelog -SDistribution)
304 rm -rf "$temp"
305 trap - EXIT
306
307 format="$(get_file_from_ref debian/source/format)"
308 case "$format" in
309     '3.0 (quilt)')  upstream=true ;;
310     '3.0 (native)') upstream=false ;;
311     '1.0'|'')
312         if get_file_from_ref debian/source/options | grep -q '^-sn *$'; then
313             upstream=false
314         elif get_file_from_ref debian/source/options | grep -q '^-sk *$'; then
315             upstream=true
316         else
317             fail 'please see "SOURCE FORMAT 1.0" in git-debpush(1)'
318         fi
319         ;;
320     *)
321         fail "unsupported debian/source/format $format"
322         ;;
323 esac
324
325 # **** Gather git history information ****
326
327 last_debian_tag=$(find_last_tag "debian/")
328 last_archive_tag=$(find_last_tag "archive/debian/")
329
330 upstream_info=""
331 if $upstream; then
332     if [ "x$upstream_tag" = x ]; then
333         upstream_tag=$(
334             set +e
335             git deborig --just-print --version="$version" \
336                            | head -n1
337             ps="${PIPESTATUS[*]}"
338             set -e
339             case "$ps" in
340                 "0 0"|"141 0") ;; # ok or SIGPIPE
341                 *" 0")
342                     echo >&2 \
343  "$us: git-deborig failed; maybe try $us --upstream=TAG"
344                     exit 0
345                     ;;
346                 *) exit 127; # presumably head will have complained
347             esac
348         )
349         if [ "x$upstream_tag" = x ]; then exit 127; fi
350     fi
351     upstream_committish=$(git rev-parse "refs/tags/${upstream_tag}"^{})
352     upstream_info=" upstream-tag=$upstream_tag upstream=$upstream_committish"
353     to_push+=("$upstream_tag")
354 fi
355
356 # **** Useful sanity checks ****
357
358 # ---- UNRELEASED suite
359
360 if [ "$target" = "UNRELEASED" ]; then
361     fail_check unreleased "UNRELEASED changelog"
362 fi
363
364 # ---- Pushing dgit view to maintainer view
365
366 if ! [ "x$last_debian_tag" = "x" ] && ! [ "x$last_archive_tag" = "x" ]; then
367     last_debian_tag_c=$(git rev-parse "$last_debian_tag"^{})
368     last_archive_tag_c=$(git rev-parse "$last_archive_tag"^{})
369     if ! [ "$last_debian_tag_c" = "$last_archive_tag_c" ] \
370             && git merge-base --is-ancestor \
371                    "$last_debian_tag" "$last_archive_tag"; then
372         fail_check dgit-view \
373 "looks like you might be trying to push the dgit view to the maintainer branch?"
374     fi
375 fi
376
377 # ---- Targeting different suite
378
379 if ! [ "x$last_debian_tag" = "x" ]; then
380     temp=$(mktemp -d)
381     trap cleanup EXIT
382     mkdir "$temp/debian"
383     git cat-file blob "$last_debian_tag":debian/changelog >"$temp/debian/changelog"
384     prev_target=$(cd $temp; dpkg-parsechangelog -SDistribution)
385     rm -rf "$temp"
386     trap - EXIT
387
388     if ! [ "$prev_target" = "$target" ] && ! [ "$target" = "UNRELEASED" ]; then
389         fail_check suite \
390 "last upload targeted $prev_target, now targeting $target; might be a mistake?"
391     fi
392 fi
393
394 # ---- Upstream tag is not ancestor of $branch
395
396 if ! [ "x$upstream_tag" = "x" ] \
397         && ! git merge-base --is-ancestor "$upstream_tag" "$branch" \
398         && ! [ "$quilt_mode" = "baredebian" ]; then
399     fail_check upstream-nonancestor \
400  "upstream tag $upstream_tag is not an ancestor of $branch; probably a mistake"
401 fi
402
403 # ---- Quilt mode-specific checks
404
405 case "$quilt_mode" in
406     gbp)
407         check_treesame "$upstream_tag" "$branch" ':!debian' ':!**.gitignore' \
408             || fail_check_upstream_nonidentical
409         check_patches_apply false
410         ;;
411     unapplied)
412         check_treesame "$upstream_tag" "$branch" ':!debian' \
413             || fail_check_upstream_nonidentical
414         check_patches_apply false
415         ;;
416     baredebian)
417         check_patches_apply false
418         ;;
419     dpm|nofix)
420         check_patches_apply true
421         ;;
422 esac
423
424 # ---- git-debrebase branch format checks
425
426 # only check branches, since you can't run `git debrebase conclude` on
427 # non-branches
428 case "$branch" in
429     refs/heads/*)
430         # see "STITCHING, PSEUDO-MERGES, FFQ RECORD" in git-debrebase(5)
431         ffq_prev_ref="refs/ffq-prev/${branch#refs/}"
432         if git show-ref --quiet --verify "$ffq_prev_ref"; then
433             fail_check unstitched \
434  "this looks like an unstitched git-debrebase branch, which should not be pushed"
435         fi
436 esac
437
438 # ---- Summary
439
440 if $failed_check; then
441     # We don't mention the --force=check options here as those are
442     # mainly for use by scripts, or when you already know what check
443     # is going to fail before you invoke git-debpush.  Keep the
444     # script's terminal output as simple as possible.  No "see the
445     # manpage"!
446     fail "some check(s) failed; you can pass --force to ignore them"
447 fi
448
449 # **** Create the git tag ****
450
451 # convert according to DEP-14 rules
452 git_version=$(echo $version | tr ':~' '%_' | sed 's/\.(?=\.|$|lock$)/.#/g')
453
454 debian_tag="$distro/$git_version"
455 to_push+=("$debian_tag")
456
457 # If the user didn't supply a quilt mode, look for it in a previous
458 # tag made by this script
459 if [ "x$quilt_mode" = "x" ] && [ "$format" = "3.0 (quilt)" ]; then
460     set +o pipefail             # perl will SIGPIPE git-cat-file(1) here
461     if [ "x$last_debian_tag" != "x" ]; then
462         quilt_mode=$(git cat-file -p $(git rev-parse "$last_debian_tag") \
463                          | perl -wne \
464                                 'm/^\[dgit.*--quilt=([a-z+]+).*\]$/;
465                                  if ($1) { print "$1\n"; exit }')
466     fi
467     set -o pipefail
468 fi
469
470 quilt_mode_text=""
471 if [ "$format" = "3.0 (quilt)" ]; then
472     if [ "x$quilt_mode" = "x" ]; then
473         echo >&2 "$us: could not determine the git branch layout"
474         echo >&2 "$us: please supply a --quilt= argument"
475         exit 1
476     else
477         quilt_mode_text=" --quilt=$quilt_mode"
478     fi
479 fi
480
481 tagmessage="$source release $version for $target
482
483 [dgit distro=$distro split$quilt_mode_text]
484 [dgit please-upload$upstream_info]
485 "
486
487 git tag "${git_tag_opts[@]}" -s -m "$tagmessage" "$debian_tag" "$branch"
488
489 # **** Do a git push ****
490
491 if $pushing; then
492     git push "$remote" "${to_push[@]}"
493 fi