aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/release/common.sh
blob: 1b1a441bf126af02075e2e0d2d55391a10c7e841 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/bin/bash

# Copyright 2015 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -eu

# Some common method for release scripts

# A release candidate is created from a branch named "release-%name%"
# where %name% is the name of the release. Once promoted to a release,
# A tag %name% will be created from this branch and the corresponding
# branch removed.
# The last commit of the release branch is always a commit containing
# the release notes in the commit message and updating the CHANGELOG.md.
# This last commit will be cherry-picked back in the master branch
# when the release candidate is promoted to a release.
# To follow tracks and to support how CI systems fetch the refs, we
# store two commit notes: the release name and the candidate number.

# Returns the branch name of the current git repository
function git_get_branch() {
  git symbolic-ref --short HEAD
}

# Show the commit message of the ref specified in argument
function git_commit_msg() {
  git show -s --pretty=format:%B "$@"
}

# Extract the release candidate number from the git notes
function get_release_candidate() {
  git notes --ref=release-candidate show "$@" 2>/dev/null || true
}

# Extract the release name from the git notes
function get_release_name() {
  git notes --ref=release show "$@" 2>/dev/null || true
}

# Get the short hash of a commit
function git_commit_hash() {
  git rev-parse "${1}"
}

# Get the subject (first line of the commit message) of a commit
function git_commit_subject() {
  git show -s --pretty=format:%s "$@"
}

# Get the list of commit hashes between two revisions
function git_log_hash() {
  local baseline="$1"
  local head="$2"
  shift 2
  git log --pretty=format:%H "${baseline}".."${head}" "$@"
}

# Extract the full release name from the git notes
function get_full_release_name() {
  local name="$(get_release_name "$@")"
  local rc="$(get_release_candidate "$@")"
  if [ -n "${rc}" ]; then
    echo "${name}rc${rc}"
  else
    echo "${name}"
  fi
}

# Extract the release notes from the git notes
function get_release_notes() {
  git notes --ref=release-notes show "$@" 2>/dev/null || true
}

# Returns the info from the branch of the release. It is the current branch
# but it errors out if the current branch is not a release branch. This
# method returns the tag of the release and the number of the current
# candidate in this release.
function get_release_branch() {
  local branch_name=$(git_get_branch)
  if [ -z "$(get_release_name)" ] || [ -z "$(get_release_candidate)" ]; then
    echo "Not a release branch: ${branch_name}." >&2
    exit 1
  fi
  echo "${branch_name}"
}

# fmt behaves differently on *BSD and on GNU/Linux, use fold.
function wrap_text() {
  fold -s -w $1 | sed 's/ *$//'
}

# Create the revision information given a list of commits. The first
# commit should be the baseline, and the other ones are the cherry-picks.
# The result is of the form:
# Baseline: BASELINE_COMMIT
#
# Cherry picks:
#    + CHERRY_PICK1: commit message summary of the CHERRY_PICK1. This
#                    message will be wrapped into 70 columns.
#    + CHERRY_PICK2: commit message summary of the CHERRY_PICK2.
function create_revision_information() {
  echo "Baseline: $(git_commit_hash "${1}")"
  local first=1
  shift
  while [ -n "${1-}" ]; do
    if [[ "$first" -eq 1 ]]; then
      echo -e "\nCherry picks:"
      first=0
    fi
    local hash="$(git_commit_hash "${1}")"
    local subject="$(git_commit_subject $hash)"
    local lines=$(echo "$subject" | wrap_text 65)  # 5 leading spaces.
    echo "   + $hash:"
    echo "$lines" | sed 's/^/     /'
    shift
  done
}

# Get the master commit
# Some machine might not have a "master" branch, use "origin/master" in that case
function get_master_ref() {
  git rev-parse --verify master 2>/dev/null || git rev-parse --verify origin/master
}

# Get the baseline of master.
# Args: $1: release branch, default to HEAD
function get_release_baseline() {
  git merge-base $(get_master_ref) "${1:-HEAD}"
}

# Returns the list of (commit hash, patch-id) from $1..$2
# Args:
#   $1: the first commit in the list (excluded)
#   $2: the last commit in the list
function get_patch_ids() {
  git_log_hash "$1" "$2" | xargs git show | git patch-id
}

