├── .docs └── img │ ├── context_hierarchy.jpg │ ├── context_project.jpg │ ├── lockswindow.PNG │ ├── preferences.PNG │ └── username.PNG ├── Editor.meta ├── Editor ├── Scripts.meta ├── Scripts │ ├── GitLocks.cs │ ├── GitLocks.cs.meta │ ├── GitLocksAssetPostprocessor.cs │ ├── GitLocksAssetPostprocessor.cs.meta │ ├── GitLocksDisplay.cs │ ├── GitLocksDisplay.cs.meta │ ├── GitLocksFileModificationWarning.cs │ ├── GitLocksFileModificationWarning.cs.meta │ ├── GitLocksObject.cs │ ├── GitLocksObject.cs.meta │ ├── GitLocksPreferences.cs │ └── GitLocksPreferences.cs.meta ├── Textures.meta ├── Textures │ ├── greenLock.png │ ├── greenLock.png.meta │ ├── greenLock_cb.png │ ├── greenLock_cb.png.meta │ ├── mixedLock.png │ ├── mixedLock.png.meta │ ├── mixedLock_cb.png │ ├── mixedLock_cb.png.meta │ ├── orangeLock.png │ ├── orangeLock.png.meta │ ├── orangeLock_cb.png │ ├── orangeLock_cb.png.meta │ ├── redLock.png │ ├── redLock.png.meta │ ├── redLock_cb.png │ └── redLock_cb.png.meta ├── tomduchene.unity-git-locks.editor.asmdef └── tomduchene.unity-git-locks.editor.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── package.json └── package.json.meta /.docs/img/context_hierarchy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/.docs/img/context_hierarchy.jpg -------------------------------------------------------------------------------- /.docs/img/context_project.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/.docs/img/context_project.jpg -------------------------------------------------------------------------------- /.docs/img/lockswindow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/.docs/img/lockswindow.PNG -------------------------------------------------------------------------------- /.docs/img/preferences.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/.docs/img/preferences.PNG -------------------------------------------------------------------------------- /.docs/img/username.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/.docs/img/username.PNG -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7f131ace80ee4b14cbb46174be777942 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bff6d47978284334e9b86d1481426c18 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocks.cs: -------------------------------------------------------------------------------- 1 | // All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using Newtonsoft.Json.Linq; 8 | using UnityEditor; 9 | using UnityEngine; 10 | using UnityEngine.SceneManagement; 11 | 12 | [InitializeOnLoad] 13 | public class GitLocks : ScriptableObject 14 | { 15 | private static List lockedObjectsCache; 16 | private static List uncommitedFilesCache; 17 | private static List modifiedOnServerFilesCache; 18 | private static bool uncommitedFilesCacheDirty = true; 19 | private static List conflictWarningIgnoreList; 20 | private static DateTime lastRefresh = DateTime.MinValue; 21 | private static bool currentlyRefreshing = false; 22 | private static string gitVersion; 23 | 24 | private static float requestTimeout = 30; // seconds 25 | 26 | private static string refreshCallbackResult; 27 | private static string refreshCallbackError; 28 | 29 | // Ignore meta files to check lockable/unlockable files 30 | static string[] ignoredExtensions = new[] { ".meta" }; 31 | 32 | static GitLocks() 33 | { 34 | EditorApplication.update += Update; 35 | EditorApplication.wantsToQuit += WantsToQuit; 36 | 37 | // Preferences default 38 | if (!EditorPrefs.HasKey("gitLocksEnabled")) 39 | { 40 | EditorPrefs.SetBool("gitLocksEnabled", true); 41 | } 42 | 43 | if (!EditorPrefs.HasKey("gitLocksAutoRefreshLocks")) 44 | { 45 | EditorPrefs.SetBool("gitLocksAutoRefreshLocks", true); 46 | } 47 | 48 | if (!EditorPrefs.HasKey("gitLocksRefreshLocksInterval")) 49 | { 50 | EditorPrefs.SetInt("gitLocksRefreshLocksInterval", 5); 51 | } 52 | 53 | if (!EditorPrefs.HasKey("gitLocksMaxFilesNumPerRequest")) 54 | { 55 | EditorPrefs.SetInt("gitLocksMaxFilesNumPerRequest", 15); 56 | } 57 | 58 | if (!EditorPrefs.HasKey("displayLocksConflictWarning")) 59 | { 60 | EditorPrefs.SetBool("displayLocksConflictWarning", true); 61 | } 62 | 63 | if (!EditorPrefs.HasKey("warnIfIStillOwnLocksOnQuit")) 64 | { 65 | EditorPrefs.SetBool("warnIfIStillOwnLocksOnQuit", true); 66 | } 67 | 68 | if (!EditorPrefs.HasKey("warnIfFileHasBeenModifiedOnServer")) 69 | { 70 | EditorPrefs.SetBool("warnIfFileHasBeenModifiedOnServer", true); 71 | } 72 | 73 | if (!EditorPrefs.HasKey("notifyNewLocks")) 74 | { 75 | EditorPrefs.SetBool("notifyNewLocks", false); 76 | } 77 | 78 | if (!EditorPrefs.HasKey("numOfMyLocksDisplayed")) 79 | { 80 | EditorPrefs.SetInt("numOfMyLocksDisplayed", 5); 81 | } 82 | 83 | if (!EditorPrefs.HasKey("gitLocksColorblindMode")) 84 | { 85 | EditorPrefs.SetBool("gitLocksColorblindMode", false); 86 | } 87 | 88 | if (!EditorPrefs.HasKey("gitLocksDebugMode")) 89 | { 90 | EditorPrefs.SetBool("gitLocksDebugMode", false); 91 | } 92 | 93 | if (!EditorPrefs.HasKey("gitLocksShowForceButtons")) 94 | { 95 | EditorPrefs.SetBool("gitLocksShowForceButtons", false); 96 | } 97 | 98 | conflictWarningIgnoreList = new List(); 99 | 100 | GetGitVersion(); 101 | } 102 | 103 | // Properties 104 | public static List LockedObjectsCache => lockedObjectsCache; 105 | 106 | public static DateTime LastRefresh => lastRefresh; 107 | 108 | public static bool CurrentlyRefreshing => currentlyRefreshing; 109 | 110 | // Methods 111 | public static void CheckLocksRefresh() 112 | { 113 | if (EditorPrefs.GetBool("gitLocksAutoRefreshLocks", true) && DateTime.Now > lastRefresh.AddMinutes(EditorPrefs.GetInt("gitLocksRefreshLocksInterval", 5)) && !EditorApplication.isCompiling && !EditorApplication.isUpdating) 114 | { 115 | RefreshLocks(); 116 | } 117 | } 118 | 119 | public static void RefreshLocks() 120 | { 121 | lastRefresh = DateTime.Now; 122 | 123 | // Get the locks asynchronously 124 | currentlyRefreshing = true; 125 | ExecuteNonBlockingProcessTerminal("git", "lfs locks --json"); 126 | } 127 | 128 | public static void RefreshCallback(string result) 129 | { 130 | // If empty result, start a simple git lfs locks (no json) to catch potential errors 131 | if (result == "[]") 132 | { 133 | ExecuteNonBlockingProcessTerminal("git", "lfs locks"); 134 | } 135 | 136 | // Check that we're receiving what seems to be a JSON result 137 | if (result[0] == '[') 138 | { 139 | // Save old locks 140 | List oldLocks; 141 | if (lockedObjectsCache != null) 142 | { 143 | oldLocks = new List(lockedObjectsCache); 144 | } 145 | else 146 | { 147 | oldLocks = new List(); 148 | } 149 | 150 | // Parse the string into a nice list of objects 151 | lockedObjectsCache = ParseJsonIntoLockedObjects(result); 152 | 153 | // Check if we should warn the user about conflicts between uncommited and locked files 154 | BuildUncommitedCache(); 155 | if (GetDisplayLocksConflictWarning()) 156 | { 157 | string conflictMessage = "The following files are currently locked and you have uncommited changes on them that you'll probably not be able to push:"; 158 | bool conflictFound = false; 159 | foreach (GitLocksObject lo in lockedObjectsCache) 160 | { 161 | if (IsLockedObjectConflictingWithUncommitedFile(lo) && !IsFileInConflictIgnoreList(lo.path)) 162 | { 163 | conflictFound = true; 164 | conflictMessage += "\n" + lo.path; 165 | AddFileToConflictWarningIgnoreList(lo.path); 166 | } 167 | } 168 | if (conflictFound) 169 | { 170 | EditorUtility.DisplayDialog("Warning", conflictMessage, "OK"); 171 | } 172 | } 173 | 174 | // Remove files from ignored warnings list if they're not locked anymore 175 | for (int i = conflictWarningIgnoreList.Count - 1; i >= 0; i--) 176 | { 177 | GitLocksObject lo = GetObjectInLockedCache(conflictWarningIgnoreList[i]); 178 | if (lo == null || lo.IsMine()) 179 | { 180 | conflictWarningIgnoreList.RemoveAt(i); 181 | } 182 | } 183 | 184 | // Check if we should warn the user that there are new locks 185 | if (EditorPrefs.GetBool("notifyNewLocks", false)) 186 | { 187 | string newLocksString = string.Empty; 188 | foreach (GitLocksObject potentialNewLock in lockedObjectsCache) 189 | { 190 | // Was the lock already there ? 191 | bool lockAlreadyThere = false; 192 | foreach (GitLocksObject lo in oldLocks) 193 | { 194 | if (lo.path == potentialNewLock.path) 195 | { 196 | lockAlreadyThere = true; 197 | break; 198 | } 199 | } 200 | 201 | if (!lockAlreadyThere && potentialNewLock.owner.name != EditorPrefs.GetString("gitLocksHostUsername")) 202 | { 203 | if (newLocksString != string.Empty) 204 | { 205 | newLocksString += "\n"; 206 | } 207 | 208 | newLocksString += "[" + potentialNewLock.owner.name + "] " + potentialNewLock.path; 209 | } 210 | } 211 | 212 | if (newLocksString != string.Empty) 213 | { 214 | EditorUtility.DisplayDialog("New locks", newLocksString, "OK"); 215 | } 216 | } 217 | 218 | // Sort the locks to show mine first 219 | if (lockedObjectsCache.Count > 0) 220 | { 221 | lockedObjectsCache.Sort(delegate (GitLocksObject a, GitLocksObject b) 222 | { 223 | // ^ is exclusive OR, compare if only one is equal to the git username 224 | if (a.IsMine() ^ b.IsMine()) 225 | { 226 | return a.IsMine() ? -1 : 1; // Put my locks first 227 | } 228 | else 229 | { 230 | return a.path.CompareTo(b.path); // When it's the same owner, sort by path 231 | } 232 | }); 233 | } 234 | } 235 | 236 | GitLocksDisplay.RepaintAll(); 237 | } 238 | 239 | public static string ExecuteProcessTerminal(string processName, string processArguments, out string errorString, bool openTerminal = false) 240 | { 241 | DebugLog("ExecuteProcessTerminal: " + processName + " with the following parameters:\n" + processArguments); 242 | 243 | if (openTerminal) 244 | { 245 | Process p = new Process(); 246 | ProcessStartInfo psi = new ProcessStartInfo(); 247 | psi.FileName = "cmd.exe"; 248 | psi.Arguments = "/k " + processName + " " + processArguments; 249 | p.StartInfo = psi; 250 | p.Start(); 251 | 252 | errorString = String.Empty; 253 | return String.Empty; 254 | } 255 | else 256 | { 257 | try 258 | { 259 | using (System.Diagnostics.Process p = new System.Diagnostics.Process()) 260 | { 261 | // Redirect the output stream of the child process. 262 | p.StartInfo.CreateNoWindow = !openTerminal; 263 | p.StartInfo.UseShellExecute = openTerminal; 264 | p.StartInfo.RedirectStandardOutput = !openTerminal; 265 | p.StartInfo.RedirectStandardError = !openTerminal; 266 | p.StartInfo.FileName = processName; 267 | p.StartInfo.Arguments = processArguments; 268 | 269 | System.Text.StringBuilder output = new System.Text.StringBuilder(); 270 | System.Text.StringBuilder error = new System.Text.StringBuilder(); 271 | 272 | using (System.Threading.AutoResetEvent outputWaitHandle = new System.Threading.AutoResetEvent(false)) 273 | using (System.Threading.AutoResetEvent errorWaitHandle = new System.Threading.AutoResetEvent(false)) 274 | { 275 | p.OutputDataReceived += (sender, e) => 276 | { 277 | if (e.Data == null) 278 | { 279 | outputWaitHandle.Set(); 280 | } 281 | else 282 | { 283 | output.AppendLine(e.Data); 284 | } 285 | }; 286 | p.ErrorDataReceived += (sender, e) => 287 | { 288 | if (e.Data == null) 289 | { 290 | errorWaitHandle.Set(); 291 | } 292 | else 293 | { 294 | error.AppendLine(e.Data); 295 | } 296 | }; 297 | 298 | p.Start(); 299 | 300 | if (!openTerminal) 301 | { 302 | p.BeginOutputReadLine(); 303 | p.BeginErrorReadLine(); 304 | } 305 | 306 | int timeout = (int)(requestTimeout * 1000); 307 | if (p.WaitForExit(timeout) && 308 | outputWaitHandle.WaitOne(timeout) && 309 | errorWaitHandle.WaitOne(timeout)) 310 | { 311 | errorString = error.ToString(); 312 | return output.ToString(); 313 | } 314 | else 315 | { 316 | string err = "Error: Process timed out (" + processName + " " + processArguments + ")"; 317 | errorString = err; 318 | UnityEngine.Debug.LogError(err); 319 | return err; 320 | } 321 | } 322 | } 323 | } 324 | catch (Exception e) 325 | { 326 | UnityEngine.Debug.Log(e); 327 | errorString = "e"; 328 | return null; 329 | } 330 | } 331 | } 332 | 333 | public static string ExecuteProcessTerminal(string processName, string processArguments, bool openTerminal = false) 334 | { 335 | string errorStr; 336 | string output = ExecuteProcessTerminal(processName, processArguments, out errorStr, openTerminal); 337 | return output; 338 | } 339 | 340 | public static string ExecuteProcessTerminalWithConsole(string processName, string processArguments) 341 | { 342 | string errorStr; 343 | string output = ExecuteProcessTerminal(processName, processArguments, out errorStr); 344 | if (errorStr.Length > 0) 345 | { 346 | EditorUtility.DisplayDialog("Git Lfs Locks Error", errorStr, "OK"); 347 | UnityEngine.Debug.LogError(errorStr); 348 | } 349 | 350 | return output; 351 | } 352 | 353 | public static void ExecuteNonBlockingProcessTerminal(string processName, string processArguments) 354 | { 355 | DebugLog("ExecuteNonBlockingProcessTerminal: " + processName + " with the following parameters:\n" + processArguments); 356 | 357 | try 358 | { 359 | // Start the child process. 360 | System.Diagnostics.Process p = new System.Diagnostics.Process(); 361 | 362 | // Redirect the output stream of the child process. 363 | p.StartInfo.CreateNoWindow = true; 364 | p.StartInfo.UseShellExecute = false; 365 | p.StartInfo.RedirectStandardOutput = true; 366 | p.StartInfo.RedirectStandardError = true; 367 | p.StartInfo.FileName = processName; 368 | p.StartInfo.Arguments = processArguments; 369 | p.EnableRaisingEvents = true; 370 | p.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) => 371 | { 372 | // Store the results to be treated on the main thread 373 | if (e.Data != null) 374 | { 375 | refreshCallbackResult = e.Data; 376 | } 377 | }); 378 | p.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler((sender, e) => 379 | { 380 | // Store the errors to be treated on the main thread 381 | if (e.Data != null) 382 | { 383 | refreshCallbackError = e.Data; 384 | } 385 | }); 386 | p.Start(); 387 | p.BeginOutputReadLine(); 388 | p.BeginErrorReadLine(); 389 | } 390 | catch (Exception e) 391 | { 392 | UnityEngine.Debug.Log("Error :" + e); 393 | } 394 | } 395 | 396 | public static List ParseJsonIntoLockedObjects(string jsonString) 397 | { 398 | JArray o = JArray.Parse(jsonString); 399 | List llo = Newtonsoft.Json.JsonConvert.DeserializeObject>(jsonString); 400 | return llo; 401 | } 402 | 403 | public static bool IsObjectAvailableToUnlock(UnityEngine.Object obj) 404 | { 405 | return IsObjectAvailableToUnlock(AssetDatabase.GetAssetPath(obj.GetInstanceID())); 406 | } 407 | 408 | public static bool IsObjectAvailableToUnlock(string path) 409 | { 410 | if (lockedObjectsCache == null) 411 | { 412 | return false; 413 | } 414 | 415 | if (ignoredExtensions.Any(s => path.Contains(s))) 416 | { 417 | return false; 418 | } 419 | 420 | foreach (GitLocksObject lo in lockedObjectsCache) 421 | { 422 | if (path.Replace("\\", "/") == lo.path.Replace("\\", "/")) 423 | { 424 | return lo.IsMine(); 425 | } 426 | } 427 | 428 | return false; 429 | } 430 | 431 | public static bool IsObjectAvailableToUnlock(GitLocksObject lo) 432 | { 433 | if (lockedObjectsCache == null) 434 | { 435 | return false; 436 | } 437 | 438 | return lo.IsMine(); 439 | } 440 | 441 | public static bool IsObjectAvailableToLock(UnityEngine.Object obj) 442 | { 443 | return IsObjectAvailableToLock(AssetDatabase.GetAssetPath(obj.GetInstanceID())); 444 | } 445 | 446 | public static bool IsObjectAvailableToLock(string path) 447 | { 448 | if (lockedObjectsCache == null) 449 | { 450 | return false; 451 | } 452 | 453 | if (ignoredExtensions.Any(s => path.Contains(s))) 454 | { 455 | return false; 456 | } 457 | 458 | foreach (GitLocksObject lo in lockedObjectsCache) 459 | { 460 | if (path.Replace("\\","/") == lo.path.Replace("\\", "/")) 461 | { 462 | return false; 463 | } 464 | } 465 | 466 | return true; 467 | } 468 | 469 | public static GitLocksObject GetObjectInLockedCache(UnityEngine.Object obj) 470 | { 471 | if (lockedObjectsCache == null) 472 | { 473 | return null; 474 | } 475 | 476 | foreach (GitLocksObject lo in lockedObjectsCache) 477 | { 478 | if (obj == lo.GetObjectReference()) 479 | { 480 | return lo; 481 | } 482 | } 483 | 484 | return null; 485 | } 486 | 487 | public static GitLocksObject GetObjectInLockedCache(string path) 488 | { 489 | if (lockedObjectsCache == null) 490 | { 491 | return null; 492 | } 493 | 494 | foreach (GitLocksObject lo in lockedObjectsCache) 495 | { 496 | if (path == lo.path) 497 | { 498 | return lo; 499 | } 500 | } 501 | 502 | return null; 503 | } 504 | 505 | public static void LockFile(string path) 506 | { 507 | List list = new List(); 508 | list.Add(path); 509 | LockFiles(list); 510 | } 511 | 512 | public static void LockFiles(List paths) 513 | { 514 | // Logs 515 | if (paths.Count > 1) 516 | { 517 | DebugLog("Trying to lock " + paths.Count + " files"); 518 | } 519 | else 520 | { 521 | DebugLog("Trying to lock " + paths[0]); 522 | } 523 | 524 | // Split into multiple requests if there are too many files (prevents triggering timeout) 525 | int numOfRequests = (int)Math.Ceiling((float)paths.Count / (float)EditorPrefs.GetInt("gitLocksMaxFilesNumPerRequest", 15)); 526 | List pathsStrings = new List(new string[numOfRequests]); 527 | for (int i = 0; i < paths.Count; i++) 528 | { 529 | string p = paths[i]; 530 | // Optionally, check if the file we want to lock has been modified on the server 531 | if (EditorPrefs.GetBool("warnIfFileHasBeenModifiedOnServer") && HasFileBeenModifiedOnServer(p)) 532 | { 533 | if (EditorUtility.DisplayDialog("File modified on the server", "Warning! The file you want to lock has been modified on the server already, you REALLY should pull before locking or you'll almost certainly get merge conflicts.", "OK, don't lock yet", "I know what I'm doing, lock anyway")) 534 | { 535 | return; 536 | } 537 | } 538 | 539 | int stringIndex = (int)Math.Floor((float)i / (float)EditorPrefs.GetInt("gitLocksMaxFilesNumPerRequest", 15) + Mathf.Epsilon); 540 | pathsStrings[stringIndex] += "\"" + p + "\" "; 541 | } 542 | 543 | // Send each request 544 | foreach (string pathsString in pathsStrings) 545 | { 546 | ExecuteProcessTerminalWithConsole("git", "lfs lock " + pathsString); 547 | } 548 | } 549 | 550 | public static void UnlockFile(string path, bool force = false) 551 | { 552 | List list = new List(); 553 | list.Add(path); 554 | UnlockFiles(list, force); 555 | } 556 | 557 | public static void UnlockFiles(List paths, bool force = false) 558 | { 559 | if (paths.Count > 1) 560 | { 561 | DebugLog("Trying to unlock " + paths.Count + " files"); 562 | } 563 | else 564 | { 565 | DebugLog("Trying to unlock " + paths[0]); 566 | } 567 | 568 | // Split into multiple requests if there are too many files (prevents triggering timeout) 569 | int numOfRequests = (int)Math.Ceiling((float)paths.Count / (float)EditorPrefs.GetInt("gitLocksMaxFilesNumPerRequest", 15)); 570 | List pathsStrings = new List(new string[numOfRequests]); 571 | for (int i = 0; i < paths.Count; i++) 572 | { 573 | string p = paths[i]; 574 | 575 | int stringIndex = (int)Math.Floor((float)i / (float)EditorPrefs.GetInt("gitLocksMaxFilesNumPerRequest", 15) + Mathf.Epsilon); 576 | pathsStrings[stringIndex] += "\"" + p + "\" "; 577 | } 578 | 579 | // Send each request 580 | foreach (string pathsString in pathsStrings) 581 | { 582 | ExecuteProcessTerminalWithConsole("git", "lfs unlock " + pathsString + (force ? "--force" : string.Empty)); 583 | } 584 | } 585 | 586 | public static void UnlockAllMyLocks() 587 | { 588 | List paths = new List(); 589 | foreach (GitLocksObject lo in lockedObjectsCache) 590 | { 591 | if (lo.IsMine()) 592 | { 593 | paths.Add(lo.path); 594 | } 595 | } 596 | 597 | UnlockFiles(paths); 598 | RefreshLocks(); 599 | } 600 | 601 | public static void UnlockMultipleLocks(List toUnlock) 602 | { 603 | List paths = new List(); 604 | foreach (GitLocksObject lo in toUnlock) 605 | { 606 | // Sanity check 607 | if (lo.IsMine()) 608 | { 609 | paths.Add(lo.path); 610 | } 611 | } 612 | 613 | UnlockFiles(paths); 614 | RefreshLocks(); 615 | } 616 | 617 | public static List GetMyLocks() 618 | { 619 | if (lockedObjectsCache == null || lockedObjectsCache.Count == 0) 620 | { 621 | return null; 622 | } 623 | 624 | List myLocks = new List(); 625 | foreach (GitLocksObject lo in lockedObjectsCache) 626 | { 627 | if (lo.IsMine()) 628 | { 629 | myLocks.Add(lo); 630 | } 631 | } 632 | 633 | return myLocks; 634 | } 635 | 636 | public static List GetOtherLocks() 637 | { 638 | if (lockedObjectsCache == null || lockedObjectsCache.Count == 0) 639 | { 640 | return null; 641 | } 642 | 643 | List myLocks = new List(); 644 | foreach (GitLocksObject lo in lockedObjectsCache) 645 | { 646 | if (!lo.IsMine()) 647 | { 648 | myLocks.Add(lo); 649 | } 650 | } 651 | 652 | return myLocks; 653 | } 654 | 655 | public static string GetGitUsername() 656 | { 657 | return EditorPrefs.GetString("gitLocksHostUsername", string.Empty); 658 | } 659 | 660 | public static string GetGitVersion() 661 | { 662 | if (gitVersion == null || gitVersion == string.Empty) 663 | { 664 | gitVersion = ExecuteProcessTerminalWithConsole("git", "--version"); 665 | } 666 | 667 | return gitVersion; 668 | } 669 | 670 | public static bool IsGitOutdated() 671 | { 672 | string[] split = GetGitVersion().Split('.'); 673 | 674 | int verNum1, verNum2; 675 | bool parse1 = int.TryParse(split[0].Substring(split[0].Length - 1), out verNum1); 676 | bool parse2 = int.TryParse(split[1], out verNum2); 677 | if (parse1 && parse2) 678 | { 679 | // Git for windows version should be greater than 2.30.0 to have the latest authentication manager 680 | return verNum1 < 2 || verNum2 < 30; 681 | } 682 | else 683 | { 684 | UnityEngine.Debug.LogWarning("GitLocks couldn't parse the Git for Windows version properly"); 685 | return true; 686 | } 687 | } 688 | 689 | public static bool GetDisplayLocksConflictWarning() 690 | { 691 | return EditorPrefs.GetBool("displayLocksConflictWarning"); 692 | } 693 | 694 | public static string GetAssetPathFromPrefabGameObject(int instanceID) 695 | { 696 | var gameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject; 697 | return GetAssetPathFromPrefabGameObject(gameObject); 698 | } 699 | 700 | public static string GetAssetPathFromPrefabGameObject(GameObject gameObject) 701 | { 702 | if (gameObject != null) 703 | { 704 | return PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject); 705 | } 706 | 707 | return string.Empty; 708 | } 709 | 710 | public static bool IsObjectPrefabRoot(int instanceID) 711 | { 712 | var gameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject; 713 | return IsObjectPrefabRoot(gameObject); 714 | } 715 | 716 | public static bool IsObjectPrefabRoot(GameObject gameObject) 717 | { 718 | if (gameObject != null) 719 | { 720 | return PrefabUtility.GetNearestPrefabInstanceRoot(gameObject) == gameObject; 721 | } 722 | 723 | return false; 724 | } 725 | 726 | public static Scene GetSceneFromInstanceID(int instanceID) 727 | { 728 | for (int index = 0; index < SceneManager.sceneCount; index++) 729 | { 730 | var scene = SceneManager.GetSceneAt(index); 731 | if (scene.handle == instanceID) 732 | { 733 | return scene; 734 | } 735 | } 736 | 737 | return default; 738 | } 739 | 740 | public static bool IsLockedObjectConflictingWithUncommitedFile(GitLocksObject lo) 741 | { 742 | if (uncommitedFilesCache == null || uncommitedFilesCache.Count == 0) 743 | { 744 | return false; 745 | } 746 | 747 | if (GetGitUsername() == null || GetGitUsername() == string.Empty) 748 | { 749 | return false; 750 | } 751 | 752 | foreach (string uncommitedPath in uncommitedFilesCache) 753 | { 754 | if (lo.path == uncommitedPath && !lo.IsMine()) 755 | { 756 | return true; 757 | } 758 | } 759 | 760 | return false; 761 | } 762 | 763 | public static void SetUncommitedCacheDirty() 764 | { 765 | uncommitedFilesCacheDirty = true; 766 | } 767 | 768 | public static void BuildUncommitedCache() 769 | { 770 | if (uncommitedFilesCache != null) 771 | { 772 | uncommitedFilesCache.Clear(); 773 | } 774 | else 775 | { 776 | uncommitedFilesCache = new List(); 777 | } 778 | 779 | char[] splitter = { '\n' }; 780 | 781 | // Staged 782 | string output = ExecuteProcessTerminal("git", "diff --name-only --staged"); 783 | string[] lines = output.Split(splitter, StringSplitOptions.RemoveEmptyEntries); 784 | List filesCandidates = new List(lines); 785 | 786 | // Not staged 787 | output = ExecuteProcessTerminal("git", "diff --name-only"); 788 | lines = output.Split(splitter, StringSplitOptions.RemoveEmptyEntries); 789 | filesCandidates.AddRange(lines); 790 | 791 | // Check all lines to see if they correspond to files and add them to the cache if so 792 | foreach (string file in filesCandidates) 793 | { 794 | // Clean and format strings 795 | string tmpFile = file; 796 | tmpFile = tmpFile.Trim(); 797 | tmpFile = tmpFile.Replace("\\r", string.Empty); 798 | tmpFile = tmpFile.Replace("\\n", string.Empty); 799 | string fullPath = Application.dataPath.Replace("Assets", string.Empty) + tmpFile; 800 | fullPath = fullPath.Replace("/", "\\"); 801 | 802 | if (System.IO.File.Exists(fullPath)) 803 | { 804 | uncommitedFilesCache.Add(tmpFile); 805 | } 806 | } 807 | 808 | uncommitedFilesCacheDirty = false; 809 | 810 | GitLocksDisplay.RepaintAll(); 811 | } 812 | 813 | public static void BuildModifiedOnServerCache() 814 | { 815 | if (modifiedOnServerFilesCache != null) 816 | { 817 | modifiedOnServerFilesCache.Clear(); 818 | } 819 | else 820 | { 821 | modifiedOnServerFilesCache = new List(); 822 | } 823 | 824 | char[] splitter = { '\n' }; 825 | 826 | // Construct list of branches to check 827 | HashSet branchesToCheck = new HashSet(); 828 | 829 | // Add optional branches to check set in preferences 830 | if (EditorPrefs.HasKey("gitLocksBranchesToCheck")) 831 | { 832 | string fullString = EditorPrefs.GetString("gitLocksBranchesToCheck"); 833 | string[] array = fullString.Split(','); 834 | foreach (string branch in array) 835 | { 836 | if (branch != string.Empty) 837 | { 838 | branchesToCheck.Add(branch); 839 | } 840 | } 841 | } 842 | 843 | // Add current branch name 844 | string currentBranch = GetCurrentBranch(); 845 | branchesToCheck.Add(currentBranch); 846 | 847 | foreach (string branch in branchesToCheck) 848 | { 849 | // Fetch 850 | ExecuteProcessTerminal("git", "git fetch origin " + branch); 851 | 852 | // List all distant commits 853 | string output = ExecuteProcessTerminal("git", "rev-list " + currentBranch + "..origin/" + branch); 854 | string[] lines = output.Split(splitter, StringSplitOptions.RemoveEmptyEntries); 855 | List commits = new List(lines); 856 | 857 | // Check all commits 858 | foreach (string commit in commits) 859 | { 860 | // Add all files in commit to the list 861 | string filesOutput = ExecuteProcessTerminal("git", "diff-tree --no-commit-id --name-only -r " + commit.Replace("\r", "")); 862 | string[] files = filesOutput.Split(splitter, StringSplitOptions.RemoveEmptyEntries); 863 | 864 | foreach (string file in files) 865 | { 866 | // Check that every file exists (sanity) 867 | if (System.IO.File.Exists(file.Replace("\r", ""))) 868 | { 869 | modifiedOnServerFilesCache.Add(file.Replace("\r", "")); 870 | } 871 | } 872 | } 873 | } 874 | } 875 | 876 | public static bool HasFileBeenModifiedOnServer(string filePath) 877 | { 878 | BuildModifiedOnServerCache(); 879 | return modifiedOnServerFilesCache.Contains(filePath); 880 | } 881 | 882 | public static void AddFileToConflictWarningIgnoreList(string path) 883 | { 884 | conflictWarningIgnoreList.Add(path); 885 | } 886 | 887 | public static bool IsFileInConflictIgnoreList(string path) 888 | { 889 | return conflictWarningIgnoreList.Contains(path); 890 | } 891 | 892 | public static bool IsEnabled() 893 | { 894 | return EditorPrefs.GetBool("gitLocksEnabled", false); 895 | } 896 | 897 | public static string GetCurrentBranch() 898 | { 899 | char[] splitter = { '\n' }; 900 | string currentBranch = ExecuteProcessTerminal("git", "rev-parse --abbrev-ref HEAD"); 901 | currentBranch = currentBranch.Split(splitter)[0].Replace("\r", ""); 902 | return currentBranch; 903 | } 904 | 905 | private static void Update() 906 | { 907 | if (!IsEnabled() || EditorApplication.isUpdating || EditorApplication.isCompiling) 908 | { 909 | return; // Early return if the whole tool is disabled of if the Editor is not available 910 | } 911 | 912 | if (uncommitedFilesCacheDirty) 913 | { 914 | BuildUncommitedCache(); 915 | } 916 | 917 | if (refreshCallbackResult != null) 918 | { 919 | if (refreshCallbackResult != string.Empty) 920 | { 921 | RefreshCallback(refreshCallbackResult); 922 | } 923 | 924 | currentlyRefreshing = false; 925 | refreshCallbackResult = null; 926 | } 927 | 928 | if (refreshCallbackError != null && refreshCallbackError != string.Empty) 929 | { 930 | if (!EditorUtility.DisplayDialog("Git Lfs Locks Error", "Git lfs locks error :\n\n" + refreshCallbackError + "\n\nIf it's your first time using the tool, you should probably setup the credentials manager", "OK", "Setup credentials")) 931 | { 932 | DebugLog("Setup credentials manager"); 933 | ExecuteProcessTerminalWithConsole("git", "config --global credential.helper manager"); 934 | } 935 | 936 | refreshCallbackError = null; 937 | } 938 | } 939 | 940 | private static bool WantsToQuit() 941 | { 942 | if (!IsEnabled()) 943 | { 944 | return true; // Early return if the whole tool is disabled 945 | } 946 | 947 | if (EditorPrefs.GetBool("warnIfIStillOwnLocksOnQuit") && GetMyLocks() != null && GetMyLocks().Count > 0) 948 | { 949 | if (EditorUtility.DisplayDialog("Remaining locks", "You still own locks on some files, do you want to quit anyway ?", "Yes", "No, take me back")) 950 | { 951 | return true; 952 | } 953 | else 954 | { 955 | GitLocksDisplay locksWindow = (GitLocksDisplay)EditorWindow.GetWindow(typeof(GitLocksDisplay), false, "Git Locks"); 956 | return false; 957 | } 958 | } 959 | else 960 | { 961 | return true; 962 | } 963 | } 964 | 965 | private static void DebugLog(string s) 966 | { 967 | if (EditorPrefs.GetBool("gitLocksDebugMode", false)) 968 | { 969 | UnityEngine.Debug.Log(s); 970 | } 971 | } 972 | } -------------------------------------------------------------------------------- /Editor/Scripts/GitLocks.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d4155b722e550bc4792e1e147bea5ae0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksAssetPostprocessor.cs: -------------------------------------------------------------------------------- 1 | // All rights reserved. 2 | 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using UnityEngine; 6 | 7 | public class GitLocksAssetPostprocessor : UnityEditor.AssetPostprocessor 8 | { 9 | private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) 10 | { 11 | foreach (string str in importedAssets) 12 | { 13 | if (GitLocks.GetObjectInLockedCache(str) != null) 14 | { 15 | GitLocks.SetUncommitedCacheDirty(); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksAssetPostprocessor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2eca742852b5d7945a46fd16eb986a5a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksDisplay.cs: -------------------------------------------------------------------------------- 1 | // All rights reserved. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | [InitializeOnLoad] 10 | public class GitLocksDisplay : EditorWindow 11 | { 12 | private static Texture greenLockIcon; 13 | private static Texture orangeLockIcon; 14 | private static Texture redLockIcon; 15 | private static Texture mixedLockIcon; 16 | 17 | // Interface sizes 18 | private static float unlockButtonWidth = 65; 19 | private static float forceUnlockButtonWidth = 95; 20 | private static float lockIconWidth = 18; 21 | private static float scrollbarWidth = 20; // In case the scoll view triggers 22 | private static float checkboxWidth = 30; 23 | 24 | private UnityEngine.Object objectToLock; 25 | private Vector2 scrollPosMine = Vector2.zero; 26 | private Vector2 scrollPosOthers = Vector2.zero; 27 | 28 | private static List selectedLocks; 29 | 30 | // Show git history 31 | private static int showHistoryMaxNumOfFilesBeforeWarning = 5; 32 | 33 | static GitLocksDisplay() 34 | { 35 | // Add our own GUI to the project and hierarchy windows 36 | EditorApplication.projectWindowItemOnGUI += DrawProjectLocks; 37 | EditorApplication.hierarchyWindowItemOnGUI += DrawHierarchyLocks; 38 | } 39 | 40 | [MenuItem("Window/Git Locks")] 41 | public static void ShowWindow() 42 | { 43 | // Show existing window instance. If one doesn't exist, make one. 44 | EditorWindow.GetWindow(typeof(GitLocksDisplay), false, "Git Locks"); 45 | } 46 | 47 | public static void RepaintAll() 48 | { 49 | EditorApplication.RepaintHierarchyWindow(); 50 | EditorApplication.RepaintProjectWindow(); 51 | if (EditorWindow.HasOpenInstances()) 52 | { 53 | EditorWindow locksWindow = GetWindow(typeof(GitLocksDisplay), false, "Git Locks", false); 54 | locksWindow.Repaint(); 55 | } 56 | } 57 | 58 | public static void DrawUILine(Color color, int thickness = 2, int padding = 10) 59 | { 60 | Rect r = EditorGUILayout.GetControlRect(GUILayout.Height(padding + thickness)); 61 | r.height = thickness; 62 | r.y += padding / 2; 63 | r.x -= 2; 64 | r.width += 6; 65 | EditorGUI.DrawRect(r, color); 66 | } 67 | 68 | public static Texture GetIconForLockedObject(GitLocksObject lo) 69 | { 70 | if (lo == null) 71 | { 72 | return null; 73 | } 74 | 75 | bool isLockConflictingWithUncommitedFile = GitLocks.IsLockedObjectConflictingWithUncommitedFile(lo); 76 | 77 | if (lo.IsMine()) 78 | { 79 | return GetGreenLockIcon(); 80 | } 81 | else if (isLockConflictingWithUncommitedFile) 82 | { 83 | return GetRedLockIcon(); 84 | } 85 | else 86 | { 87 | return GetOrangeLockIcon(); 88 | } 89 | } 90 | 91 | public static void DisplayLockIcon(string path, Rect selectionRect, float offset, bool small = false) 92 | { 93 | var frame = new Rect(selectionRect); 94 | 95 | // Handle files 96 | GitLocksObject lo = GitLocks.GetObjectInLockedCache(path); 97 | if (lo != null) 98 | { 99 | frame.x += offset + (small ? 3f : 0f); 100 | frame.width = small ? 12f : 18f; 101 | 102 | Texture lockTexture = GetIconForLockedObject(lo); 103 | string tooltip; 104 | 105 | // Fill tooltip 106 | if (lo.IsMine()) 107 | { 108 | tooltip = "Locked by me"; 109 | } 110 | else if (GitLocks.IsLockedObjectConflictingWithUncommitedFile(lo)) 111 | { 112 | tooltip = "Conflicting with lock by " + lo.owner.name; 113 | } 114 | else 115 | { 116 | tooltip = "Locked by " + lo.owner.name; 117 | } 118 | 119 | if (GUI.Button(frame, new GUIContent(lockTexture, tooltip), GUI.skin.label)) 120 | { 121 | if (lo.IsMine()) 122 | { 123 | if (!EditorUtility.DisplayDialog("Asset locked by you", "You have locked this asset, you're safe working on it.", "OK", "Unlock")) 124 | { 125 | GitLocks.UnlockFile(lo.path); 126 | GitLocks.RefreshLocks(); 127 | } 128 | } 129 | else if (GitLocks.IsLockedObjectConflictingWithUncommitedFile(lo)) 130 | { 131 | EditorUtility.DisplayDialog("Asset locked by someone else and conflicting", "User " + lo.owner.name + " has locked this asset (" + lo.GetLockDateTimeString() + ") and you have uncommited modifications: you should probably discard them as you won't be able to push them.", "OK"); 132 | } 133 | else 134 | { 135 | EditorUtility.DisplayDialog("Asset locked by someone else", "User " + lo.owner.name + " has locked this asset (" + lo.GetLockDateTimeString() + "), you cannot work on it.", "OK"); 136 | } 137 | } 138 | } 139 | 140 | // Handle folders 141 | if (Directory.Exists(path) && GitLocks.LockedObjectsCache != null) 142 | { 143 | bool containsOneOfMyLocks = false; 144 | bool containsOneOfOtherLocks = false; 145 | bool containsOneConflictingLock = false; 146 | foreach (GitLocksObject dlo in GitLocks.LockedObjectsCache) 147 | { 148 | string folderPath = path + "/"; 149 | if (dlo.path.Contains(folderPath)) 150 | { 151 | if (dlo.IsMine()) 152 | { 153 | containsOneOfMyLocks = true; 154 | } 155 | else if (GitLocks.IsLockedObjectConflictingWithUncommitedFile(dlo)) 156 | { 157 | containsOneConflictingLock = true; 158 | containsOneOfOtherLocks = true; 159 | } 160 | else 161 | { 162 | containsOneOfOtherLocks = true; 163 | } 164 | if (containsOneOfMyLocks && containsOneOfOtherLocks) 165 | { 166 | break; 167 | } 168 | } 169 | } 170 | 171 | if (containsOneOfMyLocks || containsOneOfOtherLocks) 172 | { 173 | frame.x += offset + 15; 174 | frame.width = 15f; 175 | string tooltip; 176 | Texture lockTexture; 177 | 178 | if (containsOneOfMyLocks && containsOneOfOtherLocks) 179 | { 180 | lockTexture = GetMixedLockIcon(); 181 | tooltip = "Folder contains files locked by me and others"; 182 | } 183 | else if (containsOneOfMyLocks) 184 | { 185 | lockTexture = GetGreenLockIcon(); 186 | tooltip = "Folder contains files locked by me"; 187 | } 188 | else if (containsOneConflictingLock) 189 | { 190 | lockTexture = GetRedLockIcon(); 191 | tooltip = "Folder contains conflicting files"; 192 | } 193 | else 194 | { 195 | lockTexture = GetOrangeLockIcon(); 196 | tooltip = "Folder contains files locked by others"; 197 | } 198 | 199 | GUI.Button(frame, new GUIContent(lockTexture, tooltip), GUI.skin.label); 200 | } 201 | } 202 | } 203 | 204 | // ----------------------- 205 | // Project window features 206 | // ----------------------- 207 | [MenuItem("Assets/Git LFS Lock %#l", false, 1100)] 208 | private static void ItemMenuLock() 209 | { 210 | UnityEngine.Object[] selected = Selection.GetFiltered(SelectionMode.DeepAssets); 211 | List paths = new List(); 212 | foreach (UnityEngine.Object o in selected) 213 | { 214 | string path = AssetDatabase.GetAssetPath(o.GetInstanceID()); 215 | if (!string.IsNullOrEmpty(path) && Directory.Exists(path)) 216 | { 217 | continue; // Folders are not lockable, skip this asset 218 | } 219 | 220 | paths.Add(path); 221 | } 222 | 223 | GitLocks.LockFiles(paths); 224 | GitLocks.RefreshLocks(); 225 | } 226 | 227 | [MenuItem("Assets/Git LFS Lock %#l", true)] 228 | private static bool ValidateItemMenuLock() 229 | { 230 | if (!GitLocks.IsEnabled()) 231 | { 232 | return false; // Early return if the whole tool is disabled 233 | } 234 | 235 | // Don't allow if the locked cache hasn't been built 236 | if (GitLocks.LastRefresh <= DateTime.MinValue) 237 | { 238 | return false; 239 | } 240 | 241 | UnityEngine.Object[] selected = Selection.GetFiltered(SelectionMode.Assets); 242 | foreach (UnityEngine.Object o in selected) 243 | { 244 | string path = AssetDatabase.GetAssetPath(o.GetInstanceID()); 245 | if (Directory.Exists(path)) 246 | { 247 | foreach (GitLocksObject lo in GitLocks.LockedObjectsCache) 248 | { 249 | string folderPath = path + "/"; 250 | if (lo.path.Contains(folderPath)) 251 | { 252 | return false; 253 | } 254 | } 255 | } 256 | else if (!GitLocks.IsObjectAvailableToLock(path)) 257 | { 258 | return false; 259 | } 260 | } 261 | 262 | return true; 263 | } 264 | 265 | [MenuItem("Assets/Git LFS Unlock %#u", false, 1101)] 266 | private static void ItemMenuUnlock() 267 | { 268 | List paths = new List(); 269 | UnityEngine.Object[] selected = Selection.GetFiltered(SelectionMode.Assets); 270 | foreach (UnityEngine.Object o in selected) 271 | { 272 | string path = AssetDatabase.GetAssetPath(o.GetInstanceID()); 273 | if (Directory.Exists(path)) 274 | { 275 | foreach (GitLocksObject lo in GitLocks.LockedObjectsCache) 276 | { 277 | string folderPath = path + "/"; 278 | if (lo.path.Contains(folderPath)) 279 | { 280 | if (GitLocks.IsObjectAvailableToUnlock(lo)) 281 | { 282 | paths.Add(lo.path); 283 | } 284 | } 285 | } 286 | } 287 | else if (GitLocks.IsObjectAvailableToUnlock(path)) 288 | { 289 | paths.Add(path); 290 | } 291 | } 292 | 293 | GitLocks.UnlockFiles(paths); 294 | GitLocks.RefreshLocks(); 295 | } 296 | 297 | [MenuItem("Assets/Git LFS Unlock %#u", true)] 298 | private static bool ValidateItemMenuUnlock() 299 | { 300 | if (!GitLocks.IsEnabled()) 301 | { 302 | return false; // Early return if the whole tool is disabled 303 | } 304 | 305 | // Don't allow if the locked cache hasn't been built 306 | if (GitLocks.LastRefresh <= DateTime.MinValue) 307 | { 308 | return false; 309 | } 310 | 311 | UnityEngine.Object[] selected = Selection.GetFiltered(SelectionMode.Assets); 312 | bool foundObjectToUnlock = false; 313 | foreach (UnityEngine.Object o in selected) 314 | { 315 | string path = AssetDatabase.GetAssetPath(o.GetInstanceID()); 316 | if (Directory.Exists(path)) 317 | { 318 | foreach (GitLocksObject lo in GitLocks.LockedObjectsCache) 319 | { 320 | string folderPath = path + "/"; 321 | if (lo.path.Contains(folderPath)) 322 | { 323 | if (lo.IsMine()) 324 | { 325 | foundObjectToUnlock = true; 326 | } 327 | else 328 | { 329 | return false; 330 | } 331 | } 332 | } 333 | } 334 | else if (GitLocks.IsObjectAvailableToUnlock(path)) 335 | { 336 | foundObjectToUnlock = true; 337 | } 338 | } 339 | 340 | return foundObjectToUnlock; 341 | } 342 | 343 | // ------------------------- 344 | // Hierarchy window features 345 | // ------------------------- 346 | [MenuItem("GameObject/Git LFS Lock", false, 40)] 347 | private static void ItemMenuLockHierarchy() 348 | { 349 | List paths = new List(); 350 | foreach (UnityEngine.Object o in Selection.objects) 351 | { 352 | string path = GitLocks.GetAssetPathFromPrefabGameObject(o.GetInstanceID()); 353 | paths.Add(path); 354 | } 355 | 356 | GitLocks.LockFiles(paths); 357 | 358 | // Clear the selection to make sure it's called only once 359 | Selection.objects = null; 360 | 361 | GitLocks.RefreshLocks(); 362 | } 363 | 364 | [MenuItem("GameObject/Git LFS Lock", true)] 365 | private static bool ValidateItemMenuLockHierarchy() 366 | { 367 | if (!GitLocks.IsEnabled()) 368 | { 369 | return false; // Early return if the whole tool is disabled 370 | } 371 | 372 | // Don't allow if the locked cache hasn't been built 373 | if (GitLocks.LastRefresh <= DateTime.MinValue) 374 | { 375 | return false; 376 | } 377 | 378 | foreach (UnityEngine.Object o in Selection.objects) 379 | { 380 | if (o == null) 381 | { 382 | return false; 383 | } 384 | 385 | string path = GitLocks.GetAssetPathFromPrefabGameObject(o.GetInstanceID()); 386 | if (path == null || path == string.Empty || !GitLocks.IsObjectAvailableToLock(path)) 387 | { 388 | return false; 389 | } 390 | } 391 | 392 | return true; 393 | } 394 | 395 | [MenuItem("GameObject/Git LFS Unlock", false, 41)] 396 | private static void ItemMenuUnlockHierarchy() 397 | { 398 | List paths = new List(); 399 | foreach (UnityEngine.Object o in Selection.objects) 400 | { 401 | string path = GitLocks.GetAssetPathFromPrefabGameObject(o.GetInstanceID()); 402 | paths.Add(path); 403 | } 404 | 405 | GitLocks.UnlockFiles(paths); 406 | 407 | // Clear the selection to make sure it's called only once 408 | Selection.objects = null; 409 | 410 | GitLocks.RefreshLocks(); 411 | } 412 | 413 | [MenuItem("GameObject/Git LFS Unlock", true)] 414 | private static bool ValidateItemMenuUnlockHierarchy() 415 | { 416 | if (!GitLocks.IsEnabled()) 417 | { 418 | return false; // Early return if the whole tool is disabled 419 | } 420 | 421 | // Don't allow if the locked cache hasn't been built 422 | if (GitLocks.LastRefresh <= DateTime.MinValue) 423 | { 424 | return false; 425 | } 426 | 427 | foreach (UnityEngine.Object o in Selection.objects) 428 | { 429 | if (o == null) 430 | { 431 | return false; 432 | } 433 | 434 | string path = GitLocks.GetAssetPathFromPrefabGameObject(o.GetInstanceID()); 435 | if (path == null || path == string.Empty || !GitLocks.IsObjectAvailableToUnlock(path)) 436 | { 437 | return false; 438 | } 439 | } 440 | 441 | return true; 442 | } 443 | 444 | // ------------------------------------------- 445 | // Draw icons in hierarchy and project windows 446 | // ------------------------------------------- 447 | private static void DrawProjectLocks(string guid, Rect selectionRect) 448 | { 449 | if (!GitLocks.IsEnabled()) 450 | { 451 | return; // Early return if the whole tool is disabled 452 | } 453 | 454 | GitLocks.CheckLocksRefresh(); 455 | 456 | string path = AssetDatabase.GUIDToAssetPath(guid); 457 | 458 | DisplayLockIcon(path, selectionRect, -12f); 459 | } 460 | 461 | private static void DrawHierarchyLocks(int instanceID, Rect selectionRect) 462 | { 463 | if (!GitLocks.IsEnabled()) 464 | { 465 | return; // Early return if the whole tool is disabled 466 | } 467 | 468 | GitLocks.CheckLocksRefresh(); 469 | 470 | string path = string.Empty; 471 | bool small = false; 472 | 473 | // Handle scenes 474 | path = GitLocks.GetSceneFromInstanceID(instanceID).path; 475 | 476 | // Handle prefabs 477 | string tmpPath = GitLocks.GetAssetPathFromPrefabGameObject(instanceID); 478 | if (tmpPath != string.Empty) 479 | { 480 | path = tmpPath; 481 | small = !GitLocks.IsObjectPrefabRoot(instanceID); 482 | } 483 | 484 | // Display 485 | if (path != string.Empty) 486 | { 487 | DisplayLockIcon(path, selectionRect, -30f, small); 488 | } 489 | } 490 | 491 | // ------------ 492 | // File history 493 | // ------------ 494 | [MenuItem("Assets/Show Git History", false, 1101)] 495 | private static void ItemMenuGitHistory() 496 | { 497 | UnityEngine.Object[] selected = Selection.GetFiltered(SelectionMode.Assets); 498 | 499 | // Display a warning if you're about to open many CLIs or browser tabs to prevent slowing down your computer if you misclick 500 | if (selected.Length <= showHistoryMaxNumOfFilesBeforeWarning || EditorUtility.DisplayDialog("Are you sure?", "More than " + showHistoryMaxNumOfFilesBeforeWarning + " files have been selected, are you sure you want to open the history for all of them?", "Yes", "Cancel")) 501 | { 502 | foreach (UnityEngine.Object o in selected) 503 | { 504 | string path = AssetDatabase.GetAssetPath(o.GetInstanceID()); 505 | if (Directory.Exists(path)) 506 | { 507 | continue; // Folders are not lockable, skip this asset 508 | } 509 | 510 | if (EditorPrefs.GetBool("gitLocksShowHistoryInBrowser", false)) 511 | { 512 | string url = EditorPrefs.GetString("gitLocksShowHistoryInBrowserUrl"); 513 | if (url != string.Empty && url.Contains("$branch") && url.Contains("$assetPath")) 514 | { 515 | url = url.Replace("$branch", GitLocks.GetCurrentBranch()); 516 | url = url.Replace("$assetPath", path); 517 | UnityEngine.Application.OpenURL(url); 518 | } 519 | else 520 | { 521 | UnityEngine.Debug.LogError("URL was not formatted correctly to show the file's history in your browser: it must be formatted like https://github.com/MyUserName/MyRepo/blob/$branch/$assetPath"); 522 | } 523 | } 524 | else 525 | { 526 | GitLocks.ExecuteProcessTerminal("git", "log \"" + path + "\"", true); 527 | } 528 | } 529 | } 530 | } 531 | 532 | [MenuItem("Assets/Show Git History", true)] 533 | private static bool ValidateItemMenuGitHistory() 534 | { 535 | UnityEngine.Object[] selected = Selection.GetFiltered(SelectionMode.Assets); 536 | foreach (UnityEngine.Object o in selected) 537 | { 538 | string path = AssetDatabase.GetAssetPath(o.GetInstanceID()); 539 | if (Directory.Exists(path)) 540 | { 541 | return false; 542 | } 543 | } 544 | 545 | return true; 546 | } 547 | 548 | // ----------- 549 | // Toolbox 550 | // ----------- 551 | private static void DrawLockedObjectLine(GitLocksObject lo, bool myLock = false) 552 | { 553 | UnityEngine.Object lockedObj = lo.GetObjectReference(); 554 | if (lockedObj != null) 555 | { 556 | float totalOtherWidth = EditorGUIUtility.currentViewWidth - lockIconWidth - scrollbarWidth - checkboxWidth; 557 | totalOtherWidth -= EditorPrefs.GetBool("gitLocksShowForceButtons") ? forceUnlockButtonWidth : 0; 558 | totalOtherWidth -= myLock ? unlockButtonWidth : 0; 559 | float othersWidth = totalOtherWidth / 3; 560 | 561 | GUILayout.BeginHorizontal(); 562 | 563 | Texture lockTexture = GetIconForLockedObject(lo); 564 | 565 | // Checkboxes for custom selection 566 | if (myLock) 567 | { 568 | if (selectedLocks == null) 569 | { 570 | selectedLocks = new List(); 571 | } 572 | bool checkbox = GUILayout.Toggle(selectedLocks.Contains(lo), ""); 573 | if (checkbox && !selectedLocks.Contains(lo)) 574 | { 575 | selectedLocks.Add(lo); 576 | } 577 | else if (!checkbox && selectedLocks.Contains(lo)) 578 | { 579 | selectedLocks.Remove(lo); 580 | } 581 | } 582 | 583 | GUILayout.Button(lockTexture, GUI.skin.label, GUILayout.Height(18), GUILayout.Width(18)); 584 | 585 | EditorGUI.BeginDisabledGroup(true); 586 | EditorGUILayout.ObjectField(lockedObj, lockedObj.GetType(), false, GUILayout.Width(othersWidth)); 587 | EditorGUI.EndDisabledGroup(); 588 | 589 | EditorGUILayout.LabelField(new GUIContent(lo.path, lo.path), GUILayout.Width(othersWidth)); 590 | 591 | GUILayout.Label(new GUIContent(lo.owner.name, lo.GetLockDateTimeString()), GUILayout.Width(othersWidth)); 592 | 593 | if (myLock) 594 | { 595 | if (GUILayout.Button("Unlock")) 596 | { 597 | GitLocks.UnlockFile(lo.path); 598 | GitLocks.RefreshLocks(); 599 | } 600 | } 601 | 602 | if (EditorPrefs.GetBool("gitLocksShowForceButtons")) 603 | { 604 | if (GUILayout.Button("Force unlock")) 605 | { 606 | if (EditorUtility.DisplayDialog("Force unlock ?", "Are you sure you want to force the unlock ? It may mess with a teammate's work !", "Yes, I know the risks", "Cancel, I'm not sure")) 607 | { 608 | GitLocks.UnlockFile(lo.path, true); 609 | GitLocks.RefreshLocks(); 610 | } 611 | } 612 | } 613 | 614 | GUILayout.EndHorizontal(); 615 | } 616 | } 617 | 618 | private static Texture GetGreenLockIcon(bool forceReload = false) 619 | { 620 | if (greenLockIcon == null || forceReload) 621 | { 622 | if (EditorPrefs.HasKey("gitLocksColorblindMode") && EditorPrefs.GetBool("gitLocksColorblindMode")) 623 | { 624 | greenLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/greenLock_cb.png", typeof(Texture)); 625 | } 626 | else 627 | { 628 | greenLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/greenLock.png", typeof(Texture)); 629 | } 630 | } 631 | 632 | return greenLockIcon; 633 | } 634 | 635 | private static Texture GetOrangeLockIcon(bool forceReload = false) 636 | { 637 | if (orangeLockIcon == null || forceReload) 638 | { 639 | if (EditorPrefs.HasKey("gitLocksColorblindMode") && EditorPrefs.GetBool("gitLocksColorblindMode")) 640 | { 641 | orangeLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/orangeLock_cb.png", typeof(Texture)); 642 | } 643 | else 644 | { 645 | orangeLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/orangeLock.png", typeof(Texture)); 646 | } 647 | } 648 | 649 | return orangeLockIcon; 650 | } 651 | 652 | private static Texture GetRedLockIcon(bool forceReload = false) 653 | { 654 | if (redLockIcon == null || forceReload) 655 | { 656 | if (EditorPrefs.HasKey("gitLocksColorblindMode") && EditorPrefs.GetBool("gitLocksColorblindMode")) 657 | { 658 | redLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/redLock_cb.png", typeof(Texture)); 659 | } 660 | else 661 | { 662 | redLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/redLock.png", typeof(Texture)); 663 | } 664 | } 665 | 666 | return redLockIcon; 667 | } 668 | 669 | private static Texture GetMixedLockIcon(bool forceReload = false) 670 | { 671 | if (mixedLockIcon == null || forceReload) 672 | { 673 | if (EditorPrefs.HasKey("gitLocksColorblindMode") && EditorPrefs.GetBool("gitLocksColorblindMode")) 674 | { 675 | mixedLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/mixedLock_cb.png", typeof(Texture)); 676 | } 677 | else 678 | { 679 | mixedLockIcon = (Texture)AssetDatabase.LoadAssetAtPath("Packages/com.tomduchene.unity-git-locks/Editor/Textures/mixedLock.png", typeof(Texture)); 680 | } 681 | } 682 | 683 | return mixedLockIcon; 684 | } 685 | 686 | public static void RefreshLockIcons() 687 | { 688 | GetGreenLockIcon(true); 689 | GetOrangeLockIcon(true); 690 | GetRedLockIcon(true); 691 | GetMixedLockIcon(true); 692 | } 693 | 694 | // ------------------------ 695 | // Git lock window features 696 | // ------------------------ 697 | private void OnGUI() 698 | { 699 | if (!GitLocks.IsEnabled()) 700 | { 701 | GUILayout.Label("Tool disabled", EditorStyles.wordWrappedLabel); 702 | if (GUILayout.Button("Enable in preferences")) 703 | { 704 | SettingsService.OpenUserPreferences("Preferences/Git Locks"); 705 | } 706 | } 707 | else 708 | { 709 | GitLocks.CheckLocksRefresh(); 710 | 711 | EditorGUILayout.Space(10); 712 | 713 | if (GitLocks.GetGitUsername() == string.Empty) 714 | { 715 | // If username hasn't been set, show this window instead of the main one to ask the user to input it's username 716 | GUILayout.Label("You need to setup your Git LFS host username for the tool to work properly, most likely your Github username", EditorStyles.wordWrappedLabel); 717 | GUILayout.BeginHorizontal(); 718 | if (GUILayout.Button("Go on github")) 719 | { 720 | UnityEngine.Application.OpenURL("https://github.com/"); 721 | } 722 | 723 | if (GUILayout.Button("Setup username in preferences")) 724 | { 725 | SettingsService.OpenUserPreferences("Preferences/Git Locks"); 726 | } 727 | 728 | GUILayout.EndHorizontal(); 729 | } 730 | else 731 | { 732 | // Refresh locks info 733 | GUILayout.BeginHorizontal(); 734 | if (GitLocks.CurrentlyRefreshing) 735 | { 736 | GUILayout.Label("Last refresh time : currently refreshing..."); 737 | } 738 | else 739 | { 740 | GUILayout.Label("Last refresh time : " + GitLocks.LastRefresh.ToShortTimeString()); 741 | } 742 | 743 | EditorGUI.BeginDisabledGroup(true); 744 | bool autoRefresh = EditorPrefs.GetBool("gitLocksAutoRefreshLocks", true); 745 | if (autoRefresh) 746 | { 747 | int refreshLocksInterval = EditorPrefs.GetInt("gitLocksRefreshLocksInterval"); 748 | string refreshIntervalStr = "Auto refresh every " + refreshLocksInterval.ToString() + " " + (refreshLocksInterval > 1 ? "minutes" : "minute"); 749 | GUILayout.Label(refreshIntervalStr); 750 | } 751 | else 752 | { 753 | GUILayout.Label("Manual refresh only"); 754 | } 755 | 756 | EditorGUI.EndDisabledGroup(); 757 | if (GUILayout.Button("Setup")) 758 | { 759 | SettingsService.OpenUserPreferences("Preferences/Git Locks"); 760 | } 761 | 762 | GUILayout.EndHorizontal(); 763 | 764 | // Refresh button 765 | if (GUILayout.Button("Refresh Git LFS locks")) 766 | { 767 | GitLocks.RefreshLocks(); 768 | } 769 | 770 | DrawUILine(Color.grey, 2, 20); 771 | 772 | // Lock action 773 | GUILayout.BeginHorizontal(); 774 | EditorGUILayout.LabelField("Lock an asset :"); 775 | this.objectToLock = EditorGUILayout.ObjectField(this.objectToLock, typeof(UnityEngine.Object), true); 776 | if (GUILayout.Button("Lock")) 777 | { 778 | string path = AssetDatabase.GetAssetPath(this.objectToLock); 779 | 780 | if (path == null || path == string.Empty) 781 | { 782 | path = GitLocks.GetAssetPathFromPrefabGameObject(this.objectToLock.GetInstanceID()); 783 | } 784 | 785 | GitLocks.LockFile(path); 786 | this.objectToLock = null; 787 | GitLocks.RefreshLocks(); 788 | } 789 | 790 | GUILayout.EndHorizontal(); 791 | 792 | GUILayout.Space(5); 793 | 794 | // My locks 795 | GUILayout.BeginHorizontal(); 796 | EditorGUILayout.LabelField("My locks", EditorStyles.boldLabel); 797 | 798 | GUILayout.FlexibleSpace(); 799 | 800 | bool allSelected = false; 801 | if (selectedLocks != null && GitLocks.GetMyLocks() != null && selectedLocks.Count > 0) 802 | { 803 | allSelected = selectedLocks.Count == GitLocks.GetMyLocks().Count; 804 | } 805 | bool allSelectedCheckbox = GUILayout.Toggle(allSelected, "All"); 806 | if (selectedLocks != null && allSelectedCheckbox && !allSelected) 807 | { 808 | selectedLocks.Clear(); 809 | selectedLocks.AddRange(GitLocks.GetMyLocks()); 810 | } 811 | else if (selectedLocks != null && !allSelectedCheckbox && allSelected) 812 | { 813 | selectedLocks.Clear(); 814 | } 815 | 816 | GUILayout.Space(10); 817 | 818 | if (GUILayout.Button("Unlock selected", GUILayout.Width(120))) 819 | { 820 | GitLocks.UnlockMultipleLocks(selectedLocks); 821 | selectedLocks.Clear(); 822 | } 823 | 824 | GUILayout.EndHorizontal(); 825 | 826 | if (GitLocks.LockedObjectsCache != null && GitLocks.LockedObjectsCache.Count > 0) 827 | { 828 | // Compute min height for "My locks" 829 | int numOfLines = Mathf.Min(EditorPrefs.GetInt("numOfMyLocksDisplayed", 5), GitLocks.GetMyLocks().Count); 830 | float scrollViewHeight = ((float)numOfLines + 0.5f) * 21.0f; // Line height hardcoded for now, +0.5 is used to make sure there's something crossing the float line for clarity 831 | 832 | this.scrollPosMine = EditorGUILayout.BeginScrollView(this.scrollPosMine, GUILayout.MinHeight(scrollViewHeight)); 833 | foreach (GitLocksObject lo in GitLocks.GetMyLocks()) 834 | { 835 | if (true || lo != null) 836 | { 837 | DrawLockedObjectLine(lo, true); 838 | } 839 | } 840 | 841 | EditorGUILayout.EndScrollView(); 842 | } 843 | 844 | DrawUILine(Color.grey, 2, 20); 845 | 846 | // Other locks 847 | GUILayout.BeginHorizontal(); 848 | EditorGUILayout.LabelField("Other locks", EditorStyles.boldLabel); 849 | GUILayout.EndHorizontal(); 850 | if (GitLocks.LockedObjectsCache != null && GitLocks.LockedObjectsCache.Count > 0) 851 | { 852 | this.scrollPosOthers = EditorGUILayout.BeginScrollView(this.scrollPosOthers); 853 | foreach (GitLocksObject lo in GitLocks.GetOtherLocks()) 854 | { 855 | if (true || lo != null) 856 | { 857 | DrawLockedObjectLine(lo); 858 | } 859 | } 860 | 861 | EditorGUILayout.EndScrollView(); 862 | } 863 | } 864 | } 865 | } 866 | } 867 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksDisplay.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f9b21c94fad686a488c68535e78cef95 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksFileModificationWarning.cs: -------------------------------------------------------------------------------- 1 | // All rights reserved. 2 | 3 | using UnityEditor; 4 | 5 | public class GitLocksFileModificationWarning : UnityEditor.AssetModificationProcessor 6 | { 7 | private static string[] OnWillSaveAssets(string[] paths) 8 | { 9 | if (!GitLocks.IsEnabled()) 10 | { 11 | return paths; // Early return if the whole tool is disabled 12 | } 13 | 14 | // Refresh the uncommited cache as it probably has changed 15 | GitLocks.SetUncommitedCacheDirty(); 16 | 17 | // check if we should display a warning for these files 18 | if (GitLocks.GetDisplayLocksConflictWarning()) 19 | { 20 | foreach (string path in paths) 21 | { 22 | GitLocksObject lo = GitLocks.GetObjectInLockedCache(path); 23 | if (lo != null && !lo.IsMine() && !GitLocks.IsFileInConflictIgnoreList(path)) 24 | { 25 | EditorUtility.DisplayDialog("Warning", "The following file you just saved is currently locked by " + lo.owner.name + ", you will not be able to push it.\n" + lo.path, "OK"); 26 | GitLocks.AddFileToConflictWarningIgnoreList(path); 27 | } 28 | } 29 | } 30 | 31 | return paths; 32 | } 33 | } -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksFileModificationWarning.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4fd98a686d11d484dbad19a9d8da2672 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksObject.cs: -------------------------------------------------------------------------------- 1 | // All rights reserved. 2 | 3 | using System; 4 | using System.Runtime.Serialization; 5 | using UnityEditor; 6 | 7 | [System.Serializable] 8 | public class GitLocksObject 9 | { 10 | public int id { get; set; } 11 | 12 | public string path { get; set; } 13 | 14 | public LockedObjectOwner owner { get; set; } 15 | 16 | public string locked_at { get; set; } 17 | 18 | public UnityEngine.Object objectRef = null; 19 | 20 | public UnityEngine.Object GetObjectReference() 21 | { 22 | if (this.objectRef != null) 23 | { 24 | return this.objectRef; 25 | } 26 | else 27 | { 28 | this.objectRef = AssetDatabase.LoadMainAssetAtPath(path); 29 | return this.objectRef; 30 | } 31 | } 32 | 33 | public bool IsMine() 34 | { 35 | return this.owner.name == GitLocks.GetGitUsername(); 36 | } 37 | 38 | public string GetLockDateTimeString() 39 | { 40 | DateTime dt = DateTime.Parse(locked_at); 41 | string r = dt.ToShortDateString() + " - " + dt.ToShortTimeString(); 42 | return r; 43 | } 44 | 45 | [OnDeserialized] 46 | internal void OnDeserializedMethod(StreamingContext context) 47 | { 48 | path = FindRelativePath("Assets"); 49 | } 50 | 51 | private string FindRelativePath(string relativeFolder) 52 | { 53 | int index = path.IndexOf(relativeFolder, StringComparison.OrdinalIgnoreCase); 54 | if (index != -1) return path.Substring(index); 55 | else return path; 56 | } 57 | 58 | } 59 | 60 | public class LockedObjectOwner 61 | { 62 | public string name; 63 | } -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksObject.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9fd381c66f64f8d419cba9fe8af2d90c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksPreferences.cs: -------------------------------------------------------------------------------- 1 | // All rights reserved. 2 | 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | public class GitLocksPreferences : SettingsProvider 7 | { 8 | public GitLocksPreferences(string path, SettingsScope scope = SettingsScope.User) : base(path, scope) 9 | { 10 | } 11 | 12 | [SettingsProvider] 13 | public static SettingsProvider RegisterSettingsProvider() 14 | { 15 | var provider = new GitLocksPreferences("Preferences/Git Locks", SettingsScope.User); 16 | 17 | // Automatically extract all keywords from the Styles. 18 | provider.keywords = new string[] 19 | { 20 | "Enable Git LFS lock tool", 21 | "Git host username", 22 | "Auto refresh locks", 23 | "Refresh locks interval (minutes)", 24 | "Display locks conflict warning", 25 | "Warn if I still own locks on quit", 26 | "Minimum number of my locks to show" 27 | }; 28 | return provider; 29 | } 30 | 31 | public override void OnGUI(string searchContext) 32 | { 33 | EditorGUILayout.Space(); 34 | EditorGUILayout.LabelField("General settings", EditorStyles.boldLabel); 35 | 36 | float previousLabelWidth = EditorGUIUtility.labelWidth; 37 | EditorGUI.indentLevel++; 38 | 39 | GUILayout.Space(5); 40 | 41 | EditorGUI.BeginChangeCheck(); 42 | bool enabled = EditorGUILayout.ToggleLeft(new GUIContent("Enable Git LFS locks tool"), EditorPrefs.GetBool("gitLocksEnabled"), GUILayout.Width(180)); 43 | if (EditorGUI.EndChangeCheck()) 44 | { 45 | EditorPrefs.SetBool("gitLocksEnabled", enabled); 46 | } 47 | 48 | if (enabled) 49 | { 50 | GUILayout.Space(5); 51 | 52 | EditorGUI.BeginChangeCheck(); 53 | EditorGUILayout.BeginHorizontal(); 54 | string username = EditorGUILayout.TextField(new GUIContent("Git host username"), EditorPrefs.GetString("gitLocksHostUsername")); 55 | if (EditorGUI.EndChangeCheck()) 56 | { 57 | EditorPrefs.SetString("gitLocksHostUsername", username); 58 | } 59 | 60 | if (GUILayout.Button("Go on github")) 61 | { 62 | UnityEngine.Application.OpenURL("https://github.com/"); 63 | } 64 | 65 | EditorGUILayout.EndHorizontal(); 66 | 67 | GUILayout.Space(5); 68 | 69 | EditorGUI.BeginChangeCheck(); 70 | int maxFilesNumPerRequest = EditorGUILayout.IntField(new GUIContent("Max number of files grouped per request"), EditorPrefs.GetInt("gitLocksMaxFilesNumPerRequest")); 71 | if (EditorGUI.EndChangeCheck()) 72 | { 73 | EditorPrefs.SetInt("gitLocksMaxFilesNumPerRequest", maxFilesNumPerRequest); 74 | } 75 | 76 | GUILayout.Space(5); 77 | 78 | EditorGUILayout.BeginHorizontal(); 79 | EditorGUI.BeginChangeCheck(); 80 | bool autoRefresh = EditorGUILayout.ToggleLeft(new GUIContent("Auto refresh locks"), EditorPrefs.GetBool("gitLocksAutoRefreshLocks"), GUILayout.Width(135)); 81 | if (EditorGUI.EndChangeCheck()) 82 | { 83 | EditorPrefs.SetBool("gitLocksAutoRefreshLocks", autoRefresh); 84 | } 85 | 86 | EditorGUI.BeginDisabledGroup(!autoRefresh); 87 | EditorGUILayout.LabelField("every", GUILayout.Width(50)); 88 | EditorGUI.BeginChangeCheck(); 89 | int interval = EditorGUILayout.IntField(EditorPrefs.GetInt("gitLocksRefreshLocksInterval"), GUILayout.Width(40)); 90 | if (EditorGUI.EndChangeCheck()) 91 | { 92 | EditorPrefs.SetInt("gitLocksRefreshLocksInterval", interval); 93 | } 94 | 95 | EditorGUILayout.LabelField("minutes", GUILayout.Width(70)); 96 | 97 | EditorGUI.EndDisabledGroup(); 98 | 99 | EditorGUILayout.EndHorizontal(); 100 | 101 | // Notifications 102 | EditorGUI.indentLevel--; 103 | EditorGUILayout.Space(10); 104 | EditorGUILayout.LabelField("Notifications", EditorStyles.boldLabel); 105 | previousLabelWidth = EditorGUIUtility.labelWidth; 106 | EditorGUI.indentLevel++; 107 | 108 | EditorGUI.BeginChangeCheck(); 109 | bool displayLocksConflictWarning = EditorGUILayout.ToggleLeft(new GUIContent("Warn if a file I modified is already locked"), EditorPrefs.GetBool("displayLocksConflictWarning")); 110 | if (EditorGUI.EndChangeCheck()) 111 | { 112 | EditorPrefs.SetBool("displayLocksConflictWarning", displayLocksConflictWarning); 113 | } 114 | 115 | EditorGUI.BeginChangeCheck(); 116 | bool warnIfIStillOwnLocksOnQuit = EditorGUILayout.ToggleLeft(new GUIContent("Warn if I still own locks on quit"), EditorPrefs.GetBool("warnIfIStillOwnLocksOnQuit")); 117 | if (EditorGUI.EndChangeCheck()) 118 | { 119 | EditorPrefs.SetBool("warnIfIStillOwnLocksOnQuit", warnIfIStillOwnLocksOnQuit); 120 | } 121 | 122 | EditorGUI.BeginChangeCheck(); 123 | bool warnIfFileHasBeenModifiedOnServer = EditorGUILayout.ToggleLeft(new GUIContent("Warn if the file has already been modified on server when locking"), EditorPrefs.GetBool("warnIfFileHasBeenModifiedOnServer")); 124 | if (EditorGUI.EndChangeCheck()) 125 | { 126 | EditorPrefs.SetBool("warnIfFileHasBeenModifiedOnServer", warnIfFileHasBeenModifiedOnServer); 127 | } 128 | 129 | if (EditorPrefs.GetBool("warnIfFileHasBeenModifiedOnServer")) 130 | { 131 | EditorGUI.BeginChangeCheck(); 132 | string branchesToCheck = EditorGUILayout.TextField(new GUIContent("Other branches to check (',' separated)"), EditorPrefs.GetString("gitLocksBranchesToCheck")); 133 | if (EditorGUI.EndChangeCheck()) 134 | { 135 | EditorPrefs.SetString("gitLocksBranchesToCheck", branchesToCheck); 136 | } 137 | } 138 | 139 | EditorGUI.BeginChangeCheck(); 140 | bool notifyNewLocks = EditorGUILayout.ToggleLeft(new GUIContent("Notify when there are new locks and when launching Unity"), EditorPrefs.GetBool("notifyNewLocks")); 141 | if (EditorGUI.EndChangeCheck()) 142 | { 143 | EditorPrefs.SetBool("notifyNewLocks", notifyNewLocks); 144 | } 145 | 146 | // Display 147 | EditorGUI.indentLevel--; 148 | EditorGUILayout.Space(10); 149 | EditorGUILayout.LabelField("Display", EditorStyles.boldLabel); 150 | previousLabelWidth = EditorGUIUtility.labelWidth; 151 | EditorGUI.indentLevel++; 152 | 153 | EditorGUI.BeginChangeCheck(); 154 | int numOfMyLocksDisplayed = EditorGUILayout.IntField(new GUIContent("Number of my locks displayed"), EditorPrefs.GetInt("numOfMyLocksDisplayed")); 155 | if (EditorGUI.EndChangeCheck()) 156 | { 157 | EditorPrefs.SetInt("numOfMyLocksDisplayed", numOfMyLocksDisplayed); 158 | } 159 | 160 | EditorGUI.BeginChangeCheck(); 161 | bool colorblindMode = EditorGUILayout.ToggleLeft(new GUIContent("Colorblind mode"), EditorPrefs.GetBool("gitLocksColorblindMode")); 162 | if (EditorGUI.EndChangeCheck()) 163 | { 164 | EditorPrefs.SetBool("gitLocksColorblindMode", colorblindMode); 165 | GitLocksDisplay.RefreshLockIcons(); 166 | } 167 | 168 | EditorGUI.BeginChangeCheck(); 169 | bool debugMode = EditorGUILayout.ToggleLeft(new GUIContent("Show debug logs"), EditorPrefs.GetBool("gitLocksDebugMode")); 170 | if (EditorGUI.EndChangeCheck()) 171 | { 172 | EditorPrefs.SetBool("gitLocksDebugMode", debugMode); 173 | } 174 | 175 | EditorGUI.BeginChangeCheck(); 176 | bool showForceButtons = EditorGUILayout.ToggleLeft(new GUIContent("Show Force buttons"), EditorPrefs.GetBool("gitLocksShowForceButtons")); 177 | if (EditorGUI.EndChangeCheck()) 178 | { 179 | EditorPrefs.SetBool("gitLocksShowForceButtons", showForceButtons); 180 | } 181 | } 182 | 183 | GUILayout.Space(10); 184 | 185 | EditorGUI.indentLevel--; 186 | EditorGUIUtility.labelWidth = previousLabelWidth; 187 | 188 | // Misc 189 | EditorGUILayout.LabelField("Misc", EditorStyles.boldLabel); 190 | 191 | EditorGUIUtility.labelWidth = 250; 192 | EditorGUI.indentLevel++; 193 | 194 | EditorGUILayout.BeginHorizontal(); 195 | 196 | EditorGUI.BeginChangeCheck(); 197 | bool showHistoryInBrowser = EditorGUILayout.ToggleLeft(new GUIContent("Show file history in browser"), EditorPrefs.GetBool("gitLocksShowHistoryInBrowser"), GUILayout.Width(195)); 198 | if (EditorGUI.EndChangeCheck()) 199 | { 200 | EditorPrefs.SetBool("gitLocksShowHistoryInBrowser", showHistoryInBrowser); 201 | } 202 | 203 | EditorGUI.BeginDisabledGroup(!showHistoryInBrowser); 204 | EditorGUI.BeginChangeCheck(); 205 | string url = EditorGUILayout.TextField(EditorPrefs.GetString("gitLocksShowHistoryInBrowserUrl")); 206 | if (EditorGUI.EndChangeCheck()) 207 | { 208 | EditorPrefs.SetString("gitLocksShowHistoryInBrowserUrl", url); 209 | } 210 | EditorGUI.EndDisabledGroup(); 211 | 212 | EditorGUILayout.EndHorizontal(); 213 | 214 | GUILayout.Space(10); 215 | 216 | EditorGUI.indentLevel--; 217 | EditorGUIUtility.labelWidth = previousLabelWidth; 218 | 219 | // Git config and troubleshooting 220 | EditorGUILayout.LabelField("Git config and troubleshooting", EditorStyles.boldLabel); 221 | 222 | EditorGUIUtility.labelWidth = 250; 223 | EditorGUI.indentLevel++; 224 | 225 | EditorGUILayout.Space(); 226 | 227 | EditorGUILayout.LabelField(new GUIContent(GitLocks.GetGitVersion()), GUILayout.Height(25)); 228 | if (GitLocks.IsGitOutdated()) 229 | { 230 | EditorGUILayout.LabelField(new GUIContent("Your git version seems outdated (2.30.0 minimum), you may need to update it and then setup the Credentials Manager for the authentication to work properly"), EditorStyles.wordWrappedLabel); 231 | if (GUILayout.Button("Update Git for Windows")) 232 | { 233 | GitLocks.ExecuteProcessTerminal("git", "update-git-for-windows", true); 234 | } 235 | } 236 | 237 | if (GUILayout.Button("Setup credentials manager (when using HTTPS)")) 238 | { 239 | GitLocks.ExecuteProcessTerminalWithConsole("git", "config --local credential.helper manager"); 240 | } 241 | 242 | EditorGUI.indentLevel--; 243 | } 244 | } -------------------------------------------------------------------------------- /Editor/Scripts/GitLocksPreferences.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6984f267f083aa54f8c7d62ac77a5eba 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Textures.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d95e68e708191a94ead1649dc333a3d2 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Textures/greenLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/greenLock.png -------------------------------------------------------------------------------- /Editor/Textures/greenLock.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bea65576b35b3504db4f2476e3d41487 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/greenLock_cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/greenLock_cb.png -------------------------------------------------------------------------------- /Editor/Textures/greenLock_cb.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8e9097f0e4f64c74ba9d9c417f9f45fe 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/mixedLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/mixedLock.png -------------------------------------------------------------------------------- /Editor/Textures/mixedLock.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f092de50f1c58904f9b74824046ba6e2 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/mixedLock_cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/mixedLock_cb.png -------------------------------------------------------------------------------- /Editor/Textures/mixedLock_cb.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1e0a33f582e26a441a7188b9b24ebcd2 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/orangeLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/orangeLock.png -------------------------------------------------------------------------------- /Editor/Textures/orangeLock.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4a3e3ac4b67ff4d49a5892b5739c1a00 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/orangeLock_cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/orangeLock_cb.png -------------------------------------------------------------------------------- /Editor/Textures/orangeLock_cb.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6d664e093d8cc704da052d22771bacaf 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/redLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/redLock.png -------------------------------------------------------------------------------- /Editor/Textures/redLock.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a698f22e17519e3438bb2da7e5010ea3 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/Textures/redLock_cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomDuchene/unity-git-locks/80a2d4ad8b45b5b88b3bc77a79f96b5670dd8f18/Editor/Textures/redLock_cb.png -------------------------------------------------------------------------------- /Editor/Textures/redLock_cb.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f13b7a145a92a164990a2d8e8e01400f 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 11 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: 1 35 | aniso: 1 36 | mipBias: 0 37 | wrapU: 0 38 | wrapV: 0 39 | wrapW: 0 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | applyGammaDecoding: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 2048 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | spriteSheet: 75 | serializedVersion: 2 76 | sprites: [] 77 | outline: [] 78 | physicsShape: [] 79 | bones: [] 80 | spriteID: 81 | internalID: 0 82 | vertices: [] 83 | indices: 84 | edges: [] 85 | weights: [] 86 | secondaryTextures: [] 87 | spritePackingTag: 88 | pSDRemoveMatte: 0 89 | pSDShowRemoveMatteOption: 0 90 | userData: 91 | assetBundleName: 92 | assetBundleVariant: 93 | -------------------------------------------------------------------------------- /Editor/tomduchene.unity-git-locks.editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity Git Locks Editor", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /Editor/tomduchene.unity-git-locks.editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4d7332a5b7562a647b5f4cf48f15cd65 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tom Duchene and Tactical-Adventures 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa42bc20fe5d65c4f8913277492c48e0 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Git Locks 2 | 3 | Provides an extensive Unity integration for Git LFS locks, which are are essentials when working in teams to prevent conflicts, especially on binary files. 4 | 5 | Locks window 6 | 7 | ## Philosophy 8 | 9 | The plugin uses simple calls to the Git LFS locks commands, which means you have to make sure that your setup is configured correctly to use LFS locks through the CLI. It also makes it independent from any authentication process: if you can use it through command line it will work in Unity! 10 | 11 | This tool is intended to be used by developers, designers and artists alike, providing a flexible, easy and safe way of working as a team. 12 | 13 |   14 |   15 | 16 | ## How to use 17 | 18 | ### Installation 19 | 20 | The easiest way is probably to add this repository as a package, either: 21 | 22 | • [Automatically](https://docs.unity3d.com/Manual/upm-ui-giturl.html) (with url https://github.com/TomDuchene/unity-git-locks.git) 23 | 24 | • Manually, by cloning this repository in your repository's Packages folder 25 | 26 | ### Repository configuration recommendations 27 | 28 | We recommend that you setup your repository to **not** set as read-only the files that haven't been locked: 29 | 30 | `git config lfs.setlockablereadonly false` 31 | 32 | This allows users to modify any file and only prevents them from pushing changes to a locked file. The tool displays (opt-out) warnings when modifying a file that has been locked by someone else. 33 | 34 | It also means you won't have to declare which file types are lockable. 35 | 36 | You could in theory use the tool with the read-only mode enabled but it has not been tested. 37 | 38 | ### Initial setup 39 | 40 | The only thing you absolutely need to setup before use is your provider's (e.g. Github) username, so the tool knows that it's you. 41 | You have to input it in the Preferences window (`Edit/Preferences/Git Locks/Git host username`), which is also accessible through the "Git Locks" window. 42 | 43 | Set Username 44 | 45 | ### Troubleshooting 46 | 47 | Every git setup is a little bit different, so here's a [troubleshooting wiki page](https://github.com/TomDuchene/unity-git-locks/wiki/Troubleshooting) to help if you have errors. 48 | 49 |   50 |   51 | 52 | ## Features 53 | 54 | ### Main window 55 | 56 | Main locks window 57 | 58 | Accessible through `Window/Git Locks`, this window will show you : 59 | 60 | - Refresh time information 61 | - A setup button to go back to the preferences 62 | - A button to manually trigger the locks refresh 63 | - An object field and button to lock an asset 64 | - A checkbox to select/unselect all your locks at once 65 | - A button to unlock all selected locks 66 | - A view of all the objects you have locked, with clickable object fields that focus the object in the project window, hoverable paths, and unlock buttons to release the lock on a file 67 | - A similar view of the locks owned by other team members 68 | 69 | ### Contextual menus 70 | 71 | HierarchyProject 72 | 73 | On a right-click, you can lock/unlock any file in the project window, and lock/unlock prefabs in the hierarchy window. 74 | 75 | Multi-selection and folder selection are supported, provided that the files in the selection are all lockable / all unlockable. 76 | 77 | In the project window, you can also show the file's git history, in a terminal (default) or in a browser (configurable through the settings). 78 | 79 | ### Lock icons 80 | 81 | Lock icons are displayed on all locked files and folders in the Git locks, Project, and Hierarchy windows. They can be clicked to obtain information on their status and unlock them directly (if they are yours). 82 | 83 | All locks are color-coded : 84 | 85 | Lock**File locked by you** 86 | 87 | You’re safe working on it (don’t forget to unlock it when you’re done). Also displayed on a folder containing at least one of your locks. 88 | 89 | Lock**File locked by someone else** 90 | 91 | You shouldn’t modify it (if you need to you can ask the owner of the lock to release it, if he/she’s drinking coconut water in Miami for the next two months you could probably force the unlock in the Git Locks window). Also displayed on a folder containing at least one of someone else's locks. 92 | 93 | Lock**File locked by someone else conflicting with your changes** 94 | 95 | Someone has locked this file but you’ve made changes to it anyway which you will not be able to push (if you’re just testing things out it’s ok, but you’ll have to discard changes before you push) 96 | 97 | Lock**Folder containing locks by you and someone else** 98 | 99 | This folder contains both locks of yours and others. 100 | 101 | ### Preferences 102 | 103 | Set Username 104 | 105 | Many options are available to customize your use of the tool: 106 | 107 | - **Max number of files grouped per request:** the bigger the number, the faster the overall locking/unlocking operation will be when processing dozens of files. If you go too high it might trigger the process timeout. Some users have errors where only one file can be processed at a time, it this case keep the value at 1 108 | - **Auto refresh toggle and time:** the refresh is done asynchronously, so you can set it pretty low if there are a lot of people working simultaneously. It is recommended to keep the auto refresh on. 109 | - **Notifications:** while not mandatory, most are really important for a smooth experience 110 | - **Other branches to check:** what branches to check in addition to the current one when verifying that the file you're about to lock hasn't been modified on the server. No spaces, comma separated. 111 | - **Number of my locks displayed:** used to show a minimum number of your own locks before the others' 112 | - **Colorblind mode:** changes all lock icons to have different shapes and a more inclusive color scheme 113 | - **Show debug logs:** show additional logs, especially the commands called 114 | - **Show Force buttons:** display additional Force Unlock buttons, which will probably work only if you have the rights to do so 115 | - **Show file history in browser:** Instead of showing the file history in a terminal, show it in your browser. You have to enter a URL corresponding to you host's format, for example "https://github.com/MyUserName/MyRepo/blob/$branch/$assetPath" (GitHub) 116 | - **Git version and update button:** you'll need to update if the version is too old 117 | - **Setup credentials manager button:** helps some users configure their CLI to authenticate if their credentials manager is not set correctly 118 | 119 | ### Keyboard shortcuts 120 | 121 | In the project window, you can use Ctrl + Maj + **L** or **U** to **L**ock/**U**nlock a file or selection of files. 122 | 123 | If you prefer having shortcuts to lock/unlock prefabs in the hierarchy, you can add some in `Edit / Shortcuts / “Main Menu/GameObject/Git LFS Lock” (or “Unlock”)` 124 | 125 |   126 |   127 | 128 | ## Misc 129 | 130 | This tool has been created by [Tom Duchêne](http://tomduchene.fr) at [Tactical Adventures](http://tactical-adventures.com), and is under the M.I.T. license. 131 | We might examine bug reports and pull requests but we can't offer you any kind of warranty or active support. 132 | We hope this tool helps your team as much as it helped ours ♥ 133 | 134 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f6ab5a70c9ff2d844bd493390cb45cba 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.tomduchene.unity-git-locks", 3 | "displayName": "Git Locks", 4 | "version": "1.2.9", 5 | "description": "An editor tool to manipulate git LFS locks", 6 | "keywords": [ 7 | "unity", 8 | "unity3d", 9 | "utility" 10 | ], 11 | "homepage": "https://github.com/TomDuchene/unity-git-locks", 12 | "bugs": { 13 | "url": "https://github.com/TomDuchene/unity-git-locks/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+ssh://git@github.com/TomDuchene/unity-git-locks.git" 18 | }, 19 | "license": "MIT", 20 | "author": "Tom Duchene (http://tomduchene.fr/)", 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "type": "Editor tool", 25 | "dependencies": { 26 | "com.unity.nuget.newtonsoft-json": "2.0.0" 27 | } 28 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10fc9bf638c381f45bd499786006a7f3 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------