1#!/bin/bash
2set -eu
3cd "$( dirname "${BASH_SOURCE[0]}" )/.."
4
5version=
6version_next=
7
8main() {
9	local opt_auto=0
10	while [[ $# -gt 0 ]] ; do
11		case $1 in
12		-auto)
13			opt_auto=1
14			;;
15		*)
16			echo "Usage: $0 [-auto]" >&2
17			exit 0
18			;;
19		esac
20		shift
21	done
22	if [[ "$opt_auto" -eq 1 ]] ; then
23		auto_prepare_release
24	else
25		interactive
26	fi
27}
28
29auto_prepare_release() {
30	echo 'script/release: auto mode for CI, will check or modify version based on tag' >&2
31
32	assert_tree_clean
33
34	local is_tag=0
35	local version_tag=
36	if version_tag=$(git describe --candidates=0 --tags HEAD 2>/dev/null) ; then
37		is_tag=1
38		version_tag=${version_tag##v}
39		version_check "$version_tag"
40	fi
41
42	local last_tag=
43	local version_replace=
44	if [[ "$is_tag" -eq 0 ]] ; then
45		last_tag=$(git tag --sort=-version:refname |head -n1)
46		last_tag=${last_tag##v}
47		version_replace="${last_tag}.post$(date -u +%y%m%d%H%M)"
48		update_version "setup.py" "s/VERSION =.+/VERSION = \"$version_replace\"/"
49		update_version "python2/httplib2/__init__.py" "s/__version__ =.+/__version__ = \"$version_replace\"/"
50		update_version "python3/httplib2/__init__.py" "s/__version__ =.+/__version__ = \"$version_replace\"/"
51		version_check "$version_replace"
52	fi
53}
54
55interactive() {
56	echo 'script/release: interactive mode for creating new tagged releases with human assistance' >&2
57
58	local branch="${1-$(git symbolic-ref --short HEAD)}"
59	version="$(PYTHONPATH=$PWD/python3 python3 -c 'import httplib2; print(httplib2.__version__)')"
60	printf "\nbranch: %s httplib2.__version__: '%s'\n" $branch $version >&2
61
62	if [[ "$branch" != "master" ]] ; then
63		echo "Must be on master" >&2
64		exit 1
65	fi
66	assert_tree_clean
67
68	last_commit_message=$(git show --format="%s" --no-patch HEAD)
69	expect_commit_message="v$version release"
70	if [[ "$last_commit_message" != "$expect_commit_message" ]] ; then
71		printf "Last commit message: '%s' expected: '%s'\n" "$last_commit_message" "$expect_commit_message" >&2
72		if confirm "Create release commit? [yN] " ; then
73			create_commit
74		elif ! confirm "Continue without proper release commit? [yN] " ; then
75			exit 1
76		fi
77	fi
78	confirm "Continue? [yN] " || exit 1
79
80	echo "Creating tag v$version" >&2
81	if ! git tag "v$version" ; then
82		echo "git tag failed " >&2
83		confirm "Continue still? [yN] " || exit 1
84	fi
85
86	echo "Building package" >&2
87	find . -name '*.pyc' -o -name '*.pyo' -o -name '*.orig' -delete
88	rm -rf python{2,3}/.cache
89	rm -rf build dist
90	local venv=./venv-release
91	if [[ ! -d "$venv" ]] ; then
92		virtualenv $venv
93		$venv/bin/pip install -U check-manifest pip 'setuptools>=43.0' wheel twine
94	fi
95	$venv/bin/python setup.py clean --all
96	$venv/bin/python setup.py sdist bdist_wheel
97	$venv/bin/check-manifest || echo "FIXME check-manifest" >&2
98
99	if confirm "Upload to PyPI? Use in special situation, normally CI (Travis) will upload to PyPI. [yN] " ; then
100		$venv/bin/twine upload dist/* || exit 1
101	fi
102
103	git push --tags
104}
105
106create_commit() {
107	echo "" >&2
108	echo "Plan:" >&2
109	echo "1. bump version" >&2
110	echo "2. update CHANGELOG" >&2
111	echo "3. commit" >&2
112	echo "4. run bin/release again" >&2
113	echo "" >&2
114
115	bump_version
116	edit_news
117
118	git diff
119	confirm "Ready to commit? [Yn] " || exit 1
120	git commit -a -m "v$version_next release"
121
122	echo "Re-exec $0 to continue" >&2
123	exec $0
124}
125
126bump_version() {
127	local current=$version
128	echo "Current version: '$current'" >&2
129	echo -n "Enter next version (empty to abort): " >&2
130	read version_next
131	if [[ -z "$version_next" ]] ; then
132		exit 1
133	fi
134	echo "Next version:    '$version_next'" >&2
135
136	update_version "python3/httplib2/__init__.py" "s/__version__ =.+/__version__ = \"$version_next\"/"
137	update_version "python2/httplib2/__init__.py" "s/__version__ =.+/__version__ = \"$version_next\"/"
138	update_version "setup.py" "s/VERSION =.+/VERSION = \"$version_next\"/"
139
140	confirm "Confirm changes? [yN] " || exit 1
141}
142
143update_version() {
144	local path="$1"
145	local sed_expr="$2"
146		# sed -E --in-place='' -e "s/VERSION =.+/VERSION = \"$version_replace\"/" setup.py
147		# sed -E --in-place='' -e "s/__version__ =.+/__version__ = \"$version_replace\"/" python2/httplib2/__init__.py python3/httplib2/__init__.py
148	echo "Updating file '$path'" >&2
149	if ! sed -E --in-place='' -e "$sed_expr" "$path" ; then
150		echo "sed error $?" >&2
151		exit 1
152	fi
153	assert_modified "$path"
154	echo "" >&2
155}
156
157edit_news() {
158	echo "Changes since last release:" >&2
159	git log --format='%h   %an   %s' "v$version"^.. -- || exit 1
160	echo "" >&2
161
162	patch -p1 <<EOT
163diff a/CHANGELOG b/CHANGELOG
164--- a/CHANGELOG
165+++ b/CHANGELOG
166@@ -0,0 +1,4 @@
167+$version_next
168+
169+  EDIT HERE. Describe important changes and link to more information.
170+
171EOT
172
173	local editor=$(which edit 2>/dev/null)
174	[[ -z "$editor" ]] && editor="$EDITOR"
175	if [[ -n "$editor" ]] ; then
176		if confirm "Open default editor for CHANGELOG? [Yn] " ; then
177			$editor CHANGELOG
178		else
179			confirm "Edit CHANGELOG manually and press any key"
180		fi
181	else
182		echo "Unable to determine default text editor." >&2
183		confirm "Edit CHANGELOG manually and press any key"
184	fi
185	echo "" >&2
186
187	assert_modified CHANGELOG
188
189	echo "" >&2
190	confirm "Confirm changes? [yN] " || exit 1
191}
192
193assert_modified() {
194	local path="$1"
195	if git diff --exit-code "$path" ; then
196		echo "File '$path' is not modified" >&2
197		exit 1
198	fi
199}
200
201assert_tree_clean() {
202	if [[ -n "$(git status --short -uall)" ]] ; then
203		echo "Tree must be clean. git status:" >&2
204		echo "" >&2
205		git status --short -uall
206		echo "" >&2
207		exit 1
208	fi
209}
210
211version_check() {
212	local need=$1
213	local version_setup=$(fgrep 'VERSION =' setup.py |tr -d " '\"" |cut -d\= -f2)
214	local version_py2=$(cd python2 ; python2 -Es -c 'import httplib2;print(httplib2.__version__)')
215	local version_py3=$(cd python3 ; python3 -Es -c 'import httplib2;print(httplib2.__version__)')
216	if [[ "$version_setup" != "$need" ]] ; then
217		echo "error: setup.py VERSION=$version_setup expected=$need" >&1
218		exit 1
219	fi
220	if [[ "$version_py2" != "$need" ]] ; then
221		echo "error: python2/httplib2/__init__.py:__version__=$version_py2 expected=$need" >&1
222		exit 1
223	fi
224	if [[ "$version_py3" != "$need" ]] ; then
225		echo "error: python3/httplib2/__init__.py:__version__=$version_py3 expected=$need" >&1
226		exit 1
227	fi
228}
229
230confirm() {
231	local reply
232	local prompt="$1"
233	read -n1 -p "$prompt" reply >&2
234	echo "" >&2
235	rc=0
236	local default_y=" \[Yn\] $"
237	if [[ -z "$reply" ]] && [[ "$prompt" =~ $default_y ]] ; then
238		reply="y"
239	fi
240	[[ "$reply" != "y" ]] && rc=1
241	return $rc
242}
243
244main "$@"
245