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