Skip to content

Commit

Permalink
Hacking on script to backport issues
Browse files Browse the repository at this point in the history
  • Loading branch information
ciberkleid committed Aug 12, 2024
1 parent ea7b299 commit ae14e9a
Show file tree
Hide file tree
Showing 5 changed files with 498 additions and 0 deletions.
125 changes: 125 additions & 0 deletions etc/backport-ticket.sh
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"
216 changes: 216 additions & 0 deletions etc/functions.sh
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
}
Loading

0 comments on commit ae14e9a

Please sign in to comment.