using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using Unity.Cloud.Collaborate.Assets; using Unity.Cloud.Collaborate.Models.Api; using Unity.Cloud.Collaborate.Models.Enums; using Unity.Cloud.Collaborate.Models.Structures; using Unity.Cloud.Collaborate.Utilities; using UnityEditor; using UnityEditor.Collaboration; using UnityEditor.Connect; using UnityEditorInternal; using UnityEngine; using UnityEngine.Assertions; using static UnityEditor.Collaboration.Collab; using ProgressInfo = UnityEditor.Collaboration.ProgressInfo; namespace Unity.Cloud.Collaborate.Models.Providers { internal class Collab : ISourceControlProvider { const string k_KServiceUrl = "developer.cloud.unity3d.com"; readonly RevisionsService m_RevisionsService; /// public event Action UpdatedChangeList; /// public event Action> UpdatedSelectedChangeList; /// public event Action UpdatedConflictState; /// public event Action UpdatedRemoteRevisionsAvailability; /// public event Action UpdatedProjectStatus; /// public event Action UpdatedOperationStatus; /// public event Action UpdatedOperationProgress; /// public event Action ErrorOccurred; /// public event Action ErrorCleared; readonly List m_Changes; bool m_ConflictCachedState; bool m_RemoteRevisionsAvailableState; // History entry requesting bits and bobs. readonly Queue<(int offset, int size, Action>)> m_HistoryRequests; [NotNull] IReadOnlyList m_HistoryEntries; (int offset, int size)? m_HistoryEntriesCache; [CanBeNull] IHistoryEntry m_HistoryEntryCache; int? m_HistoryEntryCountCache; string m_TipCache; [CanBeNull] IErrorInfo m_ErrorInfo; [CanBeNull] IProgressInfo m_ProgressInfo; ProjectStatus m_ProjectStatus; public Collab() { m_RevisionsService = new RevisionsService(instance, UnityConnect.instance); m_Changes = new List(); m_HistoryEntries = new List(); m_HistoryRequests = new Queue<(int offset, int size, Action>)>(); // Get initial values. var info = instance.collabInfo; m_ConflictCachedState = info.conflict; m_RemoteRevisionsAvailableState = info.update; m_TipCache = info.tip; m_ProgressInfo = info.inProgress ? ProgressInfoFromCollab(instance.GetJobProgress(0)) : null; m_ErrorInfo = instance.GetError(UnityConnect.UnityErrorFilter.ByContext | UnityConnect.UnityErrorFilter.ByChild, out var errInfo) ? ErrorInfoFromUnity(errInfo) : null; m_ProjectStatus = GetNewProjectStatus(info, UnityConnect.instance.connectInfo, UnityConnect.instance.projectInfo); SetupEvents(); } /// /// Setup events for the provider. /// void SetupEvents() { // just connect notifier events. instance.ChangeItemsChanged += OnChangeItemsChanged; instance.SelectedChangeItemsChanged += OnSelectedChangeItemsChanged; instance.RevisionUpdated_V2 += OnRevisionUpdated; instance.CollabInfoChanged += OnCollabInfoChanged; instance.JobsCompleted += OnJobsCompleted; instance.ErrorOccurred_V2 += OnErrorOccurred; instance.ErrorCleared += OnErrorCleared; instance.StateChanged += OnCollabStateChanged; UnityConnect.instance.StateChanged += OnUnityConnectStateChanged; UnityConnect.instance.ProjectStateChanged += OnUnityConnectProjectStateChanged; m_RevisionsService.FetchRevisionsCallback += OnReceiveHistoryEntries; } #region Callback & Helper Methods /// /// Event handler for when the change list has changed. /// /// New change list. /// Whether or not the list is filtered. Should always be false. void OnChangeItemsChanged(ChangeItem[] changes, bool isFiltered) { UpdateChanges(changes); UpdatedChangeList?.Invoke(); } /// /// WIP method to handle partial publish in collab. /// /// Received changes. /// Whether or not it's a partial publish. Should always be true. void OnSelectedChangeItemsChanged(ChangeItem[] changes, bool isFiltered) { // This is used by selective commit. Assert all API calls to here are setting isFiltered to true ! Debug.Assert(isFiltered); var selectedChanges = changes.Select(e => e.Path).ToList(); UpdatedSelectedChangeList?.Invoke(selectedChanges); } /// /// Event handler for when a revision has been created or updated. It's not called 100% of the time when a user /// publishes a new revision. /// /// New collab info. /// New revision id. /// Action that occured. void OnRevisionUpdated(CollabInfo info, string rev, string action) { // Invalidate the cache. m_HistoryEntriesCache = null; m_HistoryEntryCache = null; m_HistoryEntryCountCache = null; // Send update event. UpdatedHistoryEntries?.Invoke(); OnCollabInfoChanged(info); } void OnCollabInfoChanged(CollabInfo info) { // Update conflict state. if (m_ConflictCachedState != info.conflict) { m_ConflictCachedState = info.conflict; UpdatedConflictState?.Invoke(info.conflict); } // Update revisions available state. if (m_RemoteRevisionsAvailableState != info.update) { m_RemoteRevisionsAvailableState = info.update; UpdatedRemoteRevisionsAvailability?.Invoke(info.update); } // Update history list if the tip has changed. if (m_TipCache != info.tip) { m_TipCache = info.tip; // Invalidate the cache. m_HistoryEntriesCache = null; m_HistoryEntryCache = null; m_HistoryEntryCountCache = null; // Send update event. UpdatedHistoryEntries?.Invoke(); } // Update project state UpdateProjectStatus(info, UnityConnect.instance.connectInfo, UnityConnect.instance.projectInfo); // Update progress state. if (info.inProgress) { // Get progress info. var progressInfo = instance.GetJobProgress(0); Assert.IsNotNull(progressInfo); // Trigger start operation if not already known. if (m_ProgressInfo == null) { UpdatedOperationStatus?.Invoke(true); } // Send progress info. m_ProgressInfo = ProgressInfoFromCollab(progressInfo); UpdatedOperationProgress?.Invoke(m_ProgressInfo); } else if (m_ProgressInfo != null) { // Signal end of job if job still exists m_ProgressInfo = null; UpdatedOperationStatus?.Invoke(false); } } void OnJobsCompleted(CollabInfo info) { // NOTE: The first start of collab sends a completion event with no prior progress info. // To handle this, skip sending completion event if there has been no start event. if (m_ProgressInfo == null) return; Assert.IsFalse(info.inProgress); m_ProgressInfo = null; UpdatedOperationStatus?.Invoke(false); } void OnErrorOccurred(UnityErrorInfo error) { if (m_ErrorInfo?.Code == error.code) return; m_ErrorInfo = ErrorInfoFromUnity(error); ErrorOccurred?.Invoke(m_ErrorInfo); } void OnErrorCleared() { m_ErrorInfo = null; ErrorCleared?.Invoke(); } /// /// On receiving history result, remove the oldest request, send the received data, then make the next request. /// /// Result from the history request. void OnReceiveHistoryEntries(RevisionsResult revisionsResult) { Assert.AreNotEqual(0, m_HistoryRequests.Count, "There should be a history request."); var (offset, size, callback) = m_HistoryRequests.Dequeue(); // Get results, cache, then send them. var results = revisionsResult?.Revisions.Select(RevisionToHistoryEntry).ToList(); if (results != null) { m_HistoryEntries = results; m_HistoryEntriesCache = (offset, size); m_HistoryEntryCountCache = revisionsResult.RevisionsInRepo; callback(revisionsResult.RevisionsInRepo, m_HistoryEntries); } // Start the next request --> has to be outside of the callback. EditorApplication.delayCall += () => ConsumeHistoryQueue(); } /// /// Event handler for receiving unity connect project state changes. /// /// New project info. void OnUnityConnectProjectStateChanged(ProjectInfo projectInfo) { UpdateProjectStatus(instance.collabInfo, UnityConnect.instance.connectInfo, projectInfo); } /// /// Event handler for receiving collab state changes. /// /// New collab state. void OnCollabStateChanged(CollabInfo info) { OnCollabInfoChanged(info); } /// /// Event handler for receiving collab state changes. /// /// UnityConnect connect info. void OnUnityConnectStateChanged(ConnectInfo connectInfo) { UpdateProjectStatus(instance.collabInfo, connectInfo, UnityConnect.instance.projectInfo); } /// /// Update cached ready value and send event if it has changed. /// void UpdateProjectStatus(CollabInfo collabInfo, ConnectInfo connectInfo, ProjectInfo projectInfo) { var currentStatus = GetNewProjectStatus(collabInfo, connectInfo, projectInfo); if (m_ProjectStatus == currentStatus) return; m_ProjectStatus = currentStatus; UpdatedProjectStatus?.Invoke(m_ProjectStatus); } /// /// Returns the current project status. /// /// Current status of this project. static ProjectStatus GetNewProjectStatus(CollabInfo collabInfo, ConnectInfo connectInfo, ProjectInfo projectInfo) { // No UPID. if (!projectInfo.projectBound) { return ProjectStatus.Unbound; } if (!connectInfo.online) { return ProjectStatus.Offline; } if (connectInfo.maintenance || collabInfo.maintenance) { return ProjectStatus.Maintenance; } if (!connectInfo.loggedIn) { return ProjectStatus.LoggedOut; } if (!collabInfo.seat) { return ProjectStatus.NoSeat; } // UPID exists, but collab off. if (!instance.IsCollabEnabledForCurrentProject()) { return ProjectStatus.Bound; } // Waiting for collab to connect and be ready. if (!instance.IsConnected() || !collabInfo.ready) { return ProjectStatus.Loading; } return ProjectStatus.Ready; } /// /// Consume the next entry on the history queue. /// /// True if an entry was just inserted. Starts the consumption cycle. void ConsumeHistoryQueue(bool afterEnqueue = false) { // Start consuming the queue if the first entry was just enqueued. if (afterEnqueue && m_HistoryRequests.Count != 1) return; // Can't consume an empty queue. if (m_HistoryRequests.Count == 0) return; var (offset, size, callback) = m_HistoryRequests.Peek(); // Execute next request. Discard if exception. try { m_RevisionsService.GetRevisions(offset, size); } catch (Exception e) { Debug.LogException(e); // Remove request and send failure callback. m_HistoryRequests.Dequeue(); callback(null, null); } } /// /// Make a history request. /// /// Offset for the request to start from. /// Target length of the resultant list. /// Callback for the result. void QueueHistoryRequest(int offset, int size, Action> callback) { m_HistoryRequests.Enqueue((offset, size, callback)); ConsumeHistoryQueue(true); } /// /// Update cache of converted change entries from provided collab changes. /// /// Received list of changes from collab. void UpdateChanges(IEnumerable changes) { m_Changes.Clear(); m_Changes.AddRange(changes.Select(change => new ChangeEntry(change.path, change.path, ChangeEntryStatusFromCollabState(change.state), false, IsCollabStateFlagSet(change.state, CollabStates.kCollabConflicted | CollabStates.kCollabPendingMerge), change)) .Cast()); } /// /// Update cache of converted change entries from provided collab changes. /// /// Received list of changes from collab. void UpdateChanges(IEnumerable changes) { m_Changes.Clear(); m_Changes.AddRange(changes.Select(change => new ChangeEntry(change.Path, change.Path, ChangeEntryStatusFromCollabState(change.State), false, IsCollabStateFlagSet(change.State, CollabStates.kCollabConflicted | CollabStates.kCollabPendingMerge), change)) .Cast()); } /// public bool GetRemoteRevisionAvailability() { // Return cached value. return m_RemoteRevisionsAvailableState; } /// public bool GetConflictedState() { // Return cached value. return m_ConflictCachedState; } /// public IProgressInfo GetProgressState() { // Return cached value. return m_ProgressInfo; } /// public IErrorInfo GetErrorState() { return m_ErrorInfo; } /// public virtual ProjectStatus GetProjectStatus() { return m_ProjectStatus; } /// public void RequestChangeList(Action> callback) { var changes = instance.GetChangesToPublish_V2().changes; UpdateChanges(changes); callback(m_Changes); // Also check for errors. if (instance.GetError(UnityConnect.UnityErrorFilter.All, out var error) && (CollabErrorCode)error.code != CollabErrorCode.Collab_ErrNone) { ErrorOccurred?.Invoke(ErrorInfoFromUnity(error)); } } /// public void RequestPublish(string message, IReadOnlyList changeEntries = null) { var changeItems = changeEntries?.Select(EntryToChangeItem).ToArray(); instance.PublishAssetsAsync(message, changeItems); ChangeItem EntryToChangeItem(IChangeEntry entry) { return entry.Tag as ChangeItem; } } #endregion #region SourceControlHistoryCommands /// public event Action UpdatedHistoryEntries; /// public void RequestHistoryEntry(string revisionId, Action callback) { // Return cached entry if possible. if (m_HistoryEntryCache?.RevisionId == revisionId) { callback(m_HistoryEntryCache); return; } // Ensure that a cleanup occurs in the case of an exception. m_RevisionsService.FetchSingleRevisionCallback += OnFetchRevisionCallback; try { m_RevisionsService.GetRevision(revisionId); } catch (Exception e) { Debug.LogException(e); m_RevisionsService.FetchSingleRevisionCallback -= OnFetchRevisionCallback; callback(null); } void OnFetchRevisionCallback(Revision? revision) { m_RevisionsService.FetchSingleRevisionCallback -= OnFetchRevisionCallback; // Failing to find the revision can result in a null revision or an empty revisionID. callback(string.IsNullOrEmpty(revision?.revisionID) ? null : RevisionToHistoryEntry(revision.GetValueOrDefault())); } } /// public void RequestHistoryPage(int offset, int pageSize, Action> callback) { // Return cached entry is possible. if (m_HistoryEntriesCache?.offset == offset && m_HistoryEntriesCache?.size == pageSize) { callback(m_HistoryEntries); return; } // Queue up the request. QueueHistoryRequest(offset, pageSize, (_, r) => callback(r)); } /// public void RequestHistoryCount(Action callback) { // Return cached value if possible. if (m_HistoryEntryCountCache != null) { callback(m_HistoryEntryCountCache); return; } QueueHistoryRequest(0, 0, (c, _) => callback(c)); } /// public void RequestDiscard(IChangeEntry entry) { // Collab cannot revert a new file as it has nothing to go back to. So, instead we delete them. if (entry.Status == ChangeEntryStatus.Added) { File.Delete(entry.Path); // Notify ADB to refresh since a change has been made. AssetDatabase.Refresh(); } else { instance.RevertFile(entry.Path, true); } } /// public void RequestBulkDiscard(IReadOnlyList entries) { var revertEntries = new List(); var deleteOccured = false; foreach (var entry in entries) { // Collab cannot revert a new file as it has nothing to go back to. So, instead we delete them. if (entry.Status == ChangeEntryStatus.Added) { File.Delete(entry.Path); deleteOccured = true; } else { revertEntries.Add((ChangeItem)entry.Tag); } } // If a change has been made, notify the ADB to refresh. if (deleteOccured) { AssetDatabase.Refresh(); } instance.RevertFiles(revertEntries.ToArray(), true); } /// public void RequestDiffChanges(string path) { instance.ShowDifferences(path); } /// public bool SupportsRevert { get; } = false; /// public void RequestRevert(string revisionId, IReadOnlyList files) { throw new NotImplementedException(); } /// public void RequestUpdateTo(string revisionId) { instance.Update(revisionId, true); } /// public void RequestRestoreTo(string revisionId) { instance.ResyncToRevision(revisionId); } /// public void RequestGoBackTo(string revisionId) { instance.GoBackToRevision(revisionId, false); } /// public void ClearError() { instance.ClearErrors(); } /// public void RequestShowConflictedDifferences(string path) { if (UnityEditor.Collaboration.Collab.IsDiffToolsAvailable()) { instance.ShowConflictDifferences(path); } else { Debug.Log(StringAssets.noMergeToolIsConfigured); } } /// public void RequestChooseMerge(string path) { if (UnityEditor.Collaboration.Collab.IsDiffToolsAvailable()) { instance.LaunchConflictExternalMerge(path); } else { Debug.Log(StringAssets.noMergeToolIsConfigured); } } /// public void RequestChooseMine(string[] paths) { instance.SetConflictsResolvedMine(paths); } /// public void RequestChooseRemote(string[] paths) { instance.SetConflictsResolvedTheirs(paths); } /// public void RequestSync() { QueueHistoryRequest(0, 1, Callback); void Callback(int? count, IReadOnlyList revisions) { if (revisions != null && revisions.Count > 0) { instance.Update(revisions[0].RevisionId, true); } else { Debug.LogError("Remote revision id is unknown. Please try again."); } } } /// public void RequestCancelJob() { instance.CancelJob(0); } /// public virtual void ShowServicePage() { SettingsService.OpenProjectSettings("Project/Services/Collaborate"); } /// public void ShowLoginPage() { UnityConnect.instance.ShowLogin(); } /// public void ShowNoSeatPage() { var unityConnect = UnityConnect.instance; var env = unityConnect.GetEnvironment(); // Map environment to url - prod is special if (env == "production") env = ""; else env += "-"; var url = "https://" + env + k_KServiceUrl + "/orgs/" + unityConnect.GetOrganizationId() + "/projects/" + unityConnect.GetProjectName() + "/unity-teams/"; Application.OpenURL(url); } /// public async void RequestTurnOnService() { try { await RequestTurnOnServiceInternal(); } catch (Exception e) { Debug.LogException(e); } } protected async Task RequestTurnOnServiceInternal() { Assert.IsTrue(Threading.IsMainThread, "This must be run on the main thread."); // Fire up the update Genesis service flag request. var http = new HttpClientHandler { CookieContainer = new CookieContainer() }; var client = new HttpClient(http); var projectGuid = UnityConnect.instance.projectInfo.projectGUID; var accessToken = UnityConnect.instance.GetAccessToken(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.TryAddWithoutValidation("X-UNITY-VERSION", InternalEditorUtility.GetFullUnityVersion()); var fullUrl = $"{UnityConnect.instance.GetConfigurationURL(CloudConfigUrl.CloudCore)}/api/projects/{projectGuid}/service_flags"; const string json = @"{ ""service_flags"": { ""collab"" : true} }"; var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await PutAsync(client, fullUrl, content); // Success. if (response?.StatusCode == HttpStatusCode.OK) { SaveAssets(); TurnOnCollabInternal(); } // Error. else if (response?.StatusCode == HttpStatusCode.Forbidden) { ShowCredentialsError(); } else { ShowGeneralError(); } } protected virtual void SaveAssets() { instance.SaveAssets(); } protected virtual Task PutAsync(HttpClient client, string fullUrl, StringContent content) { return client.PutAsync(fullUrl, content); } protected virtual void TurnOnCollabInternal() { // enable the server from the client.. instance.SetCollabEnabledForCurrentProject(true); // persist by marking collab on in settings PlayerSettings.SetCloudServiceEnabled("Collab", true); } protected virtual void ShowCredentialsError() { // TODO - ahmad :- Show an Error UI. Debug.LogError("You need owner privilege to enable or disable collab."); } protected virtual void ShowGeneralError() { // TODO - ahmad :- Show an Error UI. Debug.LogError("cannot enable collab"); } #endregion #region Static Helper Methods /// /// Converts a Collab Revision to an IHistoryEntry. /// /// Revision to convert. /// Resultant IHistoryEntry IHistoryEntry RevisionToHistoryEntry(Revision revision) { var time = DateTimeOffset.FromUnixTimeSeconds((long)revision.timeStamp); var entries = revision.entries.Select(ChangeActionToChangeEntry).ToList(); var status = HistoryEntryStatus.Ahead; if (revision.isObtained) status = HistoryEntryStatus.Behind; if (revision.revisionID == m_RevisionsService.tipRevision) status = HistoryEntryStatus.Current; return new HistoryEntry(revision.revisionID, status, revision.author, revision.comment, time, entries); } /// /// Converts a Collab ChangeAction to an IChangeEntry. /// /// ChangeAction to convert. /// Resultant IChangeEntry static IChangeEntry ChangeActionToChangeEntry(ChangeAction action) { var unmerged = false; var status = ChangeEntryStatus.None; switch (action.action.ToLower()) { case "added": status = ChangeEntryStatus.Added; break; case "conflict": status = ChangeEntryStatus.Unmerged; unmerged = true; break; case "deleted": status = ChangeEntryStatus.Deleted; break; case "ignored": status = ChangeEntryStatus.Ignored; break; case "renamed": case "moved": status = ChangeEntryStatus.Renamed; break; case "updated": status = ChangeEntryStatus.Modified; break; default: Debug.LogError($"Unknown file status: {action.action}"); break; } return new ChangeEntry(action.path, status: status, unmerged: unmerged); } /// /// Converts a Collab CollabStates to an ChangeEntryStatus. /// Note that CollabStates is a bitwise flag, while /// ChangeEntryStatus is an enum, so ordering matters. /// /// ChangeAction to convert. /// Resultant ChangeEntryStatus static ChangeEntryStatus ChangeEntryStatusFromCollabState(CollabStates state) { if (IsCollabStateFlagSet(state, CollabStates.kCollabIgnored)) { return ChangeEntryStatus.Ignored; } if (IsCollabStateFlagSet(state, CollabStates.kCollabConflicted | CollabStates.kCollabPendingMerge)) { return ChangeEntryStatus.Unmerged; } if (IsCollabStateFlagSet(state, CollabStates.kCollabAddedLocal)) { return ChangeEntryStatus.Added; } if (IsCollabStateFlagSet(state, CollabStates.kCollabMovedLocal)) { return ChangeEntryStatus.Renamed; } if (IsCollabStateFlagSet(state, CollabStates.kCollabDeletedLocal)) { return ChangeEntryStatus.Deleted; } if (IsCollabStateFlagSet(state, CollabStates.kCollabCheckedOutLocal)) { return ChangeEntryStatus.Modified; } return ChangeEntryStatus.Unknown; } /// /// Checks the state of a flag in CollabStates. /// /// State to check from. /// Flag to check in the state. /// True if flag is set. static bool IsCollabStateFlagSet(CollabStates state, CollabStates flag) { return (state & flag) != 0; } static IProgressInfo ProgressInfoFromCollab([CanBeNull] ProgressInfo collabProgress) { if (collabProgress == null) return null; return new Structures.ProgressInfo( collabProgress.title, collabProgress.extraInfo, collabProgress.currentCount, collabProgress.totalCount, collabProgress.lastErrorString, collabProgress.lastError, collabProgress.canCancel, collabProgress.isProgressTypePercent, collabProgress.percentComplete); } static IErrorInfo ErrorInfoFromUnity(UnityErrorInfo error) { return new ErrorInfo( error.code, error.priority, error.behaviour, error.msg, error.shortMsg, error.codeStr); } #endregion enum CollabErrorCode { Collab_ErrNone = 0, Collab_Error, Collab_ErrProjectNotLinked, Collab_ErrNoSuchRepository, Collab_ErrNotLoggedIn, Collab_ErrNotConnected, Collab_ErrLocalCache, Collab_ErrNotUpToDate, Collab_ErrCannotGetRevision, Collab_ErrCannotGetRemote, Collab_ErrCannotGetLocal, Collab_ErrInvalidHost, Collab_ErrInvalidPort, Collab_ErrInvalidRevision, Collab_ErrNotSnapshot, Collab_ErrNoSuchRemoteFile, Collab_ErrNoSuchLocalFile, Collab_ErrJobNotDefined, Collab_ErrJobAlreadyRunning, Collab_ErrAlreadyUpToDate, Collab_ErrJobNotRunning, Collab_ErrNotSupported, Collab_ErrJobCancelled, Collab_ErrCannotSubmitChanges, Collab_ErrMD5DoesNotMatch, Collab_ErrRemoteChanged, Collab_ErrCannotCreateTempDir, Collab_ErrCannotDownloadEntry, Collab_ErrCannotCreatePath, Collab_ErrCannotCreateFile, Collab_ErrCannotCopyFile, Collab_ErrCannotMoveFile, Collab_ErrCannotDeleteFile, Collab_ErrCannotGetProjects, Collab_ErrCannotRestoreSnapshot, Collab_ErrFileWasAddedLocally, Collab_ErrFileIsModified, Collab_ErrFileIsMissing, Collab_ErrFileAlreadyExists, Collab_ErrAutomaticMergeBaseIsMissing, Collab_ErrSmartMergeConflicts, Collab_ErrTextMergeConflicts, Collab_ErrAutomaticMerge, Collab_ErrSmartMerge, Collab_ErrTextMerge, Collab_ErrExternalDiff, Collab_ErrExternalMerge, Collab_ErrParseJson, Collab_ErrWrongSerializationMode, Collab_ErrNoDiffRevisions, Collab_ErrWorkspaceChanged, Collab_ErrRefreshChannelAccess, Collab_ErrUpdateInProgress, Collab_ErrSoftLocksJobRunning, Collab_ErrCannotGetSoftLocks, Collab_ErrPostSoftLocks, Collab_ErrRequestCancelled, Collab_ErrCollabInErrorState, Collab_ErrUsageExceeded, Collab_ErrRepositoryLocked, Collab_ErrJobWaitingForSubTasks, Collab_ErrBadRequest = 400, Collab_ErrNotAuthorized = 401, Collab_ErrInternalServerError = 500, Collab_ErrBadGateway = 502, Collab_ErrServerUnavailable = 503, Collab_ErrSmartMergeSetConflictState, Collab_ErrTextMergeSetConflictState, Collab_ErrExternalMergeSetConflictState, Collab_ErrNoDiffMergeToolsConfigured, Collab_ErrUnsupportedDiffMergeToolConfigured, Collab_ErrNoSeat, Collab_ErrNoSeatHidden } } }