Files
saturnclient/scripts/port.sh
T
selimaj-devandclaude 019e5cfb43 Build 1.21.11 with Stonecutter from a shared src/
Step 1 of #8. 1.21.11's version-specific code moves from versions/1.21.11/src
to the top-level src/, built by Stonecutter as the project :1.21.11. The other
versions still build from their own copies as :mc-<v> until they're folded in.

- settings.gradle: applies Stonecutter (Groovy controller) with
  stonecutterVersions = ["1.21.11"] and skips those folders when including
  the old mc-<v> projects.
- The old root build.gradle becomes stonecutter.gradle (the root project's
  script, with the shared repositories, common wiring and buildAll), and
  1.21.11's build.gradle becomes the root build.gradle that Stonecutter runs
  for each version. versions/1.21.11 keeps its gradle.properties and run/.
- The jar has the same 415 entries, fabric.mod.json and mixin configs as
  before; the only class differences also appear when rebuilding an
  untouched version today.
- scripts/port.sh treats src/ as the active Stonecutter version, so changes
  port between it and the old copies both ways.
- README and VERSION_GUIDE describe the layout, version comments and how to
  add a version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 04:18:42 +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) share
# the top-level src/, which counts as one version: the active one set in
# stonecutter.gradle. 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/^def stonecutterVersions = \[\(.*\)\]/\1/p' settings.gradle | tr -d '" ' | tr ',' ' ')
stonecutter_active=$(sed -n 's/^stonecutter\.active "\(.*\)"/\1/p' stonecutter.gradle 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