blob: 26a3046c06c1d1515dfeb455b7855cfa7792b8fe [file] [log] [blame]
#!/bin/bash
#set -x
# Enable error tracing but don't exit on error for the whole script
set -E
# Global variables
LINUX_DIR="$HOME/linux"
TEMP_PATCH=""
PENDING_DIR="$HOME/pending/series"
WORKTREE_DIR="$HOME/git-worktrees"
# Function to create a temporary git worktree
create_git_worktree() {
local base_branch="$1"
local branch_name="$2"
local linux_dir="$3"
mkdir -p "$WORKTREE_DIR"
# Generate a unique path with timestamp
local worktree_path="${WORKTREE_DIR}/${branch_name}-$(date +%s)"
# If the directory already exists for some reason, remove it
if [ -d "$worktree_path" ]; then
rm -rf "$worktree_path"
fi
cd "$linux_dir"
# Verify the base branch exists
if ! git rev-parse --verify "$base_branch" >/dev/null 2>&1; then
echo "Error: Branch $base_branch does not exist" >&2
return 1
fi
# Create the worktree
local worktree_result=$(git worktree add --detach "$worktree_path" "$base_branch" 2>&1)
if [ $? -ne 0 ]; then
echo "Error creating worktree: $worktree_result" >&2
return 1
fi
echo "$worktree_path"
}
# Function to remove a git worktree
remove_git_worktree() {
local worktree_path="$1"
local linux_dir="$2"
# Check if the worktree path exists
if [ ! -d "$worktree_path" ]; then
return 0
fi
cd "$linux_dir"
# First make sure any ongoing git operations are aborted
cd "$worktree_path" 2>/dev/null
if [ $? -eq 0 ]; then
# If there's a rebase in progress, abort it
if [ -d ".git/rebase-apply" ]; then
git rebase --abort >/dev/null 2>&1
fi
# If there's an am in progress, abort it
if [ -d ".git/rebase-apply" ]; then
git am --abort >/dev/null 2>&1
fi
# Reset and clean the worktree
git reset --hard >/dev/null 2>&1
git clean -fdx >/dev/null 2>&1
cd "$linux_dir"
fi
# Try to remove the worktree
if ! git worktree remove --force "$worktree_path" >/dev/null 2>&1; then
# If git worktree remove fails, try more aggressive cleanup
echo "Warning: Failed to remove worktree via git, using rm -rf" >&2
rm -rf "$worktree_path"
# Prune the worktree list
git worktree prune >/dev/null 2>&1
fi
}
# Function to generate unique message ID
generate_message_id() {
local timestamp=$(date +%Y%m%d%H%M%S)
local random=$(openssl rand -hex 8)
echo "<${timestamp}-${random}@stable.kernel.org>"
}
# Function to generate unique response filename
generate_response_filename() {
local mbox_file="$1"
local base_dir="$HOME/Mail/stable/respo"
local subject=$(formail -xSubject: < "$mbox_file" | tr '\n' ' ')
local sender=$(formail -xFrom: < "$mbox_file" | tr -dc '[:alnum:]@.<> _-')
local date_str=$(formail -xDate: < "$mbox_file")
local timestamp=$(date -d "$date_str" +%Y%m%d%H%M%S 2>/dev/null || date +%Y%m%d%H%M%S)
local message_id=$(formail -xMessage-ID: < "$mbox_file" | tr -dc '[:alnum:]@._-')
# Clean subject (take first 30 chars, remove special chars)
local clean_subject=$(echo "$subject" | tr -dc '[:alnum:] ' | tr ' ' '_' | cut -c1-30)
# Clean sender email (extract just the email part if possible, otherwise use whole string)
local clean_sender=$(echo "$sender" | grep -o '[^< ]*@[^> ]*' || echo "$sender" | tr -dc '[:alnum:]@._-')
# Create unique filename
local filename="${clean_sender}-${timestamp}-${clean_subject}-${message_id}.response"
echo "${base_dir}/${filename}"
}
check_response_exists() {
local mbox_file="$1"
local response_file=$(generate_response_filename "$mbox_file")
if [ -f "$response_file" ]; then
echo "Response file already exists: $response_file"
return 0
fi
return 1
}
# Function to decode UTF-8 MIME encoded text using Python
decode_mime_header() {
local encoded_text="$1"
[ -z "$encoded_text" ] && return 1
python3 -c '
import sys
import email.header
import email.quoprimime
import email.base64mime
def decode_header(text):
# Add a maximum iteration count to prevent infinite loops
max_iterations = 100
iteration = 0
# Handle quoted-printable and base64 encoded UTF-8 headers
while "=?UTF-8?" in text and iteration < max_iterations:
iteration += 1
start = text.find("=?UTF-8?")
end = text.find("?=", start) + 2
if end <= 1: # No closing "?=" found
break
encoded_part = text[start:end]
try:
# Extract encoding type (B or Q) and encoded text
parts = encoded_part.split("?")
if len(parts) != 5:
# Skip this part and continue with the rest of the text
text = text[:start] + text[end:]
continue
charset, encoding, encoded_text = parts[1:4]
if encoding.upper() == "B":
decoded = email.base64mime.decode(encoded_text.encode())
elif encoding.upper() == "Q":
decoded = email.quoprimime.header_decode(encoded_text)
else:
# Skip unsupported encoding
text = text[:start] + text[end:]
continue
# Replace encoded part with decoded text
new_text = text[:start] + decoded.decode(charset) + text[end:]
# Ensure we made progress
if new_text == text:
# If no change happened, skip this part to avoid infinite loop
text = text[:start] + text[end:]
else:
text = new_text
except Exception:
# If decoding fails, skip this part
text = text[:start] + text[end:]
continue
# If we hit the maximum iterations, fall back to using standard library
if iteration >= max_iterations:
try:
# Try the standard Python email.header module as a fallback
decoded_parts = email.header.decode_header(text)
return str(email.header.make_header(decoded_parts))
except Exception:
# If even that fails, return as is
pass
return text
print(decode_header(sys.argv[1]))
' "$encoded_text" 2>/dev/null || echo "$encoded_text"
}
# Function to normalize author string for comparison
normalize_author() {
local author="$1"
local normalized=""
# First decode any UTF-8 MIME encoding
normalized=$(decode_mime_header "$author")
# Remove extra quotes and normalize whitespace
normalized=$(echo "$normalized" | sed -E '
# Remove surrounding quotes if they exist
s/^"([^"]+)"$/\1/
# Remove extra spaces around < and >
s/[[:space:]]*<[[:space:]]*/</g
s/[[:space:]]*>[[:space:]]*/>/g
# Normalize spaces between parts
s/[[:space:]]+/ /g
# Trim leading/trailing whitespace
s/^[[:space:]]+//
s/[[:space:]]+$//
')
echo "$normalized"
}
# Function to check if we should ignore this mail based on sender
should_ignore_mail() {
local mbox_file="$1"
local from=$(formail -xFrom: < "$mbox_file" | sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g')
if check_response_exists "$MBOX_FILE"; then
exit 0
fi
# List of authors to ignore
if [[ "$from" =~ ^"Sasha Levin".*$ ]] || \
[[ "$from" =~ ^"Linux Kernel Distribution System".*$ ]] || \
[[ "$from" =~ ^"Greg Kroah-Hartman".*$ ]]; then
return 0
fi
return 1
}
# Function to check if mail contains a git patch
is_git_patch() {
local mbox_file="$1"
# We need to use formail -c to process the entire email including headers and body
formail -c < "$mbox_file" | awk '
BEGIN {
found=0
p=0
in_headers=1
has_patch_subject=0
has_diff=0
}
# Track when we move from email headers to body
in_headers && /^$/ { in_headers=0; next }
# Look for [PATCH] in subject line while in headers
in_headers && /^Subject:.*\[PATCH/ { has_patch_subject=1; next }
# Look for patch separator
!in_headers && /^---$/ { p=NR; next }
# Look for diff or index after separator
p && (/^diff --git/ || /^index [0-9a-f]/) { has_diff=1; exit }
# Exit if we go too far past the separator
NR > p+20 && p { exit }
END { exit !(has_patch_subject && has_diff) }
'
}
# Function to decode UTF-8 email subject
decode_subject() {
local encoded_subject="$1"
echo "$encoded_subject" | decode_mime_header
}
# Function to extract series info from subject
extract_series_info() {
local subject="$1"
# Pattern to match [PATCH X/N] format
local part_pattern='\[PATCH.*[[:space:]]([0-9]+)/([0-9]+)\]'
if [[ $subject =~ $part_pattern ]]; then
local current="${BASH_REMATCH[1]}"
local total="${BASH_REMATCH[2]}"
# Remove leading zeros to avoid octal interpretation
current=$((10#$current))
total=$((10#$total))
echo "$current $total"
return 0
fi
return 1
}
# Function to get message ID from mail
get_message_id() {
local mbox_file="$1"
formail -xMessage-ID: < "$mbox_file" | tr -d '[:space:]'
}
# Function to get in-reply-to ID from mail
get_in_reply_to() {
local mbox_file="$1"
formail -xIn-Reply-To: < "$mbox_file" | tr -d '[:space:]'
}
# Function to generate series directory name
get_series_dir() {
local message_id="$1"
local in_reply_to="$2"
# Use the first message's ID (either this message if it's first, or the one it replies to)
local series_id="${in_reply_to:-$message_id}"
# Create a safe directory name from the message ID
echo "${series_id}" | sed 's/[<>]//g' | tr -c '[:alnum:]' '_'
}
# Function to generate a more unique patch filename
generate_patch_filename() {
local base_dir="$1"
local part="$2"
local subject="$3"
local sender="$4"
local timestamp="$5"
local message_id="$6"
# Clean subject to use in filename (take first 30 chars, remove special chars)
local clean_subject=$(echo "$subject" | tr -dc '[:alnum:] ' | tr ' ' '_' | cut -c1-30)
# Clean sender email (extract just the email part if possible, otherwise use whole string)
local clean_sender=$(echo "$sender" | grep -o '[^< ]*@[^> ]*' || echo "$sender" | tr -dc '[:alnum:]@._-')
# Create unique filename combining all elements
local clean_message_id=$(echo "$message_id" | tr -dc '[:alnum:]@._-')
echo "${part}-${clean_sender}-${timestamp}-${clean_subject}-${clean_message_id}.mbox"
}
# Function to strip leading zeros from a number
strip_leading_zeros() {
local num="$1"
# Use parameter expansion to remove leading zeros
# Then use 10#$num to ensure base-10 interpretation
num="${num##0}"
# If num is empty, it was zero, so return 0
if [ -z "$num" ]; then
echo "0"
else
echo "$((10#$num))" # Force base-10 interpretation
fi
}
# Function to store patch in series directory
store_patch() {
local mbox_file="$1"
local series_dir="$2"
local part=$(strip_leading_zeros "$3")
# Extract additional information for filename
local subject=$(formail -xSubject: < "$mbox_file" | tr '\n' ' ')
local sender=$(formail -xFrom: < "$mbox_file" | tr -dc '[:alnum:]@.<> _-')
local date_str=$(formail -xDate: < "$mbox_file")
local timestamp=$(date -d "$date_str" +%Y%m%d%H%M%S 2>/dev/null || date +%Y%m%d%H%M%S)
local message_id=$(formail -xMessage-ID: < "$mbox_file")
# Generate unique filename
local filename=$(generate_patch_filename "$series_dir" "$part" "$subject" "$sender" "$timestamp" "$message_id")
mkdir -p "$series_dir"
cp "$mbox_file" "$series_dir/$filename"
# Create a symlink with just the part number for backward compatibility
ln -sf "$filename" "$series_dir/$part.mbox"
}
# Function to check if series is complete
is_series_complete() {
local series_dir="$1"
local total_parts="$2"
for ((i=1; i<=total_parts; i++)); do
if [ ! -f "$series_dir/$i.mbox" ]; then
return 1
fi
done
return 0
}
# Function to clean subject for searching
clean_subject() {
local subject="$1"
local cleaned_subject
# Special handling for FAILED patch subjects
if [[ "$subject" =~ FAILED:.*\"([^\"]+)\".*failed[[:space:]]to[[:space:]]apply ]]; then
# Extract the actual patch subject from within quotes
cleaned_subject="${BASH_REMATCH[1]}"
else
# Regular subject cleaning
cleaned_subject=$(echo "$subject" | \
sed -E 's/\[[^]]*\]//g' | \
tr '\n' ' ' | \
tr -s ' ' | \
sed -E 's/^[[:space:]]+|[[:space:]]+$//g') # Trim whitespace
fi
# Remove leading "Re: " if present
cleaned_subject=$(echo "$cleaned_subject" | sed -E 's/^Re: *//')
echo "$cleaned_subject"
}
# Function to find commit by subject in origin/master
find_commit_by_subject() {
local subject="$1"
local linux_dir="$2"
local cleaned_subject
cd "$linux_dir"
cleaned_subject=$(clean_subject "$subject")
cleaned_subject=$(printf '%s' "$cleaned_subject" | sed 's/[[\.*^$/]/\\&/g')
git log origin/master --format="%H" --grep="^${cleaned_subject}$" -1
}
# Function to apply previous patches in series
apply_series_patches() {
local series_dir="$1"
local current_part="$2"
local worktree_path="$3"
cd "$worktree_path"
# Clean up any previous rebase-apply directory that might exist
if [ -d ".git/rebase-apply" ]; then
rm -rf ".git/rebase-apply"
fi
# Reset to ensure clean state
git reset --hard >/dev/null 2>&1
# Apply all patches up to but not including the current one
for ((i=1; i<current_part; i++)); do
local patch_file="$series_dir/$i.mbox"
if [ ! -f "$patch_file" ]; then
echo "Error: Patch file $i does not exist in series"
return 1
fi
# Try to apply the patch
local apply_result=$(git am "$patch_file" 2>&1)
if [ $? -ne 0 ]; then
echo "Error: Failed to apply patch $i in series"
echo "Error details: $apply_result"
# Make sure to clean up
git am --abort >/dev/null 2>&1
return 1
fi
done
return 0
}
# Function to extract kernel versions from subject
extract_kernel_versions() {
local subject="$1"
local active_versions_file="$HOME/stable-queue/active_kernel_versions"
local found_versions=()
local range_start=""
local range_end=""
# First check for version ranges (e.g., "5.10-6.1" or "5.10.y-6.1.y" or "v5.10-v6.1" or "v6.1-v5.4")
if [[ "$subject" =~ (^|[^0-9.])(v)?([0-9]+\.[0-9]+)(\.y)?-(v)?([0-9]+\.[0-9]+)(\.y)?([^0-9.]|$) ]]; then
local ver1="${BASH_REMATCH[3]}"
local ver2="${BASH_REMATCH[6]}"
# Determine which is higher and lower for the range check
if [ "$(printf "%s\n%s" "$ver1" "$ver2" | sort -V | head -n1)" = "$ver2" ]; then
range_start="$ver2"
range_end="$ver1"
else
range_start="$ver1"
range_end="$ver2"
fi
# Read all versions and filter those within range
while IFS= read -r version; do
# Version is in range if:
# 1. It's >= range_start (version is not smallest when sorted with range_start)
# 2. It's <= range_end (version is not largest when sorted with range_end)
if [ "$(printf "%s\n%s" "$range_start" "$version" | sort -V | head -n1)" = "$range_start" ] && \
[ "$(printf "%s\n%s" "$version" "$range_end" | sort -V | head -n1)" = "$version" ]; then
found_versions+=("$version")
fi
done < "$active_versions_file"
fi
# If no range found, check for individual versions
if [ ${#found_versions[@]} -eq 0 ]; then
while IFS= read -r version; do
# Check for version numbers with optional v prefix and optional .y suffix
# Use a more strict pattern that requires the exact X.Y or X.Y.y format
if printf "%s" "$subject" | grep -E -q "(^|[^0-9.])${version}(\.y)?([^0-9.]|$)|(^|[[:space:]]|\[)v${version}(\.y)?([^0-9.]|$)"; then
found_versions+=("$version")
fi
done < "$active_versions_file"
fi
if [ ${#found_versions[@]} -gt 0 ]; then
# Sort versions in ascending order
result=$(printf "%s\n" "${found_versions[@]}" | sort -V | tr '\n' ' ' | sed 's/ $//')
echo "$result"
return 0
fi
cat "$active_versions_file"
}
# Function to extract commit SHA1 from email body
extract_commit_sha1() {
local email_body="$1"
local sha1=""
# First pattern: Look for "commit [SHA1] upstream" with case insensitivity
# Using -i flag makes grep ignore case when matching
sha1=$(echo "$email_body" | grep -i -E "commit [0-9a-f]{40} upstream" | \
sed -E 's/.*[Cc][Oo][Mm][Mm][Ii][Tt] ([0-9a-f]{40}) [Uu][Pp][Ss][Tt][Rr][Ee][Aa][Mm].*/\1/')
if [ -n "$sha1" ]; then
echo "$sha1"
return 0
fi
# Second pattern: Look for "[ Upstream commit [SHA1] ]" with case insensitivity
sha1=$(echo "$email_body" | grep -i -E "\[[[:space:]]*upstream[[:space:]]+commit[[:space:]]+[0-9a-f]{40}[[:space:]]*\]" | \
sed -E 's/.*\[[[:space:]]*[Uu][Pp][Ss][Tt][Rr][Ee][Aa][Mm][[:space:]]+[Cc][Oo][Mm][Mm][Ii][Tt][[:space:]]+([0-9a-f]{40})[[:space:]]*\].*/\1/')
if [ -n "$sha1" ]; then
echo "$sha1"
return 0
fi
return 1
}
# Function to extract patch author with normalization
extract_patch_author() {
local mbox_file="$1"
local author=$(formail -xFrom: < "$mbox_file")
normalize_author "$author"
}
# Function to get commit author with normalization
get_commit_author() {
local linux_dir="$1"
local sha1="$2"
cd "$linux_dir"
local author=$(git log -1 --format="%an <%ae>" "$sha1")
normalize_author "$author"
}
# Function to compare authors and check if they match
authors_match() {
local author1="$1"
local author2="$2"
# Normalize both authors
local norm1=$(normalize_author "$author1")
local norm2=$(normalize_author "$author2")
# Extract email parts for comparison
local email1=$(echo "$norm1" | grep -o '<[^>]*>' || echo "")
local email2=$(echo "$norm2" | grep -o '<[^>]*>' || echo "")
# If emails match and are non-empty, authors match
if [ -n "$email1" ] && [ "$email1" = "$email2" ]; then
return 0
fi
# If emails don't match or are empty, compare full normalized strings
[ "$norm1" = "$norm2" ]
}
# Function to get sorted kernel versions
get_sorted_versions() {
sort -rV "$HOME/stable-queue/active_kernel_versions"
}
# Function to check if version1 is newer than version2
is_version_newer() {
local v1="$1"
local v2="$2"
if [ "$(echo -e "$v1\n$v2" | sort -V | tail -n1)" = "$v1" ]; then
return 0
fi
return 1
}
# Function to check newer kernels for commit
check_newer_kernels() {
local sha1="$1"
local target_versions="$2"
local linux_dir="$3"
local -n c_results=$4
cd "$linux_dir"
local all_versions=($(get_sorted_versions))
local target_array=($target_versions)
local newest_target=${target_array[0]}
local temp_dir=$(mktemp -d)
local pids=()
local checked_versions=()
# Function to check a single branch and write results to temp file
check_single_branch() {
local version="$1"
local branch="pending-${version}"
local result_file="$temp_dir/$version"
# Check if branch exists
if ! git rev-parse --verify "$branch" >/dev/null 2>&1; then
echo "$version.y | Branch not found" > "$result_file"
return
fi
# Check if commit is an ancestor using merge-base
if [ "$(git merge-base "$branch" "$sha1")" = "$sha1" ]; then
echo "$version.y | Present (exact SHA1)" > "$result_file"
return
fi
# Try to find by subject if SHA1 not found
local subject
subject=$(git log -1 --format=%s "$sha1")
if [ -n "$subject" ]; then
# Search in the specific branch
local found_commit
found_commit=$(git log "$branch" --format=%H --grep="^${subject}$" -1)
if [ -n "$found_commit" ]; then
echo "$version.y | Present (different SHA1: ${found_commit:0:12})" > "$result_file"
else
echo "$version.y | Not found" > "$result_file"
fi
else
echo "$version.y | Not found" > "$result_file"
fi
}
# Launch parallel processes for each relevant version
for version in "${all_versions[@]}"; do
# Only check versions newer than our target using sort -V for proper semantic version comparison
# If version is newer than newest_target, it will be the second item when sorted
if [ "$(printf "%s\n%s" "$newest_target" "$version" | sort -V | head -n1)" = "$newest_target" ] && \
[ "$newest_target" != "$version" ]; then
check_single_branch "$version" &
pids+=($!)
checked_versions+=("$version")
fi
done
# Wait for all processes to complete
for pid in "${pids[@]}"; do
wait "$pid"
done
# Clear the results array
c_results=()
# Collect results in order (from newest to oldest)
for version in "${checked_versions[@]}"; do
if [ -f "$temp_dir/$version" ]; then
c_results+=("$(cat "$temp_dir/$version")")
fi
done
# Clean up
rm -rf "$temp_dir"
}
# Function to validate commit exists in upstream
validate_commit() {
local sha1="$1"
local linux_dir="$2"
cd "$linux_dir"
git merge-base --is-ancestor "$sha1" origin/master
}
# Function to compare patch with upstream
compare_with_upstream() {
local mbox_file="$1"
local sha1="$2"
local linux_dir="$3"
local series_dir="$4"
local current_part="$5"
local debug_output=()
if [[ ! $sha1 =~ ^[0-9a-f]{40}$ ]] || [ "$sha1" = "0000000000000000000000000000000000000000" ]; then
return 0
fi
cd "$linux_dir"
# Extract subject to determine target kernel versions
local subject=$(formail -xSubject: < "$mbox_file")
local kernel_versions=($(extract_kernel_versions "$subject"))
if [ ${#kernel_versions[@]} -eq 0 ]; then
debug_output+=("No kernel versions found in subject, cannot compare")
printf '%s\n' "${debug_output[@]}"
return 1
fi
# Use the newest version for comparison since it's closest to upstream
local version=${kernel_versions[0]}
local stable_branch="stable/linux-${version}.y"
local temp_branch="temp-compare-${version}-$(date +%s)"
# Create a worktree instead of checking out branches
local worktree_path=$(create_git_worktree "$stable_branch" "$temp_branch" "$linux_dir")
if [ -z "$worktree_path" ]; then
debug_output+=("Failed to create worktree for ${stable_branch}")
printf '%s\n' "${debug_output[@]}"
return 1
fi
cd "$worktree_path"
# Clean up any previous rebase-apply directory that might exist
if [ -d ".git/rebase-apply" ]; then
rm -rf ".git/rebase-apply"
fi
# Reset to ensure clean state
git reset --hard >/dev/null 2>&1
# If this is part of a series, apply previous patches first
if [ -n "$series_dir" ] && [ "$current_part" -gt 1 ]; then
if ! apply_series_patches "$series_dir" "$current_part" "$worktree_path"; then
debug_output+=("Failed to apply previous patches in series for comparison only.")
debug_output+=("This doesn't affect the main patch testing.")
cd "$linux_dir"
remove_git_worktree "$worktree_path" "$linux_dir"
printf '%s\n' "${debug_output[@]}"
return 1
fi
fi
# Try to apply the patch
local apply_result=$(git am "$mbox_file" 2>&1)
if [ $? -eq 0 ]; then
# Get the SHA1 of our newly applied patch
local new_sha1=$(git rev-parse HEAD)
debug_output+=("")
# Compare the ranges using range-diff
if ! git range-diff "${sha1}^".."$sha1" "${new_sha1}^".."$new_sha1"; then
debug_output+=("Failed to generate range-diff, but patch applies cleanly.")
fi
# Clean up after range-diff
cd "$linux_dir"
remove_git_worktree "$worktree_path" "$linux_dir"
else
# Clean up failed git-am
git am --abort >/dev/null 2>&1
cd "$linux_dir"
remove_git_worktree "$worktree_path" "$linux_dir"
debug_output+=("Note: Couldn't generate comparison with upstream commit.")
debug_output+=("This is just for the diff comparison and doesn't affect the patch application.")
debug_output+=("Error: $apply_result")
fi
printf '%s\n' "${debug_output[@]}"
}
# Function to test commit on a branch
test_commit_on_branch() {
local sha1="$1"
local version="$2"
local linux_dir="$3"
local mbox_file="$4"
local series_dir="$5"
local current_part="$6"
local -n results=$7
local -n errors=$8
local result=0
cd "$linux_dir"
local branch="stable/linux-${version}.y"
local temp_branch="temp-${version}-${sha1:0:8}"
# Create a worktree instead of checking out branches
local worktree_path=$(create_git_worktree "$branch" "$temp_branch" "$linux_dir")
if [ -z "$worktree_path" ]; then
results+=("stable/linux-${version}.y | Failed (branch not found) | N/A")
return 1
fi
cd "$worktree_path"
# Clean up any previous rebase-apply directory that might exist
if [ -d ".git/rebase-apply" ]; then
rm -rf ".git/rebase-apply"
fi
# Reset to ensure clean state
git reset --hard >/dev/null 2>&1
# Apply series patches if needed
if [ -n "$series_dir" ] && [ "$current_part" -gt 1 ]; then
if ! apply_series_patches "$series_dir" "$current_part" "$worktree_path"; then
results+=("stable/linux-${version}.y | Failed (series apply) | N/A")
cd "$linux_dir"
remove_git_worktree "$worktree_path" "$linux_dir"
return 1
fi
fi
# Apply current patch
local apply_result=$(git am "$mbox_file" 2>&1)
if [ $? -ne 0 ]; then
# Extract patch content and try to apply with --reject to get .rej files
local temp_patch=$(mktemp)
formail -I "" < "$mbox_file" | sed '1,/^$/d' > "$temp_patch"
git apply --reject "$temp_patch" >/dev/null 2>&1
# Find and read any .rej files
local reject_content=""
while IFS= read -r -d '' rej_file; do
reject_content+=$'\n'"$(cat "$rej_file")"
rm -f "$rej_file"
done < <(find . -name "*.rej" -print0)
rm -f "$temp_patch"
# Clean up
git checkout -f >/dev/null 2>&1
git am --abort >/dev/null 2>&1
results+=("stable/linux-${version}.y | Failed | N/A")
if [ -n "$reject_content" ]; then
errors+=("Patch failed to apply on ${branch}. Reject:")
errors+=("$reject_content")
else
errors+=("Patch failed to apply on ${branch}: $apply_result")
fi
cd "$linux_dir"
remove_git_worktree "$worktree_path" "$linux_dir"
return 1
fi
# Run build test
local build_output
build_output=$(stable build log 2>&1)
local build_ret=$?
if [ $build_ret -ne 0 ]; then
results+=("stable/linux-${version}.y | Success | Failed")
if [ -n "$build_output" ]; then
errors+=("Build error for ${branch}:")
errors+=("$(echo "$build_output" | sed 's/^/ /')")
errors+=("")
else
errors+=("Build error for ${branch}: (no output captured)")
fi
result=1
else
results+=("stable/linux-${version}.y | Success | Success")
fi
cd "$linux_dir"
remove_git_worktree "$worktree_path" "$linux_dir"
return $result
}
# Function to check for fixes referencing a commit
check_fixes_for_commit() {
local sha1="$1"
local linux_dir="$2"
local -n result_array=$3
cd "$linux_dir"
# Look for commits with Fixes: tag pointing to our commit
local fixes_commits=$(git log origin/master --grep="Fixes: ${sha1:0:12}" --format="%H %s")
if [ -n "$fixes_commits" ]; then
result_array+=("Found fixes commits:")
while IFS= read -r line; do
local fix_sha1="${line%% *}"
local fix_subject="${line#* }"
result_array+=("${fix_sha1:0:12} ${fix_subject}")
done <<< "$fixes_commits"
fi
}
# Function to check if commit was reverted
check_if_reverted() {
local sha1="$1"
local linux_dir="$2"
local -n result_array=$3
cd "$linux_dir"
# Look for revert commits in subject and body
local revert_commits=$(git log origin/master --grep="This reverts commit ${sha1:0:12}\|^Revert \".*${sha1:0:12}.*\"" --format="%H %s")
# Also look for Fixes: tags in revert commits
local fixes_reverts=$(git log origin/master --grep="^Revert.*\|Fixes: ${sha1:0:12}" --format="%H %B" | \
awk -v sha="$sha1" '
/^[0-9a-f]{40}/ { commit=$1; subject=$0; sub(/^[0-9a-f]{40}[[:space:]]*/, "", subject); }
/^Revert/ && /Fixes: '"${sha1:0:12}"'/ { print commit " " subject; }
')
# Combine both results
local all_reverts=$(printf "%s\n%s" "$revert_commits" "$fixes_reverts" | sort -u)
if [ -n "$all_reverts" ]; then
result_array+=("Found revert commits:")
while IFS= read -r line; do
if [ -n "$line" ]; then
local revert_sha1="${line%% *}"
local revert_subject="${line#* }"
result_array+=("${revert_sha1:0:12} ${revert_subject}")
fi
done <<< "$all_reverts"
fi
}
# Function to process single patch or series patch
process_patch() {
local mbox_file="$1"
local series_dir="$2"
local current_part="$3"
local -n p_results=$4
local -n p_errors=$5
local worktree_path="$6" # Optional worktree path when called from test_series
local failed=0
# Extract series info first
local subject=$(formail -xSubject: < "$mbox_file")
local series_info=$(extract_series_info "$subject")
local is_series_part=0
local total_parts=0
if [ -n "$series_info" ]; then
read current_part total_parts <<< "$series_info"
is_series_part=1
fi
# If this is part of a series and not the first patch, verify previous patches can be applied
# Skip this check if we're called from test_series with a provided worktree
if [ -n "$series_dir" ] && [ "$current_part" -gt 1 ] && [ -z "$worktree_path" ]; then
# Try to apply previous patches in a temporary worktree
cd "$LINUX_DIR"
local temp_branch="temp-series-check-$(date +%s)"
# Create a worktree
worktree_path=$(create_git_worktree "HEAD" "$temp_branch" "$LINUX_DIR")
if [ -z "$worktree_path" ]; then
p_results+=("All branches | Failed (could not create worktree) | N/A")
p_errors+=("Error: Cannot proceed - failed to create worktree")
return 1
fi
if ! apply_series_patches "$series_dir" "$current_part" "$worktree_path"; then
cd "$LINUX_DIR"
remove_git_worktree "$worktree_path" "$LINUX_DIR"
p_results+=("All branches | Failed (previous patches in series failed to apply) | N/A")
p_errors+=("Error: Cannot proceed - previous patches in series failed to apply")
p_errors+=("This is part ${current_part}/${total_parts} of a series.")
p_errors+=("Please ensure all previous patches in the series apply cleanly.")
return 1
fi
cd "$LINUX_DIR"
remove_git_worktree "$worktree_path" "$LINUX_DIR"
worktree_path="" # Reset to empty after cleanup
fi
# Extract subject to get kernel versions
local subject=$(formail -xSubject: < "$mbox_file")
local kernel_versions=$(extract_kernel_versions "$subject")
# Extract email body
local email_body=$(formail -I "" < "$mbox_file")
local claimed_sha1=$(extract_commit_sha1 "$email_body" || true)
local found_sha1=""
local author_mismatch=""
local diff_output=""
# Find or validate SHA1
if [ -z "$claimed_sha1" ]; then
found_sha1=$(find_commit_by_subject "$subject" "$LINUX_DIR")
else
if validate_commit "$claimed_sha1" "$LINUX_DIR"; then
found_sha1="$claimed_sha1"
else
found_sha1=$(find_commit_by_subject "$subject" "$LINUX_DIR")
fi
fi
# Use temporary SHA1 if none found
if [ -z "$found_sha1" ]; then
found_sha1="0000000000000000000000000000000000000000"
fi
# Compare authors if we found a commit
if [ -n "$found_sha1" ] && [ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
local patch_author=$(extract_patch_author "$mbox_file")
local commit_author=$(get_commit_author "$LINUX_DIR" "$found_sha1")
if ! authors_match "$patch_author" "$commit_author"; then
author_mismatch="Backport author: $patch_author
Commit author: $commit_author"
fi
fi
# Compare with upstream if we have a valid SHA1
if [[ "$found_sha1" =~ ^[0-9a-f]{40}$ ]] && [ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
diff_output=$(compare_with_upstream "$mbox_file" "$found_sha1" "$LINUX_DIR" "$series_dir" "$current_part")
fi
# Test on each kernel version
# Skip individual testing if we're called with a worktree (as part of test_series)
if [ -z "$worktree_path" ]; then
for version in $kernel_versions; do
if ! test_commit_on_branch "$found_sha1" "$version" "$LINUX_DIR" "$mbox_file" \
"$series_dir" "$current_part" p_results p_errors; then
failed=1
fi
done
else
# If we have a worktree provided, just test applying the patch to it
cd "$worktree_path"
# Get the actual branch name for reporting
local branch_name=""
# First try to extract from the worktree path which should have the format temp-series-VERSION-timestamp
if [[ "$worktree_path" =~ temp-series-([0-9]+\.[0-9]+)- ]]; then
branch_name="${BASH_REMATCH[1]}"
# Fallback to checking the git branch
else
# Get current git branch
branch_name=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD)
# Extract version if it's in the branch name
if [[ "$branch_name" =~ linux-([0-9]+\.[0-9]+)\.y ]]; then
branch_name="${BASH_REMATCH[1]}"
fi
fi
# Default if we couldn't determine the branch
local full_branch_name="Current branch"
if [ -n "$branch_name" ]; then
full_branch_name="stable/linux-${branch_name}.y"
fi
# Clean up any previous rebase-apply directory that might exist
if [ -d ".git/rebase-apply" ]; then
rm -rf ".git/rebase-apply"
fi
# Reset to ensure clean state
git reset --hard >/dev/null 2>&1
# Try to apply the patch
local apply_result=$(git am "$mbox_file" 2>&1)
if [ $? -ne 0 ]; then
p_results+=("$full_branch_name | Failed to apply | N/A")
p_errors+=("Error applying patch to worktree: $apply_result")
git am --abort >/dev/null 2>&1
failed=1
else
# Run build test
local build_output
build_output=$(stable build log 2>&1)
local build_ret=$?
if [ $build_ret -ne 0 ]; then
p_results+=("$full_branch_name | Success | Failed")
if [ -n "$build_output" ]; then
p_errors+=("Build error:")
p_errors+=("$(echo "$build_output" | sed 's/^/ /')")
p_errors+=("")
else
p_errors+=("Build error: (no output captured)")
fi
failed=1
else
p_results+=("$full_branch_name | Success | Success")
fi
fi
fi
# Check newer kernels if we have a valid SHA1
local -a newer_kernel_results=()
if [[ "$found_sha1" =~ ^[0-9a-f]{40}$ ]] && [ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
check_newer_kernels "$found_sha1" "$kernel_versions" "$LINUX_DIR" newer_kernel_results
fi
# Check for fixes and reverts if we have a valid SHA1
local -a fixes_results=()
local -a revert_results=()
if [[ "$found_sha1" =~ ^[0-9a-f]{40}$ ]] && [ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
check_fixes_for_commit "$found_sha1" "$LINUX_DIR" fixes_results
check_if_reverted "$found_sha1" "$LINUX_DIR" revert_results
fi
# Generate response for this patch
generate_response "$mbox_file" "$claimed_sha1" "$found_sha1" \
"$(printf '%s\n' "${p_results[@]}")" "$diff_output" \
"$author_mismatch" "$(printf '%s\n' "${p_errors[@]}")" \
"$(printf '%s\n' "${newer_kernel_results[@]}")" \
"$(printf '%s\n' "${fixes_results[@]}")" \
"$(printf '%s\n' "${revert_results[@]}")"
return $failed
}
# Function to test complete series
test_series() {
local series_dir="$1"
local total_parts="$2"
local linux_dir="$3"
local failed=0
# Get the first patch to determine target kernel versions
local first_patch="$series_dir/1.mbox"
if [ ! -f "$first_patch" ]; then
echo "Error: First patch in series not found: $first_patch"
return 1
fi
# Extract subject and kernel versions from first patch
local subject=$(formail -xSubject: < "$first_patch")
local kernel_versions=$(extract_kernel_versions "$subject")
if [ -z "$kernel_versions" ]; then
echo "Error: Cannot determine target kernel versions from patch subject"
return 1
fi
# Process each kernel version
for version in $kernel_versions; do
local stable_branch="stable/linux-${version}.y"
echo "Testing series on $stable_branch..."
# Process each patch in the series
for ((i=1; i<=total_parts; i++)); do
local mbox_file="$series_dir/$i.mbox"
declare -a patch_results=()
declare -a patch_errors=()
# Create a worktree for this branch
# Use a naming convention that includes the version for easier extraction later
local temp_branch="temp-series-${version}-$(date +%s)"
local worktree_path=$(create_git_worktree "$stable_branch" "$temp_branch" "$linux_dir")
if [ -z "$worktree_path" ]; then
echo "Error: Failed to create worktree for branch $stable_branch"
failed=1
continue
fi
# If not the first patch, apply previous patches
if [ $i -gt 1 ]; then
if ! apply_series_patches "$series_dir" "$i" "$worktree_path"; then
echo "Error: Failed to apply previous patches in series on $stable_branch"
remove_git_worktree "$worktree_path" "$linux_dir"
failed=1
break
fi
fi
# Now process the current patch
if ! process_patch "$mbox_file" "$series_dir" "$i" patch_results patch_errors "$worktree_path"; then
failed=1
remove_git_worktree "$worktree_path" "$linux_dir"
break
fi
# Clean up worktree after testing each patch
remove_git_worktree "$worktree_path" "$linux_dir"
done
done
return $failed
}
# Function to generate email response
generate_response() {
local mbox_file="$1"
local claimed_sha1="$2"
local found_sha1="$3"
local results="$4"
local diff_output="$5"
local author_mismatch="$6"
local build_errors="$7"
local newer_kernel_results="$8"
local fixes_results="$9"
local revert_results="${10}"
local response_file=$(generate_response_filename "$mbox_file")
# Check if this is a stable-only commit
local email_body=$(formail -I "" < "$mbox_file")
local is_stable_only=0
if echo "$email_body" | grep -i "stable.only" >/dev/null 2>&1; then
is_stable_only=1
fi
{
# Get the From, Subject, Message-ID, and Date from original email for threading
formail -X From: -X Subject: < "$mbox_file"
echo "Message-ID: $(generate_message_id)"
echo "Date: $(date -R)"
echo "In-Reply-To: $(formail -xMessage-ID: < "$mbox_file")"
echo "From: $(git config user.name) <$(git config user.email)>"
# Use original subject line directly from the email, just add Re: if needed
local orig_subject=$(formail -xSubject: < "$mbox_file" | \
tr '\n' ' ' | \
tr -s ' ' | \
sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g')
# Only add "Re: " if it's not already there
if [[ ! "$orig_subject" =~ ^Re: ]]; then
echo "Subject: Re: $orig_subject"
else
echo "Subject: $orig_subject"
fi
# Add summary section to determine if there are issues
local has_issues=0
local summary=()
local is_series_part=0
local series_info=$(extract_series_info "$orig_subject")
if [ -n "$series_info" ]; then
read current_part total_parts <<< "$series_info"
is_series_part=1
if [ "$current_part" -gt 1 ]; then
summary+=("ℹ️ This is part ${current_part}/${total_parts} of a series")
fi
fi
# Check for build failures
if [[ "$results" == *"| Failed"* ]]; then
has_issues=1
summary+=("❌ Build failures detected")
fi
# Check for missing or unverified commit (skip for stable-only commits)
if [ $is_stable_only -eq 0 ]; then
if [ -z "$found_sha1" ] || [ "$found_sha1" = "0000000000000000000000000000000000000000" ]; then
has_issues=1
summary+=("⚠️ Could not find matching upstream commit")
elif [ -n "$claimed_sha1" ] && [ "$claimed_sha1" != "$found_sha1" ]; then
has_issues=1
summary+=("⚠️ Provided upstream commit SHA1 does not match found commit")
elif [ -z "$claimed_sha1" ] && [ -n "$found_sha1" ]; then
has_issues=1
summary+=("⚠️ Found matching upstream commit but patch is missing proper reference to it")
fi
fi
# Check for fixes
if [ -n "$fixes_results" ]; then
has_issues=1
summary+=("⚠️ Found follow-up fixes in mainline")
fi
# Check for reverts
if [ -n "$revert_results" ]; then
has_issues=1
summary+=("❌ Commit was reverted in mainline")
fi
# Add appropriate email headers based on whether there are issues
if [ $has_issues -eq 1 ]; then
# If there are issues, send to author and CC stable
local author_email=$(formail -xFrom: < "$mbox_file" | sed -e 's/.*<\([^>]*\)>.*/\1/')
echo "To: $author_email"
echo "Cc: stable@vger.kernel.org"
else
# If no issues, send only to stable
echo "To: stable@vger.kernel.org"
fi
echo
echo "[ Sasha's backport helper bot ]"
echo
echo "Hi,"
echo
# Print summary section
if [ $has_issues -eq 1 ]; then
echo "Summary of potential issues:"
printf '%s\n' "${summary[@]}"
echo
else
# Check if patch applies and builds successfully
if [[ "$results" == *"| Success | Success"* ]]; then
echo "✅ All tests passed successfully. No issues detected."
echo "No action required from the submitter."
echo
fi
fi
# Report on SHA1 verification and commit status
if [ $is_stable_only -eq 1 ]; then
echo "Note: This is a stable-only commit."
elif [ -n "$claimed_sha1" ]; then
if [ "$claimed_sha1" = "$found_sha1" ]; then
echo "The upstream commit SHA1 provided is correct: $claimed_sha1"
if [ -n "$author_mismatch" ]; then
echo
echo "WARNING: Author mismatch between patch and upstream commit:"
echo "$author_mismatch"
fi
else
echo "The claimed upstream commit SHA1 ($claimed_sha1) was not found."
if [ -n "$found_sha1" ] && [ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
echo "However, I found a matching commit: $found_sha1"
if [ -n "$author_mismatch" ]; then
echo
echo "WARNING: Author mismatch between patch and found commit:"
echo "$author_mismatch"
fi
fi
fi
elif [ -n "$found_sha1" ] && [ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
echo "Found matching upstream commit: $found_sha1"
if [ -n "$author_mismatch" ]; then
echo
echo "WARNING: Author mismatch between patch and found commit:"
echo "$author_mismatch"
fi
else
echo "No upstream commit was identified. Using temporary commit for testing."
fi
echo
# Add newer kernel check results if available
if [ -n "$newer_kernel_results" ] && [ "${#newer_kernel_results[@]}" -gt 0 ]; then
echo "Status in newer kernel trees:"
printf '%s\n' "${newer_kernel_results[@]}"
echo
fi
# Add fixes and revert information if available
if [ -n "$fixes_results" ]; then
echo "$fixes_results"
echo
fi
if [ -n "$revert_results" ]; then
echo "$revert_results"
echo
fi
# Add diff if there are differences and we have a valid SHA1
if [ -n "$diff_output" ] && [[ "$found_sha1" =~ ^[0-9a-f]{40}$ ]] && \
[ "$found_sha1" != "0000000000000000000000000000000000000000" ]; then
# Check if this is just a failure message rather than actual diff
if [[ "$diff_output" == *"Couldn't generate comparison"* ]]; then
echo "Note: Could not generate a diff with upstream commit:"
else
echo "Note: The patch differs from the upstream commit:"
fi
echo "---"
echo "$diff_output"
echo "---"
echo
fi
# Print results table
if [ $is_series_part -eq 1 ] && [ "$current_part" -gt 1 ] && [ $has_issues -eq 1 ]; then
echo "NOTE: These results are for this patch alone. Full series testing will be"
echo "performed when all parts are received."
echo
fi
echo "Results of testing on various branches:"
echo
printf "| %-25s | %-11s | %-10s |\n" "Branch" "Patch Apply" "Build Test"
echo "|---------------------------|-------------|------------|"
while IFS='|' read -r branch status build; do
if [ -n "$branch" ]; then
printf "| %-25s | %-11s | %-10s |\n" "$branch" "$status" "$build"
fi
done <<< "$results"
# Add build errors if any
if [ -n "$build_errors" ]; then
echo
echo "Build Errors:"
echo "$build_errors"
fi
} > "$response_file"
echo "Response written to $response_file"
}
# Cleanup function to remove any remaining worktrees
cleanup() {
if [ -d "$LINUX_DIR" ]; then
cd "$LINUX_DIR"
# Remove any worktrees we created
if [ -d "$WORKTREE_DIR" ]; then
for worktree in "$WORKTREE_DIR"/*; do
if [ -d "$worktree" ]; then
# Try to cleanup any git operations first
if [ -d "$worktree/.git" ]; then
cd "$worktree" 2>/dev/null
if [ $? -eq 0 ]; then
# Abort any in-progress git operations
git am --abort >/dev/null 2>&1
git rebase --abort >/dev/null 2>&1
git reset --hard >/dev/null 2>&1
cd "$LINUX_DIR"
fi
fi
# Now try to remove via git
if ! git worktree remove --force "$worktree" >/dev/null 2>&1; then
# If that fails, use rm -rf
rm -rf "$worktree"
fi
fi
done
# Try to remove the directory
rm -rf "$WORKTREE_DIR" 2>/dev/null || true
# Prune any worktree references
git worktree prune >/dev/null 2>&1
fi
# Clean up any temp patch file
rm -f "$TEMP_PATCH" 2>/dev/null || true
fi
}
# Main script
main() {
if [ $# -ne 1 ]; then
echo "Usage: $0 <mbox_file>"
exit 1
fi
MBOX_FILE="$1"
local diff_output=""
local failed=0
echo "Looking at $MBOX_FILE"
# Validate inputs
if [ ! -f "$MBOX_FILE" ]; then
echo "Error: File '$MBOX_FILE' not found"
exit 1
fi
if [ ! -d "$LINUX_DIR" ]; then
echo "Error: Linux git tree not found at $LINUX_DIR"
exit 1
fi
if [ ! -f "$HOME/stable-queue/active_kernel_versions" ]; then
echo "Error: Active kernel versions file not found at ~/stable-queue/active_kernel_versions"
exit 1
fi
# Check if we should ignore this mail
if should_ignore_mail "$MBOX_FILE"; then
echo "Skipping mail from ignored author"
exit 0
fi
# Check if mail contains a git patch
if ! is_git_patch "$MBOX_FILE"; then
echo "Skipping mail: not a git patch"
exit 0
fi
# Extract subject and series information
subject=$(formail -xSubject: < "$MBOX_FILE")
if [ -z "$subject" ]; then
echo "Error: Could not extract subject from mbox"
exit 1
fi
# Get kernel versions to process
kernel_versions=$(extract_kernel_versions "$subject")
local has_specific_versions=0
if [ "$kernel_versions" != "$(cat $HOME/stable-queue/active_kernel_versions)" ]; then
has_specific_versions=1
fi
# Extract email body
email_body=$(formail -I "" < "$MBOX_FILE")
# Try to find SHA1 in the email body
claimed_sha1=$(extract_commit_sha1 "$email_body" || true)
found_sha1=""
# If we didn't find SHA1 in the body, try to find it by subject
if [ -z "$claimed_sha1" ]; then
found_sha1=$(find_commit_by_subject "$subject" "$LINUX_DIR")
else
if validate_commit "$claimed_sha1" "$LINUX_DIR"; then
found_sha1="$claimed_sha1"
else
found_sha1=$(find_commit_by_subject "$subject" "$LINUX_DIR")
fi
fi
# Skip if we have no SHA1 and no specific kernel versions
if [ -z "$found_sha1" ] && [ $has_specific_versions -eq 0 ]; then
echo "No commit SHA1 found and no specific kernel versions in subject. Skipping patch."
exit 0
fi
# Check if this is part of a series
series_info=$(extract_series_info "$subject")
if [ -n "$series_info" ]; then
read current_part total_parts <<< "$series_info"
# Skip 0/N patches
if [ "$current_part" -eq 0 ]; then
echo "Skipping 0/$total_parts patch"
exit 0
fi
# Get message IDs and determine series directory
message_id=$(get_message_id "$MBOX_FILE")
in_reply_to=$(get_in_reply_to "$MBOX_FILE")
series_dir="$PENDING_DIR/$(get_series_dir "$message_id" "$in_reply_to")"
# Store this patch
store_patch "$MBOX_FILE" "$series_dir" "$current_part"
echo "Processing part $current_part of $total_parts"
# Check if series is now complete
if is_series_complete "$series_dir" "$total_parts"; then
echo "Series complete, testing all patches..."
if ! test_series "$series_dir" "$total_parts" "$LINUX_DIR"; then
echo "Series testing failed. See logs for details."
failed=1
else
echo "Series testing completed successfully."
fi
# Clean up series directory regardless of result
rm -rf "$series_dir"
exit $failed
else
echo "Series incomplete, waiting for remaining patches..."
echo "Processed part $current_part of $total_parts"
exit 0
fi
else
# Single patch processing
declare -a patch_results=()
declare -a patch_errors=()
if ! process_patch "$MBOX_FILE" "" "1" patch_results patch_errors; then
failed=1
fi
fi
exit $failed
}
# Set up trap for cleanup
trap cleanup EXIT ERR
# Run main script
main "$@"