| #!/usr/bin/env bash |
| |
| USAGE="Usage: ${0##*/} <last> <commit> [...]" |
| START="$PWD" |
| LAST= |
| UPSTREAM= |
| COMMIT= |
| BRANCH= |
| |
| die() { |
| [ "$#" -eq 0 ] || echo "$*" >&2 |
| exit 1 |
| } |
| |
| err() { |
| echo "$*" >&2 |
| } |
| |
| quit() { |
| [ "$#" -eq 0 ] || echo "$*" |
| exit 0 |
| } |
| |
| short() { |
| git rev-parse --short "$1" |
| } |
| |
| # returns the latest commit ID in $REPLY. Returns 0 on success, non-zero on |
| # failure with $REPLY empty. |
| get_last_commit() { |
| REPLY=$(git rev-parse HEAD) |
| test -n "$REPLY" |
| } |
| |
| # returns the name of the current branch (1.8, 1.9, etc) in $REPLY. Returns 0 |
| # on success, non-zero on failure with $REPLY empty. |
| get_branch() { |
| local major subver ext |
| REPLY=$(git describe --tags HEAD --abbrev=0 2>/dev/null) |
| REPLY=${REPLY#v} |
| subver=${REPLY#[0-9]*.[0-9]*[-.]*[0-9].} |
| [ "${subver}" != "${REPLY}" ] || subver="" |
| major=${REPLY%.$subver} |
| ext=${major#*[0-9].*[0-9]} |
| REPLY=${major%${ext}} |
| test -n "$REPLY" |
| } |
| |
| # returns the path to the next "up" remote in $REPLY, and zero on success |
| # or non-zero when the last one was reached. |
| up() { |
| REPLY=$(git remote -v | awk '/^up\t.*\(fetch\)$/{print $2}') |
| test -n "$REPLY" |
| } |
| |
| # returns the path to the next "down" remote in $REPLY, and zero on success |
| # or non-zero when the last one was reached. |
| down() { |
| REPLY=$(git remote -v | awk '/^down\t.*\(fetch\)$/{print $2}') |
| test -n "$REPLY" |
| } |
| |
| # verifies that the repository is clean of any pending changes |
| check_clean() { |
| test -z "$(git status -s -uno)" |
| } |
| |
| # verifies that HEAD is the master |
| check_master() { |
| test "$(git rev-parse --verify -q HEAD 2>&1)" = "$(git rev-parse --verify -q master 2>&1)" |
| } |
| |
| # tries to switch to the master branch, only if the current one is clean. Dies on failure. |
| switch_master() { |
| check_clean || die "$BRANCH: local changes, stopping on commit $COMMIT (upstream $UPSTREAM)" |
| git checkout master >/dev/null 2>&1 || die "$BRANCH: failed to checkout master, stopping on commit $COMMIT (upstream $UPSTREAM)" |
| } |
| |
| # walk up to the first repo |
| walk_up() { |
| cd "$START" |
| } |
| |
| # updates the "up" remote repository. Returns non-zero on error. |
| update_up() { |
| git remote update up >/dev/null 2>&1 |
| } |
| |
| # backports commit "$1" with a signed-off by tag. In case of failure, aborts |
| # the change and returns non-zero. Unneeded cherry-picks do return an error |
| # because we don't want to accidently backport the latest commit instead of |
| # this one, and we don't know this one's ID. |
| backport_commit() { |
| local empty=1 |
| |
| if ! git cherry-pick -sx "$1"; then |
| [ -n "$(git diff)" -o -n "$(git diff HEAD)" ] || empty=0 |
| git cherry-pick --abort |
| return 1 |
| fi |
| } |
| |
| [ "$1" != "-h" -a "$1" != "--help" ] || quit "$USAGE" |
| [ -n "$1" -a -n "$2" ] || die "$USAGE" |
| |
| LAST="$1" |
| shift |
| |
| # go back to the root of the repo |
| cd $(git rev-parse --show-toplevel) |
| START="$PWD" |
| |
| while [ -n "$1" ]; do |
| UPSTREAM="$(short $1)" |
| [ -n "$UPSTREAM" ] || die "branch $BRANCH: unknown commit ID $1, cannot backport." |
| COMMIT="$UPSTREAM" |
| BRANCH="-source-" |
| while :; do |
| if ! down; then |
| err "branch $BRANCH: can't go further, is repository 'down' properly set ?" |
| break |
| fi |
| |
| cd "$REPLY" || die "Failed to 'cd' to '$REPLY' from '$PWD', is repository 'down' properly set ?" |
| |
| check_clean || die "Local changes in $PWD, stopping before backporting commit $COMMIT (upstream $UPSTREAM)" |
| |
| check_master || switch_master || die "Cannot switch to 'master' branch in $PWD, stopping before backporting commit $COMMIT (upstream $UPSTREAM)" |
| get_branch || die "Failed to get branch name in $PWD, stopping before backporting commit $COMMIT (upstream $UPSTREAM)" |
| BRANCH="$REPLY" |
| |
| update_up || die "$BRANCH: failed to update repository 'up', stopping before backporting commit $COMMIT (upstream $UPSTREAM)" |
| |
| backport_commit "$COMMIT" || die "$BRANCH: failed to backport commit $COMMIT (upstream $UPSTREAM). Leaving repository $PWD intact." |
| |
| if [ "$BRANCH" = "$LAST" ]; then |
| # reached the stop point, don't apply further |
| break |
| fi |
| |
| get_last_commit || die "$BRANCH: cannot retrieve last commit ID, stopping after backporting commit $COMMIT (upstream $UPSTREAM)" |
| COMMIT="$(short $REPLY)" |
| done |
| walk_up || die "Failed to go back to $PWD, stopping *after* backporting upstream $UPSTREAM" |
| shift |
| done |