# Returns the original commit a commit was cherry-picked from master
# Args:
#   $1: the commit to find
#   $2: the baseline from which to look for (up to master)
#   $3: master ref (optional, default master)
#   $4: The list of master changes as returned by get_patch_ids (optional)
function get_cherrypick_origin() {
  local master=${3:-$(get_master_ref)}
  local master_changes="${4-$(get_patch_ids "${2}" "${master}")}"
}

# Get the list of cherry-picks since master
# Args:
#   $1: branch, default to HEAD
#   $2: baseline change, default to $(get_release_baseline $1)
function get_cherrypicks() {
  local branch="${1:-HEAD}"
  local master=$(get_master_ref)
  local baseline="${2:-$(get_release_baseline "${branch}")}"
  # List of changes since the baseline on the release branch
  local changes="$(git_log_hash "${baseline}" "${branch}" --reverse)"
  # List of changes since the baseline on the master branch, and their patch-id
  local master_changes="$(git_log_hash "${baseline}" "${master}" | xargs git show | git patch-id)"
  # Now for each changes on the release branch
  for i in ${changes}; do
    local hash=$(git notes --ref=cherrypick show "$i" 2>/dev/null || true)
    if [ -z "${hash}" ]; then
      # Find the change with the same patch-id on the master branch if the note is not present
      hash=$(echo "${master_changes}" \
          | grep "^$(git show "$i" | git patch-id | cut -d " " -f 1)" \
          | cut -d " " -f 2)
    fi
    if [ -z "${hash}" ]; then
     # We don't know which cherry-pick it is coming from, fall back to the new commit hash.
     echo "$i"
    else
     echo "${hash}"
    fi
  done
}

# Generate the title of the release with the date from the release name ($1).
function get_release_title() {
  echo "Release ${1} ($(date +%Y-%m-%d))"
}

# Generate the release message to be added to the changelog
# from the release notes for release $1
# Args:
#   $1: release name
#   $2: release ref (default HEAD)
#   $3: delimiter around the revision information (default none)
function generate_release_message() {
  local release_name="$1"
  local branch="${2:-HEAD}"
  local delimiter="${3-}"
  local baseline="$(get_release_baseline "${branch}")"
  local cherrypicks="$(get_cherrypicks "${branch}" "${baseline}")"

  get_release_title "$release_name"
  echo

  if [ -n "${delimiter}" ]; then
    echo "${delimiter}"
  fi
  create_revision_information $baseline $cherrypicks
  if [ -n "${delimiter}" ]; then
    echo "${delimiter}"
  fi

  echo
  get_release_notes "${branch}"
}

# Returns the release notes for the CHANGELOG.md taken from either from
# the notes for a release candidate or from the commit message for a
# full release.
function get_full_release_notes() {
  local release_name="$(get_full_release_name "$@")"

  if [[ "${release_name}" =~ rc[0-9]+$ ]]; then
    # Release candidate, we need to generate from the notes
    generate_release_message "${release_name}" "$@"
  else
    # Full release, returns the commit message
    git_commit_msg "$@"
  fi
}

# Merge three release notes using branch $1 as a base
# Args:
#   $1 the branch name to use to play on
#   $2 the new generated release notes
#   $3 the last generated release notes
#   $4 the last edited release notes
function merge_release_notes() {
  local branch_name="$1"
  local relnotes="$2"
  local last_relnotes="$3"
  local last_savedrelnotes="$4"
  if [ "${last_relnotes}" == "${last_savedrelnotes}" ]; then
    echo "${relnotes}"
  else
    # Merge the three release notes, use git merge for it
    git checkout -q -b "${branch_name}-merge-notes-1"
    echo "${last_relnotes}" >.relnotes
    git add .relnotes
    git commit -q -m "last_relnotes" --allow-empty
    echo "${last_savedrelnotes}" >.relnotes
    git add .relnotes
    git commit -q -m "last_savedrelnotes" --allow-empty
    git checkout -q -b "${branch_name}-merge-notes-2" HEAD~
    echo "${relnotes}" >.relnotes
    git add .relnotes
    git commit -q -m "relnotes" --allow-empty
    git merge -q --no-commit "${branch_name}-merge-notes-1" &>/dev/null || true
    cat .relnotes

    # Clean-up
    git merge --abort || true &>/dev/null
    git checkout -q "${branch_name}"
    git branch -D ${branch_name}-merge-notes-{1,2} >/dev/null
  fi
}