Files
saturnclient/scripts/port.sh
T
selimaj-devandclaude eebfe8fa4d
Check / compile (pull_request) Successful in 7m47s
Switch the root build scripts to Kotlin and limit parallel remaps
- settings.gradle, stonecutter.gradle and the root build.gradle become
  .kts. Loom's version for the Stonecutter versions moves to the root
  gradle.properties (loom_version), read by pluginManagement, since the
  Kotlin plugins block can't read properties. The old mc-<v> projects keep
  their Groovy scripts until they're folded in.
- buildAll ran all eight remapJar tasks at once. Each loads a Minecraft
  classpath and remaps a ~50 MB jar, which doesn't fit in the 3 GB daemon
  heap: a cold build stalled in garbage collection for 40+ minutes, and
  earlier runs failed. A shared build service now lets two remaps run
  together; a cold buildAll takes about 2 minutes.
- port.sh and VERSION_GUIDE follow the .kts file names.

The 1.21.11 jar is the same as with the Groovy scripts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 04:51:28 +02:00

185 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Port a change made in one Minecraft version's code to the other versions.
#
# Takes the diff under one version's source folder and applies it to the others
# with `git apply --3way`: it applies cleanly where the files match, and leaves
# normal merge conflicts only where a version really differs. Only source is
# ported; build.gradle and gradle.properties are version-specific on purpose.
#
# Versions moved to Stonecutter (stonecutterVersions in settings.gradle.kts)
# share the top-level src/, which counts as one version: the active one set in
# stonecutter.gradle.kts. The others keep their own copy in versions/<v>/src.
#
# Usage:
# scripts/port.sh [--from VERSION] [--to V1,V2,...] [--commit REV] [--dry-run]
#
# --from version the change was made in. Default: the only version with
# uncommitted changes (or changed by --commit).
# --to comma-separated versions to port to. Default: all others.
# --commit port the changes a commit made instead of uncommitted changes.
# --dry-run report what would happen without changing anything.
#
# Exit status: 0 if every version applied cleanly (or already had the change),
# 1 if any version has conflicts or failed.
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
from=""
to=""
commit=""
dry_run=false
while [ $# -gt 0 ]; do
case "$1" in
--from) from="$2"; shift 2 ;;
--to) to="$2"; shift 2 ;;
--commit) commit="$2"; shift 2 ;;
--dry-run) dry_run=true; shift ;;
-h|--help) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Unknown option: $1 (see --help)" >&2; exit 2 ;;
esac
done
# Versions sharing the Stonecutter src/, and the one that stands for them.
stonecutter_versions=$(sed -n 's/^val stonecutterVersions = listOf(\(.*\))/\1/p' settings.gradle.kts | tr -d '" ' | tr ',' ' ')
stonecutter_active=$(sed -n 's/^stonecutter active "\(.*\)"/\1/p' stonecutter.gradle.kts 2>/dev/null || true)
is_stonecutter() {
printf '%s\n' $stonecutter_versions | grep -qx "$1"
}
# Where a version's source lives.
src_of() {
if is_stonecutter "$1"; then echo "src"; else echo "versions/$1/src"; fi
}
old_versions=$(for d in versions/*/; do [ -d "$d/src" ] && basename "$d"; done)
all_versions=$(printf '%s\n' $old_versions $stonecutter_active | grep . | sort -t. -k1,1n -k2,2n -k3,3n)
version_exists() {
[ -d "$(src_of "$1")" ]
}
# Versions with changes, from the working tree or from the given commit.
changed_versions() {
if [ -n "$commit" ]; then
git show --name-only --format= "$commit" -- versions/ src/
else
git diff HEAD --name-only -- versions/ src/
git ls-files --others --exclude-standard -- versions/ src/
fi | sed -n -e 's#^versions/\([^/]*\)/src/.*#\1#p' -e "s#^src/.*#${stonecutter_active}#p" | sort -u
}
if [ -z "$from" ]; then
candidates=$(changed_versions)
count=$(printf '%s\n' "$candidates" | grep -c . || true)
if [ "$count" -ne 1 ]; then
echo "Changes found in ${count} versions (${candidates//$'\n'/, }); pass --from to choose one." >&2
exit 2
fi
from="$candidates"
fi
version_exists "$from" || { echo "No such version: $from" >&2; exit 2; }
if is_stonecutter "$from"; then
from="$stonecutter_active"
fi
from_src=$(src_of "$from")
if [ -z "$to" ]; then
to=$(printf '%s\n' $all_versions | grep -vx "$from" | paste -sd, -)
fi
# New files are only included in `git diff HEAD` once git knows about them.
if [ -z "$commit" ]; then
untracked=$(git ls-files --others --exclude-standard -- "$from_src")
if [ -n "$untracked" ]; then
if $dry_run; then
echo "note: new files in $from_src are untracked and won't be ported in a dry run:" >&2
printf ' %s\n' $untracked >&2
else
printf '%s\n' "$untracked" | xargs git add --intent-to-add --
fi
fi
fi
# --full-index gives --3way the real blob ids to merge against.
if [ -n "$commit" ]; then
patch=$(git show --full-index --binary --format= "$commit" -- "$from_src")
else
patch=$(git diff HEAD --full-index --binary -- "$from_src")
fi
if [ -z "$patch" ]; then
echo "No changes under $from_src to port." >&2
exit 2
fi
files=$(printf '%s\n' "$patch" | grep -c '^diff --git' || true)
echo "Porting $files file(s) from $from${commit:+ (commit $commit)}"
from_re=$(printf '%s' "$from_src" | sed 's/\./\\./g')
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
status=0
IFS=',' read -r -a targets <<< "$to"
for target in "${targets[@]}"; do
if [ "$target" = "$from" ]; then
continue
fi
if is_stonecutter "$target" && [ "$target" != "$stonecutter_active" ]; then
printf ' %-8s skipped: shares src/ with %s\n' "$target" "$stonecutter_active"
[ "$from" = "$stonecutter_active" ] || status=1
continue
fi
if ! version_exists "$target"; then
printf ' %-8s skipped: no such version\n' "$target"
status=1
continue
fi
# Point the patch's file headers at the target version's source folder.
target_src=$(src_of "$target")
printf '%s\n' "$patch" | sed -E "/^(diff --git |--- |\+\+\+ |rename (from|to) |copy (from|to) )/ s#([ab]/|(from|to) )${from_re}/#\1${target_src}/#g" > "$tmp/$target.patch"
if git apply --check --reverse "$tmp/$target.patch" 2>/dev/null; then
printf ' %-8s already has this change\n' "$target"
continue
fi
if $dry_run; then
if git apply --check "$tmp/$target.patch" 2>/dev/null; then
printf ' %-8s would apply cleanly\n' "$target"
else
printf ' %-8s would need a 3-way merge (possible conflicts)\n' "$target"
fi
continue
fi
if output=$(git apply --3way "$tmp/$target.patch" 2>&1); then
printf ' %-8s applied\n' "$target"
else
conflicts=$(git diff --name-only --diff-filter=U -- "$target_src")
if [ -n "$conflicts" ]; then
printf ' %-8s CONFLICTS in:\n' "$target"
printf '%s\n' "$conflicts" | sed 's/^/ /'
else
printf ' %-8s FAILED:\n' "$target"
printf '%s\n' "$output" | sed 's/^/ /'
fi
status=1
fi
done
if ! $dry_run; then
echo
echo "Ported changes are staged in the target versions. Resolve any conflicts, then build"
echo "the affected versions, e.g. ./gradlew compileJava"
fi
exit $status