#!/bin/sh usage () { echo Usage: `basename $0` "[-h|-u] [-v] [--] prior soon"; } # Copyright (C) 2020 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only help () { usage cat <&2; } die () { warn "$@"; exit 1; } run () { mutter "Running: $@"; eval "$@" || die "Failed ($?): $@"; } logged () { mention "Running: $@"; eval "$@"; } second () { if [ $# -lt 2 ] then die "No argument supplied for $1" elif [ -z "$2" ] then die "Empty argument passed for $1 " fi echo "$2" } # Parse command-line: RELNAME= CHATTY= AMEND= TASK= bad () { usage >&2; die "$@"; } while [ $# -gt 0 ] do case "$1" in -u|--usage) usage; exit 0 ;; -h|--help) help; exit 0 ;; -a|--amend) AMEND=--amend; shift ;; --replace) AMEND=--replace; shift ;; -r|--release) RELNAME=`second "$@"`; shift 2 ;; -t|--task|--task-number) TASK=`second "$@"`; shift 2 ;; -v|--verbose) CHATTY=more; shift ;; -q|--quiet) CHATTY=less; shift ;; --) shift; break ;; -*) bad "Unrecognised option: $1" ;; *) break ;; esac done # Check basic expectations of context: if [ -d src -a -f sync.profile ] then SYNC='sync.profile' SRCDIR=src elif [ -f Source/sync.profile ] # qtwebkit/'s eccentric layout ... then SYNC='Source/sync.profile' SRCDIR=Source else die "I expect to be run in the top level directory of a module (see --help)." fi MODULE=`/bin/pwd` MODULE=`basename "$MODULE"` THERE=`dirname $0` [ -n "$THERE" -a -x "$THERE/resetboring.py" ] || \ die "I don't know where resetboring.py is: please run me via an explicit path." python3 -c 'from dulwich.repo import Repo; from dulwich.index import IndexEntry' || \ die "I need dulwich installed (for resetboring.py; see --help)." # dulwich 0.16.3 has been known to work; 0.9.4 is too old. # Select revisions to compare: [ $# -eq 2 ] || bad "Expected exactly two arguments, got $#: $@" for arg do git rev-parse "$arg" -- >/dev/null || bad "Failed to parse $arg as a git ref" done PRIOR="$1" RELEASE="$2" [ -n "$RELNAME" ] || RELNAME="$RELEASE" RESTORE="`git branch | sed -n -e '/^\* (HEAD/ s/.* \([^ ]*\))$/\1/ p' -e '/^\*/ s/.* // p'`" # Implement --verbose, --quiet: mutter () { true; } mention () { warn "$@"; } # Option to pass to various git commands: QUIET= UNQUIET=-q case "$CHATTY" in more) UNQUIET= mutter () { warn "$@"; } ;; less) QUIET=-q mention () { true; } ;; *) ;; esac retire () { mention "$@"; exit; } checkout () { run git checkout $QUIET "$@"; } # Get API headers of $RELEASE checked out on a branch off $PRIOR: BRANCH="api-review-$PRIOR-$RELNAME" mutter "Checking for branch $BRANCH to check out" case `git branch | grep -wF " $BRANCH" | grep "^[* ] $BRANCH"'$'` in '') checkout -b "$BRANCH" "$PRIOR" NEWBRANCH=yes if [ -n "$AMEND" ] then mention "Ignoring requested $AMEND: no prior $BRANCH" AMEND= fi ;; '* '*) case "$AMEND" in '--replace') mutter "On prior branch $BRANCH; shall be removed and recreated" ;; '--amend') mutter "Already on branch $BRANCH; preparing to amend it" ;; *) mutter "Already on branch $BRANCH; preparing to extend it" ;; esac ;; ' '*) case "$AMEND" in '--replace') mutter "Replacing existing branch $BRANCH (reusing its Change-Id)" ;; '--amend') mutter "Reusing existing branch $BRANCH; preparing to amend it" checkout "$BRANCH" ;; *) mutter "Reusing existing branch $BRANCH; preparing to extend it" checkout "$BRANCH" ;; esac ;; esac # Implement --replace and --amend: CHANGEID= if [ -n "$AMEND" ] then # Suppress --amend or --replace unless we have a prior commit on $BRANCH: if git diff --quiet "$BRANCH" "$PRIOR" then mention "Suppressing requested $AMEND: no prior commit on $BRANCH" AMEND= else # Read (and validate) last commit's Change-Id: CHANGEID=`git show --summary $BRANCH | sed -ne '/Change-Id:/ s/.*: *//p'` [ -n "$CHANGEID" ] || die "No prior Change-Id from $BRANCH" expr "$CHANGEID" : "^I[0-9a-f]\{40\}$" >/dev/null || \ die "Bad prior Change-Id ($CHANGEID) from $BRANCH" # Also preserve Task-number, if present and not over-ridden: [ -n "$TASK" ] || TASK=`git show --summary $BRANCH | sed -ne '/Task-number:/ s/.*: *//p'` fi fi if [ "$AMEND" = '--replace' ] then AMEND= checkout "$RELEASE" run git branch -D "$BRANCH" checkout -b "$BRANCH" "$PRIOR" fi # Even when we do have a prior commit, the headers it reports as # deleted are not actually deleted as part of that commit; so their # deletion below shall ensure they're reported in the commit message, # whether AMENDing or not. We could filter these when not AMENDing, # but (doing so would be fiddly and) any restored would then be # described as deleted in the first commit's message, without # mentioning that they're restored in the second (albeit any change in # them shall show up in the diff). # apiheaders commit # Echoes one header name per line apiheaders () { # Preliminary file-list, to be filtered: git ls-tree -r --name-only "$1" \ | grep "^$SRCDIR"'/[-a-zA-Z0-9_/]*\.h$' \ | grep -vi '^src/tools/' \ | grep -v _p/ \ | grep -v '_\(p\|pch\)\.h$' \ | grep -v '/qt[a-z0-9][a-z0-9]*-config\.h$' \ | grep -v '/\.' \ | grep -vi '/\(private\|doc\|tests\|examples\|build\)/' \ | grep -v '/ui_[^/]*\.h$' \ | if git checkout "$1" -- $SYNC >&2 then mutter "Using $SYNC's list of API headers" "$THERE"/sync-filter-api-headers \ || warn "Failed ($?) to filter header list for $1" else mutter "No $SYNC in $1: falling back on filtered git ls-tree" while read f # Add some further kludgy filtering: do case "$f" in # qtbase just has to be different: src/plugins/platforms/eglfs/api/*) echo "$f" ;; src/3rdparty/angle/include/*) echo "$f" ;; # Otherwise, plugins and 3rdparty aren't API: src/plugins/*) ;; */3rdparty/*) ;; *) echo "$f" ;; esac done fi } # Make sure any sub-submodules are in their right states: git submodule update --checkout mutter "Purging obsolete headers" # so renames get detected and handled correctly: apiheaders HEAD | while read h # Update former API headers, remove them if removed: do git checkout $UNQUIET "$RELEASE" -- "$h" || git rm $UNQUIET -f -- "$h" done 2>&1 | grep -wv "error: pathspec '.*' did not match any" mutter "Checking out $RELNAME's API headers" apiheaders "$RELEASE" | tr '\n' '\0' | \ xargs -0r git checkout "$RELEASE" -- || die "Failed new header checkout" git diff --quiet || die "All changes should initially be staged." if git diff --quiet --cached "$PRIOR" then mutter "Clearing away unused branch and restoring $RESTORE" git checkout $QUIET "$RESTORE" git branch -D "$BRANCH" retire "No changes to API (not even boring ones)" fi mutter "Reverting the boring bits" run "$THERE/resetboring.py" --disclaim | while read f do git checkout $QUIET "$PRIOR" -- "$f" || logged rm -f "$f" done if git diff --quiet --cached "$PRIOR" then retire "All the change here looks boring: check with git diff -D" fi # Find a good place to prepare our commit message if [ -f .git ] then GITDIR=`cut -d ' ' -f 2 <.git` else GITDIR=.git fi # Suppress clang-format pre-commit hook: if [ -e $GITDIR/hooks/pre-commit ] then PRECOMMIT=$GITDIR/hooks/isolated-pre-commit mv $GITDIR/hooks/pre-commit $PRECOMMIT # But put it back when we're done: commit () { mutter "Running: git commit $@" git commit "$@" STAT=$? mv $PRECOMMIT $GITDIR/hooks/pre-commit [ $STAT -eq 0 ] || die "Failed ($STAT): $@" } else commit () { run git commit "$@"; } fi ( echo "$MODULE: API comparison from $PRIOR to $RELNAME" echo git status | grep 'deleted:' | tr '\t' ' ' git diff | wc | python3 -c 'import sys; row = sys.stdin.readline().split(); print(); \ print("Excluded {} lines ({} words, {} bytes) of boring changes.".format(*row) \ if any(int(x) != 0 for x in row) else "Found nothing boring to ignore.")' cat < "$GITDIR/COMMIT_EDITMSG" # The git status in that holds a lock that precludes the git commit; # so we can't just pipe the message and use -F - to deliver it. commit $QUIET $AMEND -F "$GITDIR/COMMIT_EDITMSG" mention "I recommend you review that what git diff -D reports (now) *is* boring." mention "(If any of it isn't, you can git add -p it and git commit --amend.)" mention "Then you can: git push gerrit $BRANCH:refs/for/$RELEASE%topic=api-change-review-$RELNAME" [ -n "$TASK" ] || warn "Warning: no Task-number: footer specified."