public override void Add(IEnumerable <string> assetPaths, bool includeMeta, bool wait = false) { if (!assetPaths.Any()) { return; } foreach (var path in assetPaths) { if (!WiseSVNIntegration.CheckAndAddParentFolderIfNeeded(path, true)) { return; } } // Don't give versioned metas, as tortoiseSVN doesn't like it. var metas = assetPaths .Select(path => path + ".meta") .Where(path => WiseSVNIntegration.GetStatus(path).Status == VCFileStatus.Unversioned) ; var pathsArg = AssetPathsToContextPaths(includeMeta ? assetPaths.Concat(metas) : assetPaths, false); var result = ShellUtils.ExecuteCommand(ClientCommand, $"/command:add /path:\"{pathsArg}\"", wait); if (!string.IsNullOrEmpty(result.Error)) { Debug.LogError($"SVN Error: {result.Error}"); } }
public void ForceLock(IEnumerable <SVNStatusData> lockedByOtherEntries) { var shouldLog = SVNPreferencesManager.Instance.PersonalPrefs.TraceLogs.HasFlag(SVNTraceLogs.SVNOperations); var lockMessage = SVNPreferencesManager.Instance.ProjectPrefs.AutoLockMessage; var targetsFileToUse = FileUtil.GetUniqueTempPathInProject(); // Not thread safe - call in main thread only. EnqueueOperation(op => WiseSVNIntegration.LockFiles(lockedByOtherEntries.Select(sd => sd.Path), true, lockMessage, "", targetsFileToUse)) .Completed += (op) => { if (op.Result != LockOperationResult.Success) { Debug.LogError($"Auto-locking by force failed with result {op.Result} for assets:\n{string.Join("\n", lockedByOtherEntries.Select(sd => sd.Path))}."); EditorUtility.DisplayDialog("SVN Auto-Locking", "Stealing lock failed. Check the logs for more info.", "I will!"); } else if (shouldLog) { Debug.Log($"Auto-locked assets by force:\n{string.Join("\n", lockedByOtherEntries.Select(sd => sd.Path))}"); } }; if (m_PendingOperations.Count > 0 && m_HasPendingOperations == false) { m_HasPendingOperations = true; m_PendingOperations.Peek().Start(); } }
private static bool DiffAsset(string assetPath) { var statusData = WiseSVNIntegration.GetStatus(assetPath); var isModified = statusData.Status != VCFileStatus.Normal && statusData.Status != VCFileStatus.Unversioned && statusData.Status != VCFileStatus.Conflicted ; isModified |= statusData.PropertiesStatus == VCPropertiesStatus.Modified; if (isModified) { m_Integration?.DiffChanges(assetPath, false); return(true); } if (statusData.Status == VCFileStatus.Conflicted || statusData.PropertiesStatus == VCPropertiesStatus.Conflicted) { m_Integration?.Resolve(assetPath, false); return(true); } return(false); }
public override void Initialize(bool freshlyCreated) { var lastModifiedDate = File.Exists(PROJECT_PREFERENCES_PATH) ? File.GetLastWriteTime(PROJECT_PREFERENCES_PATH).Ticks : 0 ; if (freshlyCreated || m_ProjectPrefsLastModifiedTime != lastModifiedDate) { LoadPreferences(); } if (freshlyCreated || m_RetryTextures) { LoadTextures(); m_RetryTextures = false; // If WiseSVN was just added to the project, Unity won't manage to load the textures the first time. Try again next frame. if (FileStatusIcons[(int)VCFileStatus.Added].image == null) { // We're using a flag as assembly reload may happen and update callback will be lost. m_RetryTextures = true; EditorApplication.CallbackFunction reloadTextures = null; reloadTextures = () => { LoadTextures(); m_RetryTextures = false; EditorApplication.update -= reloadTextures; }; EditorApplication.update += reloadTextures; } Debug.Log($"Loaded WiseSVN Preferences. WiseSVN is turned {(PersonalPrefs.EnableCoreIntegration ? "on" : "off")}."); if (PersonalPrefs.EnableCoreIntegration) { var svnError = WiseSVNIntegration.CheckForSVNErrors(); // svn: warning: W155007: '...' is not a working copy! // This can be returned when project is not a valid svn checkout. (Probably) if (svnError.Contains("W155007")) { Debug.LogError("This project is NOT under version control (not a proper SVN checkout)."); // System.ComponentModel.Win32Exception (0x80004005): ApplicationName='...', CommandLine='...', Native error= The system cannot find the file specified. // Could not find the command executable. The user hasn't installed their CLI (Command Line Interface) so we're missing an "svn.exe" in the PATH environment. // This is allowed only if there isn't ProjectPreference specified CLI path. } else if (svnError.Contains("0x80004005")) { Debug.LogError("SVN CLI (Command Line Interface) not found. You need to install it in order for the SVN integration to work properly."); // Any other error. } else if (!string.IsNullOrEmpty(svnError)) { Debug.LogError($"SVN command line interface returned this error:\n{svnError}"); } } } SVNContextMenusManager.SetupContextType(PersonalPrefs.ContextMenusClient); }
private void OnGUI() { var outputStyle = new GUIStyle(EditorStyles.textArea); outputStyle.wordWrap = false; var textSize = outputStyle.CalcSize(new GUIContent(m_CombinedOutput)); m_OutputScroll = EditorGUILayout.BeginScrollView(m_OutputScroll); EditorGUILayout.LabelField(m_CombinedOutput, outputStyle, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true), GUILayout.MinWidth(textSize.x), GUILayout.MinHeight(textSize.y)); EditorGUILayout.EndScrollView(); EditorGUILayout.BeginHorizontal(); { bool isWorking = m_SVNOperation == null || m_SVNOperation.HasFinished; EditorGUI.BeginDisabledGroup(isWorking); if (GUILayout.Button("Abort")) { m_SVNOperation.Abort(false); m_CombinedOutput += "Aborting...\n"; m_StateLabel = "Aborting..."; } if (GUILayout.Button("Kill")) { m_SVNOperation.Abort(true); m_CombinedOutput += "Killing...\n"; m_StateLabel = "Killing..."; } EditorGUI.EndDisabledGroup(); GUILayout.FlexibleSpace(); EditorGUILayout.LabelField(m_StateLabel); GUILayout.FlexibleSpace(); if (GUILayout.Button("Clear")) { m_CombinedOutput = ""; } EditorGUI.BeginDisabledGroup(!isWorking); if (GUILayout.Button("Get Status")) { m_SVNOperation = WiseSVNIntegration.GetStatusesAsync(".", recursive: true, offline: false); m_SVNOperation.AnyOutput += (line) => { m_CombinedOutput += line + "\n"; }; m_SVNOperation.Completed += (op) => { m_StateLabel = op.AbortRequested ? "Aborted!" : "Completed!"; m_CombinedOutput += m_StateLabel + "\n\n"; m_SVNOperation = null; }; m_StateLabel = "Working..."; } EditorGUI.EndDisabledGroup(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); { GUILayout.FlexibleSpace(); GUILayout.Label("External SVN Client:"); if (GUILayout.Button("Commit", GUILayout.ExpandWidth(false))) { SVNContextMenusManager.CommitAll(); } if (GUILayout.Button("Update", GUILayout.ExpandWidth(false))) { SVNContextMenusManager.UpdateAll(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); }
private void OnStatusDatabaseChanged() { if (!IsActive) { return; } var statusDatabaseData = SVNStatusesDatabase.Instance.GetAllKnownStatusData(false, true, true) .Where(sd => !Directory.Exists(sd.Path)) .ToList() ; var newEntries = new List <SVNStatusData>(); foreach (var statusData in statusDatabaseData) { if (AddOrUpdateKnowsStatusData(statusData)) { newEntries.Add(statusData); } } m_KnownData.RemoveAll(known => !statusDatabaseData.Any(unknown => known.Path == unknown.Path)); var shouldLock = new List <SVNStatusData>(newEntries.Count); var lockedByOtherEntries = new List <SVNStatusData>(newEntries.Count); var shouldUnlock = new List <SVNStatusData>(); // Check for new assets to lock. foreach (var statusData in newEntries) { // NOTE: Deleted status never occurs as it is not provided by the SVNStatusesDatabase. :( if (statusData.Status != VCFileStatus.Modified && statusData.Status != VCFileStatus.Deleted && statusData.Status != VCFileStatus.Replaced) { continue; } if (statusData.LockStatus == VCLockStatus.LockedHere) { continue; } var assetPath = statusData.Path; bool isMeta = false; if (statusData.Path.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) { assetPath = statusData.Path.Substring(0, statusData.Path.LastIndexOf(".meta")); isMeta = true; } assetPath = assetPath.Replace("\\", "/"); var autoLockingParam = m_ProjectPrefs.AutoLockingParameters .FirstOrDefault(al => assetPath.StartsWith(al.TargetFolder, StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrEmpty(autoLockingParam.TargetFolder)) { continue; } if (isMeta && !autoLockingParam.IncludeTargetMetas) { continue; } if (SVNPreferencesManager.ShouldExclude(autoLockingParam.Exclude, assetPath)) { continue; } bool matched = IsAssetOfType(assetPath, autoLockingParam.TargetTypes, statusData.Status == VCFileStatus.Deleted); if (!matched && !autoLockingParam.TargetTypes.HasFlag(AssetType.OtherTypes)) { continue; } if (statusData.LockStatus == VCLockStatus.NoLock) { shouldLock.Add(statusData); continue; } lockedByOtherEntries.Add(statusData); } // Check for old assets to unlock. foreach (var statusData in statusDatabaseData) { if (statusData.Status != VCFileStatus.Normal) { continue; } var assetPath = statusData.Path; if (statusData.Path.EndsWith(".meta", StringComparison.OrdinalIgnoreCase)) { assetPath = statusData.Path.Substring(0, statusData.Path.LastIndexOf(".meta")); } assetPath = assetPath.Replace("\\", "/"); var autoLockingParam = m_ProjectPrefs.AutoLockingParameters .FirstOrDefault(al => assetPath.StartsWith(al.TargetFolder, StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrEmpty(autoLockingParam.TargetFolder)) { continue; } if (SVNPreferencesManager.ShouldExclude(autoLockingParam.Exclude, assetPath)) { continue; } bool matched = IsAssetOfType(assetPath, autoLockingParam.TargetTypes, false); if (!matched && !autoLockingParam.TargetTypes.HasFlag(AssetType.OtherTypes)) { continue; } if (statusData.LockStatus != VCLockStatus.NoLock && statusData.LockStatus != VCLockStatus.LockedOther) { shouldUnlock.Add(statusData); } } var shouldLog = SVNPreferencesManager.Instance.PersonalPrefs.TraceLogs.HasFlag(SVNTraceLogs.SVNOperations); var lockMessage = SVNPreferencesManager.Instance.ProjectPrefs.AutoLockMessage; if (shouldLock.Count > 0) { var targetsFileToUse = FileUtil.GetUniqueTempPathInProject(); // Not thread safe - call in main thread only. EnqueueOperation(op => WiseSVNIntegration.LockFiles(shouldLock.Select(sd => sd.Path), false, lockMessage, "", targetsFileToUse)) .Completed += (op) => { if (op.Result != LockOperationResult.Success) { Debug.LogError($"Auto-locking failed with result {op.Result} for assets:\n{string.Join("\n", shouldLock.Select(sd => sd.Path))}"); } else if (shouldLog) { Debug.Log($"Auto-locked assets:\n{string.Join("\n", shouldLock.Select(sd => sd.Path))}"); } }; } if (shouldUnlock.Count > 0) { var targetsFileToUse = FileUtil.GetUniqueTempPathInProject(); // Not thread safe - call in main thread only. EnqueueOperation(op => WiseSVNIntegration.UnlockFiles(shouldUnlock.Select(sd => sd.Path), false, targetsFileToUse)) .Completed += (op) => { // If lock was stolen or broken, there is no good way to release it without causing error. // In that case the lock will be cleared from the local cache, so ignore the error. if (op.Result != LockOperationResult.Success && op.Result != LockOperationResult.LockedByOther) { Debug.LogError($"Auto-unlocking failed with result {op.Result} for assets:\n{string.Join("\n", shouldUnlock.Select(sd => sd.Path))}"); } else if (shouldLog) { Debug.Log($"Auto-unlocked assets:\n{string.Join("\n", shouldUnlock.Select(sd => sd.Path))}"); } }; } if (lockedByOtherEntries.Count > 0) { SVNAutoLockingForceWindow.PromptForceLock(lockedByOtherEntries); } if (m_PendingOperations.Count > 0 && m_HasPendingOperations == false) { m_HasPendingOperations = true; m_PendingOperations.Peek().Start(); } }
private void GatherProjectsIn(BranchScanParameters scanParams, List <BranchProject> results) { var listEntries = new List <string>(); var normalizedEntries = new List <string>(); HashSet <string> branchSignatureEntries = new HashSet <string>(scanParams.BranchSignatureRootEntries); var lastBranch = new BranchProject() { BranchName = "Unknown", BranchURL = scanParams.EntryPointURL, BranchRelativePath = string.Empty, UnityProjectURL = string.Empty, }; Queue <string> urls = new Queue <string>(); urls.Enqueue(scanParams.EntryPointURL); while (urls.Count > 0) { var url = urls.Dequeue(); LastProcessedEntry = url.Length == scanParams.EntryPointURL.Length ? scanParams.EntryPointURL : url.Remove(0, scanParams.EntryPointURL.Length + 1); listEntries.Clear(); var opResult = WiseSVNIntegration.ListURL(url, false, listEntries); if (opResult != ListOperationResult.Success) { if (opResult == ListOperationResult.NotFound) { Debug.LogError($"{GetType().Name} failed to find url: \"{url}\"."); } m_LastError = opResult; results.Clear(); return; } // Folders have '/' at the end but the user shouldn't define them this way. normalizedEntries.Clear(); normalizedEntries.Capacity = Mathf.Max(listEntries.Count, normalizedEntries.Capacity); normalizedEntries.AddRange(listEntries.Select(e => e.TrimEnd('/'))); // Is this a branch? if (branchSignatureEntries.IsSubsetOf(normalizedEntries)) { lastBranch.BranchName = Path.GetFileName(url); lastBranch.BranchURL = url; lastBranch.BranchRelativePath = url.Remove(0, scanParams.EntryPointURL.Length + 1); lastBranch.UnityProjectRelativePath = null; lastBranch.UnityProjectURL = null; } // This is a Unity project folder. if (m_UnityProjectEntries.IsSubsetOf(normalizedEntries)) { if (!string.IsNullOrEmpty(lastBranch.UnityProjectURL)) { // TODO: if BranchURL == UnityURL => Shouldn't be a problem. // if BranchURL != UnityRL => take the Unity folder name from the Working Copy (or find its original name in the repository branch). Accept only that name. Debug.LogError($"Multiple Unity projects found in the branch \"{lastBranch.BranchURL}\". This is still not supported.\n{lastBranch.UnityProjectURL}\n{url}"); } lastBranch.UnityProjectURL = url; lastBranch.UnityProjectRelativePath = url.Remove(0, scanParams.EntryPointURL.Length + 1); results.Add(lastBranch); // No need to dig in the Unity project folder. continue; } for (int i = 0; i < normalizedEntries.Count; ++i) { // Only interested in folders. if (listEntries[i].LastOrDefault() != '/') { continue; } var folderName = normalizedEntries[i]; if (scanParams.ExcludesFolderNames.Contains(folderName)) { continue; } urls.Enqueue(url + "/" + folderName); } } }
private static void GatherConflicts(object param) { var jobData = (ConflictsScanJobData)param; var results = jobData.Reults; var logParams = new LogParams() { FetchAffectedPaths = true, FetchCommitMessages = false, StopOnCopy = true, Limit = 10, }; const string svnDateFormat = "yyyy-MM-dd"; switch (jobData.LimitType) { case ConflictsScanLimitType.Days: logParams.RangeStart = "{" + System.DateTime.Now.AddDays(-1 * jobData.LimitParam).ToString(svnDateFormat) + "}"; logParams.RangeEnd = "HEAD"; break; case ConflictsScanLimitType.Weeks: logParams.RangeStart = "{" + System.DateTime.Now.AddDays(-1 * 7 * jobData.LimitParam).ToString(svnDateFormat) + "}"; logParams.RangeEnd = "HEAD"; break; case ConflictsScanLimitType.Months: logParams.RangeStart = "{" + System.DateTime.Now.AddMonths(-1 * jobData.LimitParam).ToString(svnDateFormat) + "}"; logParams.RangeEnd = "HEAD"; break; case ConflictsScanLimitType.Revisions: // Revisions are calculated per branch. Do nothing here. break; case ConflictsScanLimitType.Unlimited: logParams.RangeStart = ""; break; default: Debug.LogError($"Unsupported ConflictsScanLimitType {jobData.LimitType} with param {jobData.LimitParam}"); break; } List <LogEntry> logEntries = new List <LogEntry>(); for (int i = 0; i < results.Length; ++i) { var result = results[i]; logEntries.Clear(); var targetURL = result.UnityURL + "/" + jobData.TargetAssetPath; var targetRelativeURL = WiseSVNIntegration.AssetPathToRelativeURL(targetURL); // Either it doesn't exist in this branch or it was moved / deleted. Can't know for sure without some deep digging. if (string.IsNullOrEmpty(targetRelativeURL)) { result.State = ConflictState.Missing; results[i] = result; continue; } if (jobData.LimitType == ConflictsScanLimitType.Revisions) { var lastChangedRevision = WiseSVNIntegration.LastChangedRevision(targetURL); if (lastChangedRevision < 0) { // Probably doesn't exist in this branch. logParams.RangeStart = ""; logParams.RangeEnd = ""; } else { logParams.RangeStart = (lastChangedRevision - jobData.LimitParam).ToString(); logParams.RangeEnd = lastChangedRevision.ToString(); } } var opResult = WiseSVNIntegration.Log(targetURL, logParams, logEntries, 60000 * 5); // Either it doesn't exist in this branch or it was moved / deleted. Can't know for sure without some deep digging. if (opResult == LogOperationResult.NotFound) { result.State = ConflictState.Missing; results[i] = result; continue; } if (opResult != LogOperationResult.Success) { result.State = ConflictState.Error; results[i] = result; continue; } result.State = ConflictState.Normal; foreach (var logEntry in logEntries) { var logPath = logEntry.AffectedPaths.FirstOrDefault(ap => ap.Path.StartsWith(targetRelativeURL)); // If not found in the affected paths -> this is the log entry of the branch copy. if (string.IsNullOrEmpty(logPath.Path)) { continue; } result.State = ConflictState.Conflicted; // Don't consider folder children for "Added" and "Deleted". Folders are just modified by their children. if (logPath.Path != targetRelativeURL) { continue; } if (logPath.Added || logPath.Replaced) { result.State = ConflictState.Added; break; } if (logPath.Deleted) { result.State = ConflictState.Missing; break; } } results[i] = result; } }
private void DrawBranchesList() { using (var scrollView = new EditorGUILayout.ScrollViewScope(m_BranchesScroll)) { m_BranchesScroll = scrollView.scrollPosition; // For hover effects to work. if (Event.current.type == EventType.MouseMove) { Repaint(); } // TODO: Sort list by folder depths: compare by lastIndexOf('/'). If equal, by string. foreach (var branchProject in Database.BranchProjects) { if (!string.IsNullOrEmpty(m_BranchFilter) && branchProject.BranchName.IndexOf(m_BranchFilter, System.StringComparison.OrdinalIgnoreCase) == -1) { continue; } // Apply setting only in Scanned mode since the thread will still be working otherwise and // cause inconsistent GUILayout structure. if (m_ConflictsScanState == ConflictsScanState.Scanned && !m_ConflictsShowNormal) { var conflictResult = m_ConflictsScanResults.First(r => r.UnityURL == branchProject.UnityProjectURL); if (conflictResult.State == ConflictState.Normal) { continue; } } using (new EditorGUILayout.HorizontalScope(/*BranchRowStyle*/)) { float buttonSize = 24f; bool repoBrowser = GUILayout.Button(RepoBrowserContent, MiniIconButtonlessStyle, GUILayout.Height(buttonSize), GUILayout.Width(buttonSize)); bool showLog = GUILayout.Button(SelectShowLogContent(branchProject), MiniIconButtonlessStyle, GUILayout.Height(buttonSize), GUILayout.Width(buttonSize)); bool switchBranch = GUILayout.Button(SwitchBranchContent, MiniIconButtonlessStyle, GUILayout.Height(buttonSize), GUILayout.Width(buttonSize)); bool branchSelected = GUILayout.Button(new GUIContent(branchProject.BranchRelativePath, branchProject.BranchURL), BranchLabelStyle); if (repoBrowser) { SVNContextMenusManager.RepoBrowser(branchProject.UnityProjectURL + "/" + AssetDatabase.GetAssetPath(m_TargetAsset)); } if (showLog) { SVNContextMenusManager.ShowLog(branchProject.UnityProjectURL + "/" + AssetDatabase.GetAssetPath(m_TargetAsset)); } if (switchBranch) { bool confirm = EditorUtility.DisplayDialog("Switch Operation", "Unity needs to be closed while switching. Do you want to close it?\n\n" + "Reason: if Unity starts crunching assets while SVN is downloading files, the Library may get corrupted.", "Yes!", "No" ); if (confirm && UnityEditor.SceneManagement.EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { var localPath = WiseSVNIntegration.WorkingCopyRootPath(); var targetUrl = branchProject.BranchURL; if (branchProject.BranchURL != branchProject.UnityProjectURL) { bool useBranchRoot = EditorUtility.DisplayDialog("Switch what?", "What do you want to switch?\n" + "- Working copy root (the whole checkout)\n" + "- Unity project folder", "Working copy root", "Unity project"); if (!useBranchRoot) { localPath = WiseSVNIntegration.ProjectRoot; targetUrl = branchProject.UnityProjectURL; } } SVNContextMenusManager.Switch(localPath, targetUrl); EditorApplication.Exit(0); } } if (branchSelected) { var menu = new GenericMenu(); var prevValue = BranchContextMenu.CopyBranchName; foreach (var value in System.Enum.GetValues(typeof(BranchContextMenu)).OfType <BranchContextMenu>()) { if ((int)value / 10 != (int)prevValue / 10) { menu.AddSeparator(""); } menu.AddItem(new GUIContent(ObjectNames.NicifyVariableName(value.ToString())), false, OnSelectBranchOption, new KeyValuePair <BranchContextMenu, BranchProject>(value, branchProject)); prevValue = value; } menu.ShowAsContext(); } } var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandWidth(true), GUILayout.Height(1f)); EditorGUI.DrawRect(rect, Color.black); } } }
public override void Initialize(bool freshlyCreated) { var lastModifiedDate = File.Exists(PROJECT_PREFERENCES_PATH) ? File.GetLastWriteTime(PROJECT_PREFERENCES_PATH).Ticks : 0 ; if (freshlyCreated || m_ProjectPrefsLastModifiedTime != lastModifiedDate) { LoadPreferences(); } if (freshlyCreated || m_RetryTextures) { LoadTextures(); m_RetryTextures = false; // If WiseSVN was just added to the project, Unity won't manage to load the textures the first time. Try again next frame. if (FileStatusIcons[(int)VCFileStatus.Modified].image == null) { // We're using a flag as assembly reload may happen and update callback will be lost. m_RetryTextures = true; EditorApplication.CallbackFunction reloadTextures = null; reloadTextures = () => { LoadTextures(); m_RetryTextures = false; EditorApplication.update -= reloadTextures; if (FileStatusIcons[(int)VCFileStatus.Modified].image == null) { Debug.LogWarning("SVN overlay icons are missing."); } }; EditorApplication.update += reloadTextures; } Debug.Log($"Loaded WiseSVN Preferences. WiseSVN is turned {(PersonalPrefs.EnableCoreIntegration ? "on" : "off")}."); if (PersonalPrefs.EnableCoreIntegration) { var svnError = ""; try { svnError = WiseSVNIntegration.CheckForSVNErrors(); } catch (Exception ex) { PersonalPrefs.EnableCoreIntegration = false; Debug.LogError($"Calling SVN CLI (Command Line Interface) caused fatal error!\nDisabling WiseSVN integration. Please fix the error and restart Unity.\n{ex}\n\n"); #if UNITY_EDITOR_OSX if (ex is IOException) { Debug.LogError($"If you installed SVN via Brew or similar, you may need to add \"/usr/local/bin\" (or wherever svn binaries can be found) to your PATH environment variable. Example:\nsudo launchctl config user path /usr/local/bin\nAlternatively, you may add relative SVN CLI path in your WiseSVN preferences (\"Assets/SVN/SVN Preferences -> Project\")\n"); } #endif } // svn: warning: W155007: '...' is not a working copy! // This can be returned when project is not a valid svn checkout. (Probably) if (svnError.Contains("W155007")) { Debug.LogError("This project is NOT under version control (not a proper SVN checkout)."); // System.ComponentModel.Win32Exception (0x80004005): ApplicationName='...', CommandLine='...', Native error= The system cannot find the file specified. // Could not find the command executable. The user hasn't installed their CLI (Command Line Interface) so we're missing an "svn.exe" in the PATH environment. // This is allowed only if there isn't ProjectPreference specified CLI path. } else if (svnError.Contains("0x80004005")) { Debug.LogError("SVN CLI (Command Line Interface) not found. You need to install it in order for the SVN integration to work properly."); // Any other error. } else if (!string.IsNullOrEmpty(svnError)) { Debug.LogError($"SVN command line interface returned this error:\n{svnError}"); } } } SVNContextMenusManager.SetupContextType(PersonalPrefs.ContextMenusClient); }