commit 6481ee1a1932de236b7583b49a69ea299fb4b7b2 Author: github-actions[bot] Date: Mon Apr 6 16:12:02 2026 +0000 Add version v1.0.2 to VCC index diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d6037e3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: VCC Release Automation + +on: + push: + tags: + - "v*" # Startet bei v1.0.2, v1.1.0 etc. + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract Metadata + id: metadata + run: | + VERSION=$(jq -r .version package.json) + PKG_NAME=$(jq -r .name package.json) + DISPLAY_NAME=$(jq -r .displayName package.json) + DESCRIPTION=$(jq -r .description package.json) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "pkg_name=$PKG_NAME" >> $GITHUB_OUTPUT + echo "display_name=$DISPLAY_NAME" >> $GITHUB_OUTPUT + echo "description=$DESCRIPTION" >> $GITHUB_OUTPUT + + - name: Create VCC Package Zip + run: | + # Wir packen nur die relevanten Dateien ohne den Repo-Hauptordner + zip -r ${{ steps.metadata.outputs.pkg_name }}-${{ github.ref_name }}.zip package.json README.md Editor/ Editor.meta package.json.meta README.md.meta + + - name: Upload GitHub Release Asset + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.metadata.outputs.pkg_name }}-${{ github.ref_name }}.zip + generate_release_notes: true + draft: false + prerelease: false + + - name: Update VCC Index + run: | + # Wechsel zum gh-pages Branch (oder erstelle ihn, falls er fehlt) + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + git fetch origin gh-pages || git checkout --orphan gh-pages + git checkout gh-pages || git checkout -b gh-pages + + # Falls die index.json noch nicht existiert, Basis-Struktur anlegen + if [ ! -f index.json ]; then + echo '{"name":"mrunknownde VCC Repo","id":"de.mrunknownde.vccrepo","url":"https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/index.json","author":"mrunknownde","packages":{}}' > index.json + fi + + # Neue Version in die index.json injizieren + ZIP_URL="https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ steps.metadata.outputs.pkg_name }}-${{ github.ref_name }}.zip" + + jq --arg ver "${{ steps.metadata.outputs.version }}" \ + --arg name "${{ steps.metadata.outputs.pkg_name }}" \ + --arg disp "${{ steps.metadata.outputs.display_name }}" \ + --arg desc "${{ steps.metadata.outputs.description }}" \ + --arg url "$ZIP_URL" \ + '.packages[$name].versions[$ver] = { + "name": $name, + "version": $ver, + "displayName": $disp, + "description": $desc, + "unity": "2022.3", + "url": $url + }' index.json > temp.json && mv temp.json index.json + + git add index.json + git commit -m "Add version ${{ github.ref_name }} to VCC index" + git push origin gh-pages \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..448624f --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +vcc.mrunk.de \ No newline at end of file diff --git a/Editor/GitDiffViewer.cs b/Editor/GitDiffViewer.cs new file mode 100644 index 0000000..ce54da0 --- /dev/null +++ b/Editor/GitDiffViewer.cs @@ -0,0 +1,77 @@ +using UnityEditor; +using UnityEngine; + +public class GitDiffViewer : EditorWindow +{ + private string filePath; + private string diffContent; + private Vector2 scrollPos; + + private readonly Color colorAddBg = new Color(0.18f, 0.35f, 0.18f, 0.6f); + private readonly Color colorRemoveBg = new Color(0.4f, 0.15f, 0.15f, 0.6f); + private readonly Color colorHeaderBg = new Color(0.2f, 0.3f, 0.5f, 0.5f); + + public static void ShowWindow(string path, string status) + { + GitDiffViewer window = GetWindow("Diff Viewer"); + window.minSize = new Vector2(700, 500); + window.LoadDiff(path, status); + } + + private void LoadDiff(string path, string status) + { + filePath = path; + + if (status.Contains("??")) + { + diffContent = $"--- NEW FILE (Untracked) ---\n\n{System.IO.File.ReadAllText(path)}"; + } + else + { + diffContent = GitPanel.RunGitCommand($"diff HEAD -- \"{path}\""); + if (string.IsNullOrWhiteSpace(diffContent)) + { + diffContent = "No text-based changes found (possibly a binary file or empty diff)."; + } + } + } + + private void OnGUI() + { + GUILayout.Space(10); + GUILayout.Label($" File Diff: {filePath}", EditorStyles.largeLabel); + GUILayout.Space(10); + + scrollPos = EditorGUILayout.BeginScrollView(scrollPos, "box"); + + GUIStyle lineStyle = new GUIStyle(EditorStyles.label); + lineStyle.richText = true; + lineStyle.wordWrap = false; + lineStyle.fontSize = 12; + + string[] lines = diffContent.Split(new[] { '\n' }); + + foreach (string line in lines) + { + Rect rect = EditorGUILayout.GetControlRect(false, 18); + + if (line.StartsWith("+") && !line.StartsWith("+++")) + { + EditorGUI.DrawRect(rect, colorAddBg); + } + else if (line.StartsWith("-") && !line.StartsWith("---")) + { + EditorGUI.DrawRect(rect, colorRemoveBg); + } + else if (line.StartsWith("@@")) + { + EditorGUI.DrawRect(rect, colorHeaderBg); + } + + string displayLine = line.Replace("\t", " "); + GUI.Label(rect, $"{displayLine}", lineStyle); + } + + EditorGUILayout.EndScrollView(); + } +} \ No newline at end of file diff --git a/Editor/GitPanel.cs b/Editor/GitPanel.cs new file mode 100644 index 0000000..2679bf7 --- /dev/null +++ b/Editor/GitPanel.cs @@ -0,0 +1,278 @@ +using UnityEditor; +using UnityEngine; +using System.Diagnostics; +using System.IO; +using System.Collections.Generic; + +public class GitPanel : EditorWindow +{ + private string commitMessage = ""; + private bool hasRepo = false; + private string[] changedFiles = new string[0]; + private Vector2 scrollPositionChanges; + private Vector2 scrollPositionHistory; + + private int selectedTab = 0; + private string[] tabNames = { "Changes", "History" }; + + private struct CommitInfo { public string hash; public string date; public string message; } + private List commitHistory = new List(); + + [MenuItem("Tools/Git-Tool")] + public static void ShowWindow() + { + GitPanel window = GetWindow("Source Control"); + window.minSize = new Vector2(350, 500); + } + + private void OnEnable() + { + CheckRepoStatus(); + SetDefaultCommitMessage(); + } + + private void SetDefaultCommitMessage() + { + commitMessage = $"Auto-Save: {System.DateTime.Now:yyyy-MM-dd HH:mm:ss}"; + } + + private void OnGUI() + { + GUILayout.Space(10); + GUILayout.Label("SOURCE CONTROL", EditorStyles.boldLabel); + GUILayout.Space(5); + + if (!hasRepo) + { + RenderInitUI(); + return; + } + + selectedTab = GUILayout.Toolbar(selectedTab, tabNames, GUILayout.Height(25)); + GUILayout.Space(10); + + if (selectedTab == 0) RenderGitUI(); + else RenderHistoryUI(); + } + + private void RenderInitUI() + { + EditorGUILayout.HelpBox("No local Git repository found. Initialize current project folder?", MessageType.Warning); + if (GUILayout.Button("Initialize Repository", GUILayout.Height(30))) + { + RunGitCommand("init"); + GenerateUnityGitIgnore(); + RunGitCommand("add .gitignore"); + RunGitCommand("commit -m \"Initial commit (GitIgnore)\""); + CheckRepoStatus(); + } + } + + private void RenderGitUI() + { + commitMessage = EditorGUILayout.TextField(commitMessage, GUILayout.Height(25)); + + GUI.backgroundColor = new Color(0.2f, 0.4f, 0.8f); + if (GUILayout.Button("✓ Commit & Push", GUILayout.Height(30))) + { + if (string.IsNullOrWhiteSpace(commitMessage)) commitMessage = $"Auto-Save: {System.DateTime.Now:yyyy-MM-dd HH:mm:ss}"; + + RunGitCommand("add ."); + RunGitCommand($"commit -m \"{commitMessage}\""); + RunGitCommand("push"); + + SetDefaultCommitMessage(); + CheckRepoStatus(); + UnityEngine.Debug.Log("Git-Tool: Changes successfully pushed!"); + } + GUI.backgroundColor = Color.white; + + GUILayout.Space(10); + EditorGUILayout.HelpBox("Legend: [M] Modified | [A] Added | [D] Deleted | [??] Untracked", MessageType.Info); + GUILayout.Space(5); + + EditorGUILayout.BeginHorizontal(); + GUILayout.Label($"CHANGES ({changedFiles.Length})", EditorStyles.boldLabel); + if (GUILayout.Button("â†ģ", GUILayout.Width(25))) + { + CheckRepoStatus(); + SetDefaultCommitMessage(); + } + EditorGUILayout.EndHorizontal(); + + scrollPositionChanges = EditorGUILayout.BeginScrollView(scrollPositionChanges, "box"); + + if (changedFiles.Length == 0) GUILayout.Label("No unsaved changes."); + else RenderFileList(changedFiles); + + EditorGUILayout.EndScrollView(); + } + + private void RenderHistoryUI() + { + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("LAST COMMITS (Click to open in Browser)", EditorStyles.boldLabel); + if (GUILayout.Button("â†ģ", GUILayout.Width(25))) FetchHistory(); + EditorGUILayout.EndHorizontal(); + GUILayout.Space(5); + + if (commitHistory.Count == 0) + { + GUILayout.Label("No commits found."); + return; + } + + scrollPositionHistory = EditorGUILayout.BeginScrollView(scrollPositionHistory, "box"); + + foreach (var commit in commitHistory) + { + Rect rect = EditorGUILayout.GetControlRect(false, 22); + + if (rect.Contains(Event.current.mousePosition)) + { + EditorGUI.DrawRect(rect, new Color(1f, 1f, 1f, 0.1f)); + } + + GUIStyle textStyle = new GUIStyle(EditorStyles.label) { richText = true }; + GUI.Label(rect, $"{commit.hash} | {commit.date} | {commit.message}", textStyle); + + Event e = Event.current; + if (e.type == EventType.MouseDown && e.button == 0 && rect.Contains(e.mousePosition)) + { + OpenCommitInBrowser(commit.hash); + e.Use(); + } + GUILayout.Space(2); + } + + EditorGUILayout.EndScrollView(); + } + + private void RenderFileList(string[] files) + { + foreach (string line in files) + { + if (line.Length < 4) continue; + + string status = line.Substring(0, 1).Trim() == "" ? line.Substring(0, 2) : line.Substring(0, 1); + string path = line.Substring(line.IndexOf('\t') + 1 > 0 ? line.IndexOf('\t') + 1 : 3).Trim(); + if (path.StartsWith("\"") && path.EndsWith("\"")) path = path.Substring(1, path.Length - 2); + + Rect rect = EditorGUILayout.GetControlRect(false, 18); + if (rect.Contains(Event.current.mousePosition)) EditorGUI.DrawRect(rect, new Color(1f, 1f, 1f, 0.1f)); + + GUIStyle labelStyle = new GUIStyle(EditorStyles.label); + GUI.Label(rect, $"[{status.Trim()}] {path}", labelStyle); + + Event e = Event.current; + if (e.isMouse && e.button == 0 && rect.Contains(e.mousePosition) && e.type == EventType.MouseDown) + { + if (e.clickCount == 1) PingAsset(path); + else if (e.clickCount == 2) GitDiffViewer.ShowWindow(path, status); + e.Use(); + } + } + } + + private void OpenCommitInBrowser(string hash) + { + string remoteUrl = RunGitCommand("config --get remote.origin.url").Trim(); + if (string.IsNullOrEmpty(remoteUrl)) + { + UnityEngine.Debug.LogWarning("Git-Tool: No remote repository configured (origin missing)."); + return; + } + + string webUrl = remoteUrl; + + if (webUrl.StartsWith("git@")) + { + webUrl = webUrl.Replace(":", "/").Replace("git@", "https://"); + } + + if (webUrl.EndsWith(".git")) + { + webUrl = webUrl.Substring(0, webUrl.Length - 4); + } + + Application.OpenURL($"{webUrl}/commit/{hash}"); + } + + private void CheckRepoStatus() + { + string projectPath = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + hasRepo = Directory.Exists(Path.Combine(projectPath, ".git")); + + if (hasRepo) + { + string output = RunGitCommand("status -s"); + changedFiles = string.IsNullOrWhiteSpace(output) ? new string[0] : output.Split(new[] { '\n', '\r' }, System.StringSplitOptions.RemoveEmptyEntries); + FetchHistory(); + } + } + + private void FetchHistory() + { + commitHistory.Clear(); + string output = RunGitCommand("log -n 25 --pretty=format:\"%h|%cd|%s\" --date=short"); + if (!string.IsNullOrWhiteSpace(output)) + { + string[] lines = output.Split(new[] { '\n', '\r' }, System.StringSplitOptions.RemoveEmptyEntries); + foreach (string line in lines) + { + string[] parts = line.Split('|'); + if (parts.Length >= 3) + { + commitHistory.Add(new CommitInfo { hash = parts[0], date = parts[1], message = parts[2] }); + } + } + } + } + + private void PingAsset(string relativePath) + { + UnityEngine.Object obj = AssetDatabase.LoadAssetAtPath(relativePath); + if (obj != null) { Selection.activeObject = obj; EditorGUIUtility.PingObject(obj); } + } + + private void GenerateUnityGitIgnore() + { + string path = Path.Combine(Application.dataPath, "../.gitignore"); + if (!File.Exists(path)) + { + string content = @".idea +.vs +bin +obj +*.sln.DotSettings.user +/Library +/Temp +/UserSettings +/Configs +/*.csproj +/*.sln +/Logs +/Packages/* +!/Packages/manifest.json +!/Packages/packages-lock.json +!/Packages/vpm-manifest.json +~UnityDirMonSyncFile~*"; + File.WriteAllText(path, content); + UnityEngine.Debug.Log(".gitignore generated successfully!"); + } + } + + public static string RunGitCommand(string arguments) + { + try + { + ProcessStartInfo startInfo = new ProcessStartInfo("git", arguments) + { + WorkingDirectory = Path.GetFullPath(Path.Combine(Application.dataPath, "..")), + UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true + }; + using (Process p = Process.Start(startInfo)) { p.WaitForExit(); return p.StandardOutput.ReadToEnd(); } + } + catch { return ""; } + } +} \ No newline at end of file diff --git a/Editor/de.mrunknownde.gittool.Editor.asmdef b/Editor/de.mrunknownde.gittool.Editor.asmdef new file mode 100644 index 0000000..3247678 --- /dev/null +++ b/Editor/de.mrunknownde.gittool.Editor.asmdef @@ -0,0 +1,16 @@ +{ + "name": "de.mrunknownde.gittool.Editor", + "rootNamespace": "", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88911c3 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# đŸ› ī¸ Unity Git Control Tool (VRChat Ready) + +A lightweight, integrated Source Control Panel built directly into Unity. Designed to eliminate the constant context-switching between the Unity Editor and external command-line tools. Perfectly tailored for VRChat World Creators and developers who want to maintain clean version control without the bloat. + +## ✨ Features +- **One-Click Init:** Initializes a new repository and automatically generates a clean Unity `.gitignore` file. +- **VS Code Style Interface:** Compact overview of modified, added, deleted, and untracked files. +- **Auto-Timestamp Commits:** If you don't provide a custom commit message, the tool gracefully falls back to a clean timestamp format. +- **Interactive File Explorer:** - `Single Click` on a file -> Pings and focuses the asset in the Unity Project View. + - `Double Click` on a file -> Opens the built-in Code Diff Viewer right inside the Editor. +- **History View:** Browse your latest commits. Click any commit to open it directly in your remote web view (Gitea, GitHub, GitLab). + +## 🚀 Installation via VRChat Creator Companion (VCC) + +You can add this tool as a custom package directly into your VCC. + +1. Open the VRChat Creator Companion. +2. Navigate to **Settings** -> **Packages**. +3. Click on **Add Repository**. +4. Enter your custom repo URL: `https://vcc.mrunk.de/index.json` +5. In your project views, under "Manage Project", the **VRChat Git Control Tool** will now appear. Simply click the plus icon to add it. + +## đŸ› ī¸ Manual Installation +1. Download the latest version as a `.zip` archive. +2. Extract the folder. +3. Place the folder directly into your Unity project's `Packages` directory. + *Alternative:* Copy the `.cs` files from the `Editor` folder into any `Editor` folder inside your `Assets` directory. + +## đŸ•šī¸ Usage +Once installed, open the tool via the top menu bar in Unity: +`Tools` -> `Git-Tool` + +A floating window will appear. You can easily dock this window into your custom layout (e.g., right next to the Inspector). + +## âš ī¸ Prerequisites +- **Git** must be installed on your system and added to your global environment variables (`PATH`). +- For automatic pushes to Gitea/GitHub to work seamlessly, you should have **SSH keys** or cached credentials configured. Unity cannot intercept terminal password prompts. \ No newline at end of file diff --git a/index.json b/index.json new file mode 100644 index 0000000..10a1210 --- /dev/null +++ b/index.json @@ -0,0 +1,28 @@ +{ + "name": "MrUnknownDE's VCC Tools", + "id": "de.mrunknownde.vccrepo", + "url": "https://vcc.mrunk.de/index.json", + "author": "mrunknownde", + "packages": { + "de.mrunknownde.gittool": { + "versions": { + "1.0.1": { + "name": "de.mrunknownde.gittool", + "version": "1.0.1", + "displayName": "VRChat Git Control Tool", + "url": "https://github.com/MrUnknownDE/unity-gittool/archive/refs/tags/v1.0.1.zip", + "unity": "2022.3", + "description": "A lightweight, integrated Git panel for Unity." + }, + "1.0.0": { + "name": "de.mrunknownde.gittool", + "version": "1.0.0", + "displayName": "VRChat Git Control Tool", + "description": "A lightweight, integrated Git panel for Unity. Ideal for VRChat World/Avatar Creators to easily push commits directly from the editor to Gitea/GitHub/GitLab and other Git hosting services.", + "unity": "2022.3", + "url": "https://github.com/MrUnknownDE/unity-vcc/releases/download/v1.0.2/de.mrunknownde.gittool-v1.0.2.zip" + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b9f20bb --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "de.mrunknownde.gittool", + "version": "1.0.0", + "displayName": "VRChat Git Control Tool", + "description": "A lightweight, integrated Git panel for Unity. Ideal for VRChat World/Avatar Creators to easily push commits directly from the editor to Gitea/GitHub/GitLab and other Git hosting services.", + "unity": "2022.3", + "author": { + "name": "mrunknownde" + }, + "dependencies": {}, + "vpmDependencies": {} +} \ No newline at end of file