Skip to content

Commit 0008185

Browse files
ciberkleidodrotbohm
authored andcommitted
GH-704 - Improved script to back-port commits into maintenance branches.
1 parent 1fb8def commit 0008185

File tree

6 files changed

+498
-48
lines changed

6 files changed

+498
-48
lines changed

etc/backport-ticket.sh

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/bin/bash
2+
3+
start_dir=$(pwd)
4+
script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
5+
6+
# Source the functions file
7+
functions="$script_dir/functions.sh"
8+
[ -x "$functions" ] || { echo -e "\nERROR: $functions is not an executable file"; exit 1; }
9+
source "$functions"
10+
11+
test_filter=""
12+
if [[ "$SPRING_MODULITH_BACKPORT_TICKET_TEST_MODE_ENABLED" == "true" ]]; then
13+
echo -e "\nTest mode is enabled"
14+
test_dir=$(getTestDir)
15+
test_repo=$(getTestRepoName)
16+
cd "$test_dir/$test_repo" || _exit 1 "Failed to change directory to $test_dir/$test_repo"
17+
current_url=$(git remote get-url origin)
18+
[[ "$?" == 0 ]] || _exit 1 "Failed to get current git url"
19+
[[ "$current_url" != *"spring-projects"* ]] || _exit 1 "Remote URL cannot contain 'spring-projects' when test mode is enabled"
20+
# Including a "since" date to enable test mode, wherein a copy of the repo is created (see test scripts)
21+
# Issues created in the test env start with 1, which would be matched with old commits rather than new "test" commits
22+
# Setting this date to "2023-11-01" will enable backporting open issue 345 if necessaey
23+
# Once that issue is closd this date can also be safely set to June 5 (GH-659), Jul 5 (GH-704), or later if these are closed by then
24+
test_filter="--since=\"2023-11-01\""
25+
fi
26+
27+
# Check that at least two inputs were provided
28+
# Format $ticketNumber $targetVersion1 $targetVersion2 ... $targetVersionN
29+
isBlank "$1" || isBlank "$2" && _exit 1 "Two inputs are required: ticketNumber targetVersion[]"
30+
31+
# Check that first input is valid
32+
echo -e "\nChecking that the first input is valid"
33+
number=$1
34+
isValidIssueNumber "$number" || _exit 1 "The provided issue number is invalid"
35+
36+
# Check that second input is valid
37+
echo -e "\nChecking that the second input is valid"
38+
# Convert the second input to an array and check each element
39+
versions=("${@:2}")
40+
for version in "${versions[@]}"; do
41+
isValidVersionNumber "$version" || _exit 1 "The provided version number [$version] is invalid"
42+
done
43+
44+
echo -e "\nChecking the state of the current branch"
45+
46+
isDefaultBranch || _exit 1 "Current branch is not the default branch"
47+
isCleanBranch || _exit 1 "Current branch is not clean"
48+
# To repair, run: git cherry-pick --abort &>/dev/null; git fetch origin && git reset --hard origin/$(git symbolic-ref --short HEAD) && git clean -fd; git checkout main
49+
50+
sourceGh=$(getGHCode "$number")
51+
branch=$(git branch --show-current)
52+
53+
echo -e "\nGathered working values:"
54+
echo "sourceGh=$sourceGh"
55+
echo "branch=$branch"
56+
echo "test_filter=$test_filter"
57+
58+
# The SHAs of all commits associated with the source ticket
59+
echo -e "\nCapturing commits for $sourceGh:"
60+
if [[ "$test_filter" == "" ]]; then
61+
git log --grep="\<$sourceGh\>" --reverse
62+
shas=$(git log --grep="\<$sourceGh\>" --reverse --format="%H")
63+
else
64+
git log --grep="\<$sourceGh\>" "$test_filter" --reverse
65+
shas=$(git log --grep="\<$sourceGh\>" "$test_filter" --reverse --format="%H")
66+
fi
67+
68+
echo -e "\nshas=\n$shas"
69+
70+
# For each of the target versions
71+
for version in "${versions[@]}"
72+
do
73+
# Turn 1.5.6 into 1.5.x
74+
targetBranch=$(getTargetBranch "$version")
75+
76+
# Checkout target branch and cherry-pick commit
77+
echo -e "\nChecking out target branch"
78+
79+
git checkout $targetBranch
80+
isCleanBranch || _exit 1 "Current branch is not clean"
81+
# To repair, run: git cherry-pick --abort &>/dev/null; git fetch origin && git reset --hard origin/$(git symbolic-ref --short HEAD) && git clean -fd; git checkout main
82+
83+
targetGh=""
84+
targetMilestone=$(getTargetMilestone "$version")
85+
86+
# Cherry-pick all previously found SHAs
87+
while IFS= read -r sha
88+
do
89+
90+
echo -e "\nCherry-pick commit $sha from $branch"
91+
git cherry-pick "$sha"
92+
retVal=$?
93+
[ "$retVal" == 0 ] || _exit 1 "Cherry-pick of commit $sha failed with return code $retVal"
94+
95+
if isBlank "$targetGh"; then
96+
targetCandidateNumbers=$(getIssueCandidatesForMilestone "$number" "$targetMilestone")
97+
IFS=$'\n' read -rd '' -a array <<< "$targetCandidateNumbers"
98+
countTargetCandidateNumbers=${#array[@]}
99+
[ $countTargetCandidateNumbers -lt 2 ] || _exit 1 "Found multiple candidate target issues [$targetCandidateNumbers] for milestone [$targetMilestone]"
100+
if [ $countTargetCandidateNumbers -eq 1 ]; then
101+
#targetNumber=$(echo "$targetCandidateNumbers" | tr -d '\n')
102+
targetNumber="$targetCandidateNumbers"
103+
echo -e "\nRetrieved existing open target issue [$targetNumber] for milestone [$targetMilestone]"
104+
isCleanIssue "$targetNumber" || _exit 1 "Target issue [$targetNumber] is not clean"
105+
else
106+
# count is 0, create a new issue
107+
targetNumber=$(createIssueForMilestone "$number" "$targetMilestone")
108+
isBlank "$targetNumber" && _exit 1 "Failed to create a new target issue for milestone [$targetMilestone]"
109+
echo -e "\nCreated new target issue [$targetNumber] for milestone [$targetMilestone]"
110+
fi
111+
targetGh=$(getGHCode "$targetNumber")
112+
fi
113+
114+
# Replace ticket reference with new one
115+
updateCommitMessage "$sourceGh" "$targetGh"
116+
echo "Updated commit message"
117+
118+
done <<< "$shas"
119+
120+
done
121+
122+
# Return to original branch
123+
git checkout "$branch"
124+
125+
cd "$start_dir"

etc/create-backport-tickets.sh

Lines changed: 0 additions & 48 deletions
This file was deleted.

etc/functions.sh

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# functions.sh
2+
3+
# _exit should be called from the main script only. If called from
4+
# another function, it will not cause the main script to exit
5+
_exit() {
6+
local exit_code="$1"
7+
local message="$2"
8+
if [ "$exit_code" -ne 0 ]; then
9+
echo -e "\nERROR: $message"
10+
else
11+
echo -e "\n$message"
12+
fi
13+
echo "Exiting script [exit_code=$exit_code]"
14+
exit "$exit_code"
15+
}
16+
17+
getTestDir() {
18+
echo "/tmp"
19+
}
20+
21+
getTestRepoName() {
22+
echo "spring-modulith-temp-copy"
23+
}
24+
25+
isBlank() {
26+
[[ "$1" =~ ^[[:space:]]*$ ]]
27+
}
28+
29+
isValidIssueNumber() {
30+
local issue="$1"
31+
# Check if the provided number matches an existing issue number
32+
# Note: The issue status should be "open" (backporting is done before an issue is closed)
33+
# But it is not necessary to filter on status - it is preferable not to constrain
34+
if ! gh issue list --limit 10000 --state "all" --json number --jq '.[].number' | grep -x -q "^$issue$"; then
35+
echo "The provided issue number [$issue] does not match an existing GitHub issue number"
36+
# output value
37+
false
38+
else
39+
echo "The provided issue number [$issue] matches an existing GitHub issue number"
40+
# output value
41+
true
42+
fi
43+
}
44+
45+
isValidVersionNumber() {
46+
local version="$1"
47+
# Check input format
48+
local regex='^[0-9]+\.[0-9]+\.[0-9]+$'
49+
if [[ "$version" =~ $regex ]]; then
50+
echo "Version [$version] matches the required format"
51+
# Check for branch
52+
local targetBranch=$(getTargetBranch "$version")
53+
local branch=$(git ls-remote --heads origin "$targetBranch")
54+
if [[ -n "$branch" ]]; then
55+
echo "Branch [$targetBranch] exists"
56+
# Check for milestone
57+
local targetMilestone=$(getTargetMilestone "$version")
58+
local milestone=$(gh api repos/:owner/:repo/milestones --jq ".[] | select(.title == \"$targetMilestone\") | .title")
59+
if [[ -n "$milestone" ]]; then
60+
echo "Milestone [$targetMilestone] exists"
61+
true
62+
else
63+
echo "Milestone [$targetMilestone] does not exist"
64+
false
65+
fi
66+
else
67+
echo "Branch [$targetBranch] does not exist"
68+
false
69+
fi
70+
else
71+
echo "Version [$version] does not match the required format [$regex]"
72+
false
73+
fi
74+
}
75+
76+
getTargetBranch() {
77+
local version="$1"
78+
local targetBranch="$(echo $version | grep -oE '^[0-9]+\.[0-9]+').x"
79+
echo "$targetBranch"
80+
}
81+
82+
getTargetMilestone() {
83+
local version="$1"
84+
local targetMilestone="$version"
85+
echo "$targetMilestone"
86+
}
87+
88+
isDefaultBranch() {
89+
local current_branch=$(git rev-parse --abbrev-ref HEAD)
90+
local default_branch=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
91+
if [ "$current_branch" != "$default_branch" ]; then
92+
echo "Current branch [$current_branch] is NOT the default branch [$default_branch]"
93+
# output value
94+
false
95+
else
96+
echo "Current branch [$current_branch] is the default branch"
97+
# output value
98+
true
99+
fi
100+
}
101+
102+
isCleanBranch() {
103+
local branch=$(git symbolic-ref --short HEAD)
104+
local remote_branch="origin/$branch"
105+
106+
# Check for ongoing cherry-pick
107+
if [ -d .git/sequencer ]; then
108+
echo "Error: Ongoing cherry-pick operation detected."
109+
# return code
110+
return 1
111+
fi
112+
113+
# Check for uncommitted changes
114+
if ! git diff-index --quiet HEAD --; then
115+
echo "Error: Uncommitted changes in the working directory."
116+
# return code
117+
return 1
118+
fi
119+
120+
# Check for untracked files and directories
121+
if [ -n "$(git clean -fdn)" ]; then
122+
echo "Error: Untracked files or directories present."
123+
# return code
124+
return 1
125+
fi
126+
127+
# Fetch latest changes from the remote
128+
git fetch origin &>/dev/null
129+
130+
# Check if local branch is ahead/behind the remote branch
131+
local local_status=$(git rev-list --left-right --count ${branch}...${remote_branch})
132+
local ahead=$(echo $local_status | awk '{print $1}')
133+
local behind=$(echo $local_status | awk '{print $2}')
134+
135+
if [ "$ahead" -ne 0 ]; then
136+
echo "Error: Local branch is ahead of the remote branch by $ahead commit(s)."
137+
# return code
138+
return 1
139+
elif [ "$behind" -ne 0 ]; then
140+
echo "Error: Local branch is behind the remote branch by $behind commit(s)."
141+
# return code
142+
return 1
143+
fi
144+
145+
# If all checks pass
146+
echo "Local branch matches the remote branch."
147+
# return code
148+
return 0
149+
}
150+
151+
getGHCode() {
152+
echo "GH-$1"
153+
}
154+
155+
getIssueCandidatesForMilestone() {
156+
local number="$1"
157+
local milestone="$2"
158+
159+
local json=$(gh issue view "$number" --json=title,labels)
160+
local title=$(echo "$json" | jq -r '.title')
161+
local labels=$(echo "$json" | jq -r '.labels[].name' | paste -sd ',' -)
162+
local body="Back-port of $(getGHCode $number)."
163+
164+
local targetCandidateNumbers=$(gh issue list --limit 10000 --state "open" --assignee "@me" --label "$labels" --milestone "$targetMilestone" --json number,title,body --jq '
165+
.[] | select(.title == "'"$title"'" and (.body | contains("'"$body"'")) ) | .number')
166+
echo "$targetCandidateNumbers"
167+
}
168+
169+
createIssueForMilestone() {
170+
local number="$1"
171+
local milestone="$2"
172+
173+
local json=$(gh issue view "$number" --json=title,labels)
174+
local title=$(echo "$json" | jq -r '.title')
175+
local labels=$(echo "$json" | jq -r '.labels[].name' | paste -sd ',' -)
176+
local body="Back-port of $(getGHCode $number)."
177+
178+
local targetNumber=$(gh issue create --assignee "@me" --label "$labels" --milestone "$targetMilestone" --title "$title" --body "$body" | awk -F '/' '{print $NF}')
179+
echo "$targetNumber"
180+
}
181+
182+
isCleanIssue() {
183+
local targetNumber="$1"
184+
local targetGh=$(getGHCode "$targetNumber")
185+
186+
# Check for commits mentioning the issue number
187+
# $test_filter set globally in calling script
188+
local commits
189+
if [[ "$test_filter" == "" ]]; then
190+
commits=$(git log --grep="\b$targetGh\b")
191+
else
192+
commits=$(git log --grep="\b$targetGh\b" "$test_filter")
193+
fi
194+
if [ -z "$commits" ]; then
195+
# There are no commits that reference this issue
196+
# output value
197+
true
198+
else
199+
# output value
200+
false
201+
fi
202+
}
203+
204+
updateCommitMessage() {
205+
local source="$1"
206+
local target="$2"
207+
local message=$(git log -1 --pretty=format:"%B" | sed "s/$source/$target/g")
208+
if [[ $(echo $message | grep "$target") != "" ]]; then
209+
# Update commit message to refer to new ticket
210+
git commit --amend -m "$message"
211+
[ "$?" -eq 0 ] || return 1
212+
return 0
213+
else
214+
return 1
215+
fi
216+
}

0 commit comments

Comments
 (0)