summaryrefslogtreecommitdiffstats
path: root/scripts/backport
blob: 4f6014093c9880ee2dfca1abd4eaef14dd4b2428 (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
#!/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 accidentally 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