Files
trail-into-darkness/Packages/com.jovian.unitypackagesync/Editor/PackageSyncWindow.cs
Sebastian Bularca 36d3f112ef added zlinq
2026-04-02 07:43:33 +02:00

320 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Newtonsoft.Json;
using UnityEditor.Callbacks;
namespace Jovian.PackageSync {
public sealed class PackageSyncWindow : EditorWindow {
// private static PackageMapping[] defaultPackages = new PackageMapping[] {
// new(
// "Save System",
// "Packages/com.jovian.savesystem",
// @"D:\repos\unity-save-system"),
// new(
// "Zone System",
// "Packages/com.jovian.zonesystem",
// @"D:\repos\unity-zone-system")
// };
private static string FilePath => Path.Combine(Directory.GetParent(Application.dataPath).FullName, "ProjectSettings/PackageSyncSettings.json");
private Vector2 scrollPosition;
private bool hasChanges;
private bool syncEnabled;
private PackageMapping[] currentPackages = Array.Empty<PackageMapping>();
[MenuItem("Jovian/Utilities/Package Sync")]
private static void ShowWindow() {
var window = GetWindow<PackageSyncWindow>("Package Sync");
window.minSize = new Vector2(400, 300);
}
[DidReloadScripts]
private static void OnScriptsReloaded() {
if (EditorPrefs.GetBool("PackageSync.AutoSync", false)) {
PushToRepos();
}
}
private static void PushToRepos() {
var packages = LoadSettings();
foreach(var package in packages) {
SyncFiles(package.packagePath, package.repoPath, package.displayName);
}
}
private static void SaveSettings(PackageMapping[] data) {
var json = JsonConvert.SerializeObject(data);
File.WriteAllText(FilePath, json);
}
private static PackageMapping[] LoadSettings() {
if(!File.Exists(FilePath)) {
File.Create(FilePath).Close();
File.WriteAllText(FilePath, string.Empty);
return Array.Empty<PackageMapping>();
}
var json = File.ReadAllText(FilePath);
return JsonConvert.DeserializeObject<PackageMapping[]>(json);
}
private void OnEnable() {
currentPackages = LoadSettings();
}
private void OnGUI() {
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.Space(8);
EditorGUI.BeginChangeCheck();
syncEnabled = EditorGUILayout.ToggleLeft("Enable Package Sync", syncEnabled);
if(EditorGUI.EndChangeCheck()) {
EditorPrefs.SetBool("PackageSync.AutoSync", syncEnabled);
if(syncEnabled) {
PushToRepos();
}
}
EditorGUILayout.Space(4);
EditorGUILayout.HelpBox(
"Sync files between Unity packages and their standalone git repos.",
MessageType.Info);
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Add Packages", EditorStyles.boldLabel);
foreach(var package in currentPackages) {
var set = new List<string> {
package.displayName,
package.packagePath,
package.repoPath
};
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginChangeCheck();
set[0] = EditorGUILayout.DelayedTextField(set[0]);
set[1] = EditorGUILayout.TextField(set[1]);
set[2] = EditorGUILayout.TextField(set[2]);
package.selected = EditorGUILayout.Toggle(package.selected, GUILayout.Width(20));
if(EditorGUI.EndChangeCheck()) {
package.displayName = set[0];
package.packagePath = set[1];
package.repoPath = set[2];
hasChanges = true;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(4);
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button("Add Package")) {
var package = new PackageMapping(string.Empty, string.Empty, string.Empty);
currentPackages = currentPackages.Append(package).ToArray();
SaveSettings(currentPackages);
}
if(GUILayout.Button("Remove Selected")) {
currentPackages = currentPackages.Where(p => !p.selected).ToArray();
SaveSettings(currentPackages);
}
EditorGUILayout.EndHorizontal();
if(hasChanges) {
SaveSettings(currentPackages);
}
EditorGUILayout.Space(8);
foreach(var package in currentPackages) {
DrawPackageSection(package);
EditorGUILayout.Space(12);
}
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Bulk Actions", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button("Push All to Repos", GUILayout.Height(28))) {
foreach(var package in currentPackages) {
SyncFiles(package.packagePath, package.repoPath, package.displayName);
}
}
if(GUILayout.Button("Pull All from Repos", GUILayout.Height(28))) {
foreach(var package in currentPackages) {
SyncFiles(package.repoPath, package.packagePath, package.displayName);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndScrollView();
}
private void DrawPackageSection(PackageMapping package) {
EditorGUILayout.LabelField(package.displayName, EditorStyles.boldLabel);
var packageExists = Directory.Exists(package.packagePath);
var repoExists = Directory.Exists(package.repoPath);
EditorGUILayout.LabelField("Package", packageExists ? package.packagePath : $"{package.packagePath} (missing)");
EditorGUILayout.LabelField("Repo", repoExists ? package.repoPath : $"{package.repoPath} (missing)");
if(!packageExists || !repoExists) {
EditorGUILayout.HelpBox(
$"Cannot sync: {(!packageExists ? "package" : "repo")} directory not found.",
MessageType.Warning);
return;
}
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button("Push to Repo →")) {
SyncFiles(package.packagePath, package.repoPath, package.displayName);
}
if(GUILayout.Button("← Pull from Repo")) {
SyncFiles(package.repoPath, package.packagePath, package.displayName);
}
EditorGUILayout.EndHorizontal();
if(GUILayout.Button("Show Differences")) {
ShowDifferences(package);
}
}
private static void SyncFiles(string sourcePath, string destPath, string displayName) {
var fullSource = Path.GetFullPath(sourcePath);
var fullDest = Path.GetFullPath(destPath);
var sourceFiles = Directory.GetFiles(fullSource, "*", SearchOption.AllDirectories)
.Where(f => !IsGitPath(f) && !f.EndsWith(".meta"))
.ToArray();
var copied = 0;
var skipped = 0;
foreach(var sourceFile in sourceFiles) {
var relativePath = sourceFile.Substring(fullSource.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var destFile = Path.Combine(fullDest, relativePath);
if(IsFileIdentical(sourceFile, destFile)) {
skipped++;
continue;
}
var destDir = Path.GetDirectoryName(destFile);
if(!Directory.Exists(destDir)) {
if(destDir != null) {
Directory.CreateDirectory(destDir);
}
}
File.Copy(sourceFile, destFile, true);
copied++;
}
// Remove files in dest that no longer exist in source
var destFiles = Directory.GetFiles(fullDest, "*", SearchOption.AllDirectories)
.Where(f => !IsGitPath(f) && !f.EndsWith(".meta"))
.ToArray();
var deleted = 0;
foreach(var destFile in destFiles) {
var relativePath = destFile.Substring(fullDest.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var sourceFile = Path.Combine(fullSource, relativePath);
if(!File.Exists(sourceFile)) {
File.Delete(destFile);
deleted++;
}
}
var direction = fullSource == Path.GetFullPath(destPath) ? "Pulled" : "Pushed";
Debug.Log($"[PackageSync] {displayName}: {direction} {copied} files, {skipped} unchanged, {deleted} removed.");
AssetDatabase.Refresh();
}
private static void ShowDifferences(PackageMapping package) {
var fullPackage = Path.GetFullPath(package.packagePath);
var fullRepo = Path.GetFullPath(package.repoPath);
var packageFiles = Directory.GetFiles(fullPackage, "*", SearchOption.AllDirectories)
.Where(f => !IsGitPath(f) && !f.EndsWith(".meta"))
.Select(f => f.Substring(fullPackage.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
.ToArray();
var repoFiles = Directory.GetFiles(fullRepo, "*", SearchOption.AllDirectories)
.Where(f => !IsGitPath(f) && !f.EndsWith(".meta"))
.Select(f => f.Substring(fullRepo.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
.ToArray();
var onlyInPackage = packageFiles.Except(repoFiles).ToArray();
var onlyInRepo = repoFiles.Except(packageFiles).ToArray();
var common = packageFiles.Intersect(repoFiles).ToArray();
var modified = 0;
foreach(var file in common) {
var packageFile = Path.Combine(fullPackage, file);
var repoFile = Path.Combine(fullRepo, file);
if(!IsFileIdentical(packageFile, repoFile)) {
Debug.Log($"[PackageSync] Modified: {file}");
modified++;
}
}
foreach(var file in onlyInPackage) {
Debug.Log($"[PackageSync] Only in package: {file}");
}
foreach(var file in onlyInRepo) {
Debug.Log($"[PackageSync] Only in repo: {file}");
}
var total = modified + onlyInPackage.Length + onlyInRepo.Length;
if(total == 0) {
Debug.Log($"[PackageSync] {package.displayName}: In sync — no differences found.");
}
else {
Debug.Log($"[PackageSync] {package.displayName}: {modified} modified, {onlyInPackage.Length} only in package, {onlyInRepo.Length} only in repo.");
}
}
private static bool IsGitPath(string path) {
return path.Replace('\\', '/').Contains("/.git/") || path.Replace('\\', '/').Contains("/.git");
}
private static bool IsFileIdentical(string fileA, string fileB) {
if(!File.Exists(fileA) || !File.Exists(fileB)) {
return false;
}
var infoA = new FileInfo(fileA);
var infoB = new FileInfo(fileB);
if(infoA.Length != infoB.Length) {
return false;
}
var bytesA = File.ReadAllBytes(fileA);
var bytesB = File.ReadAllBytes(fileB);
return bytesA.SequenceEqual(bytesB);
}
private sealed class PackageMapping {
public string displayName;
public string packagePath;
public string repoPath;
public bool selected;
public PackageMapping(string displayName, string packagePath, string repoPath) {
this.displayName = displayName;
this.packagePath = packagePath;
this.repoPath = repoPath;
}
}
}
}