forked from Shardstone/trail-into-darkness
First commit on my server, yey!
This commit is contained in:
8
Packages/com.jovian.unitypackagesync/Editor.meta
Normal file
8
Packages/com.jovian.unitypackagesync/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c085a3cfa0ca4949ac2b368ccb9bade
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Jovian.PackageSync.Editor",
|
||||
"rootNamespace": "Jovian.PackageSync",
|
||||
"references": [],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27826bbfce0730c4eb507ed69a3c6e77
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
319
Packages/com.jovian.unitypackagesync/Editor/PackageSyncWindow.cs
Normal file
319
Packages/com.jovian.unitypackagesync/Editor/PackageSyncWindow.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
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/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac2844b690dc31a418b0670c82f6365f
|
||||
15
Packages/com.jovian.unitypackagesync/package.json
Normal file
15
Packages/com.jovian.unitypackagesync/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "com.jovian.unitypackagesync",
|
||||
"version": "0.1.0",
|
||||
"displayName": "Jovian Package Sync",
|
||||
"description": "Editor tool for syncing local Unity packages with standalone git repositories.",
|
||||
"unity": "2022.3",
|
||||
"keywords": [
|
||||
"package",
|
||||
"sync",
|
||||
"git"
|
||||
],
|
||||
"author": {
|
||||
"name": "Jovian"
|
||||
}
|
||||
}
|
||||
7
Packages/com.jovian.unitypackagesync/package.json.meta
Normal file
7
Packages/com.jovian.unitypackagesync/package.json.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cc17f0cce3469044a0ff0038183354c
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user