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]>
185 lines
6.3 KiB
Bash
Executable File
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
|