1#!/bin/bash
2#Copyright 2021 The gRPC authors.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16set -euo pipefail
17
18ensure_command () {
19  if command -v "$1" 1>/dev/null 2>&1; then
20    return 0
21  else
22    echo "$1 is not installed. Please install it to proceed." 1>&2
23    exit 1
24  fi
25}
26
27display_usage () {
28  cat << EOF >/dev/stderr
29USAGE: $0 PR_ID GITHUB_USER BACKPORT_BRANCHES REVIEWERS [-c PER_BACKPORT_COMMAND]
30   PR_ID: The ID of the PR to be backported.
31   GITHUB_USER: Your GitHub username.
32   BACKPORT_BRANCHES: A space-separated list of branches to which the source PR will be backported.
33   REVIEWERS: A comma-separated list of users to add as both reviewer and assignee.
34   PER_BACKPORT_COMMAND : An optional command to run after cherrypicking the PR to the target branch.
35     If you use this option, ensure your working directory is clean, as "git add -A" will be used to
36     incorporate any generated files. Try running "git clean -xdff" beforehand.
37
38Example: $0 25456 gnossen "v1.30.x v1.31.x v1.32.x v1.33.x v1.34.x v1.35.x v1.36.x" "menghanl,gnossen"
39Example: $0 25493 gnossen "\$(seq 30 33 | xargs -n1 printf 'v1.%s.x ')" "menghanl" -c ./tools/dockerfile/push_testing_images.sh
40EOF
41  exit 1
42}
43
44ensure_command "curl"
45ensure_command "egrep"
46ensure_command "hub"
47ensure_command "jq"
48
49if [ "$#" -lt "4" ]; then
50  display_usage
51fi
52
53PR_ID="$1"
54GITHUB_USER="$2"
55BACKPORT_BRANCHES="$3"
56REVIEWERS="$4"
57shift 4
58
59PER_BACKPORT_COMMAND=""
60while getopts "c:" OPT; do
61  case "$OPT" in
62    c )
63      PER_BACKPORT_COMMAND="$OPTARG"
64      ;;
65    \? )
66      echo "Invalid option: $OPTARG" >/dev/stderr
67      display_usage
68      ;;
69    : )
70      echo "Invalid option: $OPTARG requires an argument." >/dev/stderr
71      display_usage
72      ;;
73  esac
74done
75
76if [[ ! -z "$(git status --porcelain)" && ! -z "$PER_BACKPORT_COMMAND" ]]; then
77  echo "Your working directory is not clean. Try running `git clean -xdff`. Warning: This is irreversible." > /dev/stderr
78  exit 1
79fi
80
81if [ -z "$GITHUB_TOKEN" ]; then
82  echo "A GitHub token is required to run this script. See " \
83         "https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token" \
84         " for more information" >/dev/stderr
85  exit 1
86fi
87
88echo "This script will create a collection of backport PRs. You will probably " \
89       "have to touch your gnubby a frustrating number of times. C'est la vie."
90printf "Press any key to continue."
91read -r RESPONSE </dev/tty
92printf "\n"
93
94
95PR_DATA=$(curl -s -u "$GITHUB_USER:$GITHUB_TOKEN" \
96          -H "Accept: application/vnd.github.v3+json" \
97          "https://api.github.com/repos/grpc/grpc/pulls/$PR_ID")
98
99STATE=$(echo "$PR_DATA" | jq -r '.state')
100if [ "$STATE" != "open" ]; then
101  TARGET_COMMITS=$(echo "$PR_DATA" | jq -r '.merge_commit_sha')
102  FETCH_HEAD_REF=$(echo "$PR_DATA" | jq -r '.base.ref')
103  SOURCE_REPO=$(echo "$PR_DATA" | jq -r '.base.repo.full_name')
104else
105  COMMITS_URL=$(echo "$PR_DATA" | jq -r '.commits_url')
106  COMMITS_DATA=$(curl -s -u "$GITHUB_USER:$GITHUB_TOKEN" \
107                 -H "Accept: application/vnd.github.v3+json" \
108                 "$COMMITS_URL")
109  TARGET_COMMITS=$(echo "$COMMITS_DATA" | jq -r '. | map(.sha) | join(" ")')
110  FETCH_HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.sha')
111  SOURCE_REPO=$(echo "$PR_DATA" | jq -r '.head.repo.full_name')
112fi
113PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
114PR_DESCRIPTION=$(echo "$PR_DATA" | jq -r '.body')
115LABELS=$(echo "$PR_DATA" | jq -r '.labels | map(.name) | join(",")')
116
117set -x
118
119git fetch "git@github.com:$SOURCE_REPO.git" "$FETCH_HEAD_REF"
120
121BACKPORT_PRS=""
122for BACKPORT_BRANCH in $BACKPORT_BRANCHES; do
123  echo "Backporting $TARGET_COMMITS to $BACKPORT_BRANCH."
124
125  git checkout "origin/$BACKPORT_BRANCH"
126
127  BRANCH_NAME="backport_${PR_ID}_to_${BACKPORT_BRANCH}"
128
129  # To make the script idempotent.
130  git branch -D "$BRANCH_NAME" || true
131  git checkout "$BACKPORT_BRANCH"
132  git checkout -b "$BRANCH_NAME"
133
134  for TARGET_COMMIT in $TARGET_COMMITS; do
135    git cherry-pick -m 1 "$TARGET_COMMIT"
136  done
137
138  if [[ ! -z "$PER_BACKPORT_COMMAND" ]]; then
139    git submodule update --init --recursive
140
141    # To remove dangling submodules.
142    git clean -xdff
143    eval "$PER_BACKPORT_COMMAND"
144    git add -A
145    git commit --amend --no-edit
146  fi
147
148  BACKPORT_PR=$(hub pull-request -p -m "[Backport] $PR_TITLE" \
149                  -m "*Beep boop. This is an automatically generated backport of #${PR_ID}.*" \
150                  -m "$PR_DESCRIPTION" \
151                  -l "$LABELS" \
152                  -b "$GITHUB_USER:$BACKPORT_BRANCH" \
153                  -r "$REVIEWERS" \
154                  -a "$REVIEWERS" | tail -n 1)
155  BACKPORT_PRS+="$BACKPORT_PR\n"
156
157  # TODO: Turn on automerge once the Github API allows it.
158done
159
160printf "Your backport PRs have been created:\n$BACKPORT_PRS"
161