-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Hacking on script to backport issues
- Loading branch information
1 parent
1fb8def
commit 898cd47
Showing
5 changed files
with
498 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
#!/bin/bash | ||
|
||
start_dir=$(pwd) | ||
script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") | ||
|
||
# Source the functions file | ||
functions="$script_dir/functions.sh" | ||
[ -x "$functions" ] || { echo -e "\nERROR: $functions is not an executable file"; exit 1; } | ||
source "$functions" | ||
|
||
test_filter="" | ||
if [[ "$SPRING_MODULITH_BACKPORT_TICKET_TEST_MODE_ENABLED" == "true" ]]; then | ||
echo -e "\nTest mode is enabled" | ||
test_dir=$(getTestDir) | ||
test_repo=$(getTestRepoName) | ||
cd "$test_dir/$test_repo" || _exit 1 "Failed to change directory to $test_dir/$test_repo" | ||
current_url=$(git remote get-url origin) | ||
[[ "$?" == 0 ]] || _exit 1 "Failed to get current git url" | ||
[[ "$current_url" != *"spring-projects"* ]] || _exit 1 "Remote URL cannot contain 'spring-projects' when test mode is enabled" | ||
# Including a "since" date to enable test mode, wherein a copy of the repo is created (see test scripts) | ||
# Issues created in the test env start with 1, which would be matched with old commits rather than new "test" commits | ||
# Setting this date to "2023-11-01" will enable backporting open issue 345 if necessaey | ||
# 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 | ||
test_filter="--since=\"2023-11-01\"" | ||
fi | ||
|
||
# Check that at least two inputs were provided | ||
# Format $ticketNumber $targetVersion1 $targetVersion2 ... $targetVersionN | ||
isBlank "$1" || isBlank "$2" && _exit 1 "Two inputs are required: ticketNumber targetVersion[]" | ||
|
||
# Check that first input is valid | ||
echo -e "\nChecking that the first input is valid" | ||
number=$1 | ||
isValidIssueNumber "$number" || _exit 1 "The provided issue number is invalid" | ||
|
||
# Check that second input is valid | ||
echo -e "\nChecking that the second input is valid" | ||
# Convert the second input to an array and check each element | ||
versions=("${@:2}") | ||
for version in "${versions[@]}"; do | ||
isValidVersionNumber "$version" || _exit 1 "The provided version number [$version] is invalid" | ||
done | ||
|
||
echo -e "\nChecking the state of the current branch" | ||
|
||
isDefaultBranch || _exit 1 "Current branch is not the default branch" | ||
isCleanBranch || _exit 1 "Current branch is not clean" | ||
# 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 | ||
|
||
sourceGh=$(getGHCode "$number") | ||
branch=$(git branch --show-current) | ||
|
||
echo -e "\nGathered working values:" | ||
echo "sourceGh=$sourceGh" | ||
echo "branch=$branch" | ||
echo "test_filter=$test_filter" | ||
|
||
# The SHAs of all commits associated with the source ticket | ||
echo -e "\nCapturing commits for $sourceGh:" | ||
if [[ "$test_filter" == "" ]]; then | ||
git log --grep="\<$sourceGh\>" --reverse | ||
shas=$(git log --grep="\<$sourceGh\>" --reverse --format="%H") | ||
else | ||
git log --grep="\<$sourceGh\>" "$test_filter" --reverse | ||
shas=$(git log --grep="\<$sourceGh\>" "$test_filter" --reverse --format="%H") | ||
fi | ||
|
||
echo -e "\nshas=\n$shas" | ||
|
||
# For each of the target versions | ||
for version in "${versions[@]}" | ||
do | ||
# Turn 1.5.6 into 1.5.x | ||
targetBranch=$(getTargetBranch "$version") | ||
|
||
# Checkout target branch and cherry-pick commit | ||
echo -e "\nChecking out target branch" | ||
|
||
git checkout $targetBranch | ||
isCleanBranch || _exit 1 "Current branch is not clean" | ||
# 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 | ||
|
||
targetGh="" | ||
targetMilestone=$(getTargetMilestone "$version") | ||
|
||
# Cherry-pick all previously found SHAs | ||
while IFS= read -r sha | ||
do | ||
|
||
echo -e "\nCherry-pick commit $sha from $branch" | ||
git cherry-pick "$sha" | ||
retVal=$? | ||
[ "$retVal" == 0 ] || _exit 1 "Cherry-pick of commit $sha failed with return code $retVal" | ||
|
||
if isBlank "$targetGh"; then | ||
targetCandidateNumbers=$(getIssueCandidatesForMilestone "$number" "$targetMilestone") | ||
IFS=$'\n' read -rd '' -a array <<< "$targetCandidateNumbers" | ||
countTargetCandidateNumbers=${#array[@]} | ||
[ $countTargetCandidateNumbers -lt 2 ] || _exit 1 "Found multiple candidate target issues [$targetCandidateNumbers] for milestone [$targetMilestone]" | ||
if [ $countTargetCandidateNumbers -eq 1 ]; then | ||
#targetNumber=$(echo "$targetCandidateNumbers" | tr -d '\n') | ||
targetNumber="$targetCandidateNumbers" | ||
echo -e "\nRetrieved existing open target issue [$targetNumber] for milestone [$targetMilestone]" | ||
isCleanIssue "$targetNumber" || _exit 1 "Target issue [$targetNumber] is not clean" | ||
else | ||
# count is 0, create a new issue | ||
targetNumber=$(createIssueForMilestone "$number" "$targetMilestone") | ||
isBlank "$targetNumber" && _exit 1 "Failed to create a new target issue for milestone [$targetMilestone]" | ||
echo -e "\nCreated new target issue [$targetNumber] for milestone [$targetMilestone]" | ||
fi | ||
targetGh=$(getGHCode "$targetNumber") | ||
fi | ||
|
||
# Replace ticket reference with new one | ||
updateCommitMessage "$sourceGh" "$targetGh" | ||
echo "Updated commit message" | ||
|
||
done <<< "$shas" | ||
|
||
done | ||
|
||
# Return to original branch | ||
git checkout "$branch" | ||
|
||
cd "$start_dir" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
# functions.sh | ||
|
||
# _exit should be called from the main script only. If called from | ||
# another function, it will not cause the main script to exit | ||
_exit() { | ||
local exit_code="$1" | ||
local message="$2" | ||
if [ "$exit_code" -ne 0 ]; then | ||
echo -e "\nERROR: $message" | ||
else | ||
echo -e "\n$message" | ||
fi | ||
echo "Exiting script [exit_code=$exit_code]" | ||
exit "$exit_code" | ||
} | ||
|
||
getTestDir() { | ||
echo "/tmp" | ||
} | ||
|
||
getTestRepoName() { | ||
echo "spring-modulith-temp-copy" | ||
} | ||
|
||
isBlank() { | ||
[[ "$1" =~ ^[[:space:]]*$ ]] | ||
} | ||
|
||
isValidIssueNumber() { | ||
local issue="$1" | ||
# Check if the provided number matches an existing issue number | ||
# Note: The issue status should be "open" (backporting is done before an issue is closed) | ||
# But it is not necessary to filter on status - it is preferable not to constrain | ||
if ! gh issue list --limit 10000 --state "all" --json number --jq '.[].number' | grep -x -q "^$issue$"; then | ||
echo "The provided issue number [$issue] does not match an existing GitHub issue number" | ||
# output value | ||
false | ||
else | ||
echo "The provided issue number [$issue] matches an existing GitHub issue number" | ||
# output value | ||
true | ||
fi | ||
} | ||
|
||
isValidVersionNumber() { | ||
local version="$1" | ||
# Check input format | ||
local regex='^[0-9]+\.[0-9]+\.[0-9]+$' | ||
if [[ "$version" =~ $regex ]]; then | ||
echo "Version [$version] matches the required format" | ||
# Check for branch | ||
local targetBranch=$(getTargetBranch "$version") | ||
local branch=$(git ls-remote --heads origin "$targetBranch") | ||
if [[ -n "$branch" ]]; then | ||
echo "Branch [$targetBranch] exists" | ||
# Check for milestone | ||
local targetMilestone=$(getTargetMilestone "$version") | ||
local milestone=$(gh api repos/:owner/:repo/milestones --jq ".[] | select(.title == \"$targetMilestone\") | .title") | ||
if [[ -n "$milestone" ]]; then | ||
echo "Milestone [$targetMilestone] exists" | ||
true | ||
else | ||
echo "Milestone [$targetMilestone] does not exist" | ||
false | ||
fi | ||
else | ||
echo "Branch [$targetBranch] does not exist" | ||
false | ||
fi | ||
else | ||
echo "Version [$version] does not match the required format [$regex]" | ||
false | ||
fi | ||
} | ||
|
||
getTargetBranch() { | ||
local version="$1" | ||
local targetBranch="$(echo $version | grep -oE '^[0-9]+\.[0-9]+').x" | ||
echo "$targetBranch" | ||
} | ||
|
||
getTargetMilestone() { | ||
local version="$1" | ||
local targetMilestone="$version" | ||
echo "$targetMilestone" | ||
} | ||
|
||
isDefaultBranch() { | ||
local current_branch=$(git rev-parse --abbrev-ref HEAD) | ||
local default_branch=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name') | ||
if [ "$current_branch" != "$default_branch" ]; then | ||
echo "Current branch [$current_branch] is NOT the default branch [$default_branch]" | ||
# output value | ||
false | ||
else | ||
echo "Current branch [$current_branch] is the default branch" | ||
# output value | ||
true | ||
fi | ||
} | ||
|
||
isCleanBranch() { | ||
local branch=$(git symbolic-ref --short HEAD) | ||
local remote_branch="origin/$branch" | ||
|
||
# Check for ongoing cherry-pick | ||
if [ -d .git/sequencer ]; then | ||
echo "Error: Ongoing cherry-pick operation detected." | ||
# return code | ||
return 1 | ||
fi | ||
|
||
# Check for uncommitted changes | ||
if ! git diff-index --quiet HEAD --; then | ||
echo "Error: Uncommitted changes in the working directory." | ||
# return code | ||
return 1 | ||
fi | ||
|
||
# Check for untracked files and directories | ||
if [ -n "$(git clean -fdn)" ]; then | ||
echo "Error: Untracked files or directories present." | ||
# return code | ||
return 1 | ||
fi | ||
|
||
# Fetch latest changes from the remote | ||
git fetch origin &>/dev/null | ||
|
||
# Check if local branch is ahead/behind the remote branch | ||
local local_status=$(git rev-list --left-right --count ${branch}...${remote_branch}) | ||
local ahead=$(echo $local_status | awk '{print $1}') | ||
local behind=$(echo $local_status | awk '{print $2}') | ||
|
||
if [ "$ahead" -ne 0 ]; then | ||
echo "Error: Local branch is ahead of the remote branch by $ahead commit(s)." | ||
# return code | ||
return 1 | ||
elif [ "$behind" -ne 0 ]; then | ||
echo "Error: Local branch is behind the remote branch by $behind commit(s)." | ||
# return code | ||
return 1 | ||
fi | ||
|
||
# If all checks pass | ||
echo "Local branch matches the remote branch." | ||
# return code | ||
return 0 | ||
} | ||
|
||
getGHCode() { | ||
echo "GH-$1" | ||
} | ||
|
||
getIssueCandidatesForMilestone() { | ||
local number="$1" | ||
local milestone="$2" | ||
|
||
local json=$(gh issue view "$number" --json=title,labels) | ||
local title=$(echo "$json" | jq -r '.title') | ||
local labels=$(echo "$json" | jq -r '.labels[].name' | paste -sd ',' -) | ||
local body="Back-port of $(getGHCode $number)." | ||
|
||
local targetCandidateNumbers=$(gh issue list --limit 10000 --state "open" --assignee "@me" --label "$labels" --milestone "$targetMilestone" --json number,title,body --jq ' | ||
.[] | select(.title == "'"$title"'" and (.body | contains("'"$body"'")) ) | .number') | ||
echo "$targetCandidateNumbers" | ||
} | ||
|
||
createIssueForMilestone() { | ||
local number="$1" | ||
local milestone="$2" | ||
|
||
local json=$(gh issue view "$number" --json=title,labels) | ||
local title=$(echo "$json" | jq -r '.title') | ||
local labels=$(echo "$json" | jq -r '.labels[].name' | paste -sd ',' -) | ||
local body="Back-port of $(getGHCode $number)." | ||
|
||
local targetNumber=$(gh issue create --assignee "@me" --label "$labels" --milestone "$targetMilestone" --title "$title" --body "$body" | awk -F '/' '{print $NF}') | ||
echo "$targetNumber" | ||
} | ||
|
||
isCleanIssue() { | ||
local targetNumber="$1" | ||
local targetGh=$(getGHCode "$targetNumber") | ||
|
||
# Check for commits mentioning the issue number | ||
# $test_filter set globally in calling script | ||
local commits | ||
if [[ "$test_filter" == "" ]]; then | ||
commits=$(git log --grep="\b$targetGh\b") | ||
else | ||
commits=$(git log --grep="\b$targetGh\b" "$test_filter") | ||
fi | ||
if [ -z "$commits" ]; then | ||
# There are no commits that reference this issue | ||
# output value | ||
true | ||
else | ||
# output value | ||
false | ||
fi | ||
} | ||
|
||
updateCommitMessage() { | ||
local source="$1" | ||
local target="$2" | ||
local message=$(git log -1 --pretty=format:"%B" | sed "s/$source/$target/g") | ||
if [[ $(echo $message | grep "$target") != "" ]]; then | ||
# Update commit message to refer to new ticket | ||
git commit --amend -m "$message" | ||
[ "$?" -eq 0 ] || return 1 | ||
return 0 | ||
else | ||
return 1 | ||
fi | ||
} |
Oops, something went wrong.