Commit 1c847220 authored by jang dong hyeok's avatar jang dong hyeok
Browse files

.

parent 076f0c68
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Api;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.UserInterface;
using Unity.Cloud.Collaborate.Utilities;
using UnityEngine;
using UnityEngine.Assertions;
namespace Unity.Cloud.Collaborate.Models
{
internal class ChangesModel : IChangesModel
{
protected readonly ISourceControlProvider m_Provider;
/// <inheritdoc />
public event Action UpdatedChangeList;
/// <inheritdoc />
public event Action OnUpdatedSelectedChanges;
/// <inheritdoc />
public event Action<bool> BusyStatusUpdated;
/// <inheritdoc />
public event Action StateChanged;
internal Dictionary<string, IChangeEntryData> entryData;
internal Dictionary<string, bool> toggledEntries;
IReadOnlyList<IChangeEntryData> m_Conflicted;
readonly ChangeEntryData m_AllItem;
readonly HashSet<string> m_Requests;
const string k_RequestNewList = "request-new-list";
/// <inheritdoc />
public string SavedRevisionSummary { get; set; }
/// <inheritdoc />
public string SavedSearchQuery { get; set; }
/// <inheritdoc />
public int ToggledCount { get; private set; }
/// <inheritdoc />
public int TotalCount { get; private set; }
/// <inheritdoc />
public int ConflictedCount => m_Conflicted.Count;
/// <inheritdoc />
public bool Conflicted => m_Provider.GetConflictedState();
/// <inheritdoc />
public bool Busy => m_Requests.Count != 0;
public ChangesModel(ISourceControlProvider provider)
{
m_Provider = provider;
m_Requests = new HashSet<string>();
m_AllItem = new ChangeEntryData { Entry = new ChangeEntry(string.Empty), All = true };
entryData = new Dictionary<string, IChangeEntryData>();
m_Conflicted = new List<IChangeEntryData>();
toggledEntries = new Dictionary<string, bool>();
SavedSearchQuery = string.Empty;
SavedRevisionSummary = string.Empty;
}
/// <inheritdoc />
public void OnStart()
{
// Setup events.
m_Provider.UpdatedChangeList += OnUpdatedChangeList;
m_Provider.UpdatedSelectedChangeList += OnUpdatedSelectedChangesList;
}
/// <inheritdoc />
public void OnStop()
{
// Clean up.
m_Provider.UpdatedChangeList -= OnUpdatedChangeList;
m_Provider.UpdatedSelectedChangeList -= OnUpdatedSelectedChangesList;
}
/// <inheritdoc />
public void RestoreState(IWindowCache cache)
{
// Populate data from cache.
SavedRevisionSummary = cache.RevisionSummary;
SavedSearchQuery = cache.ChangesSearchValue;
toggledEntries = cache.SimpleSelectedItems ?? new Dictionary<string, bool>();
StateChanged?.Invoke();
}
/// <inheritdoc />
public void SaveState(IWindowCache cache)
{
// Save data.
cache.RevisionSummary = SavedRevisionSummary;
cache.ChangesSearchValue = SavedSearchQuery;
cache.SimpleSelectedItems = new SelectedItemsDictionary(toggledEntries);
}
/// <summary>
/// Event handler for when the source control provider receives an updated history list.
/// </summary>
void OnUpdatedChangeList()
{
// Only one request at a time.
if (!AddRequest(k_RequestNewList)) return;
m_Provider.RequestChangeList(OnReceivedChangeList);
}
void OnUpdatedSelectedChangesList(IReadOnlyList<string> list)
{
ToggleAllEntries(false);
foreach (var path in list)
{
UpdateEntryToggle(path, true);
}
OnUpdatedSelectedChanges?.Invoke();
}
/// <summary>
/// Event handler to receive changes from the provider.
/// </summary>
/// <param name="list">Change list received.</param>
void OnReceivedChangeList([CanBeNull] IReadOnlyList<IChangeEntry> list)
{
if (list != null)
{
UpdateChangeList(list);
UpdatedChangeList?.Invoke();
}
else
{
Debug.LogError("Failed to fetch latest change list.");
}
RemoveRequest(k_RequestNewList);
}
/// <summary>
/// Convert and cache new list of changes.
/// </summary>
/// <param name="list">New list of changes.</param>
internal virtual void UpdateChangeList([NotNull] IReadOnlyList<IChangeEntry> list)
{
TotalCount = list.Count;
// Create a new set of containers.
var newEntryData = new Dictionary<string, IChangeEntryData> { [string.Empty] = m_AllItem };
var newToggledEntries = new Dictionary<string, bool>();
var conflicted = new List<IChangeEntryData>();
var all = m_AllItem.Toggled;
var toggledCount = 0;
foreach (var entry in list)
{
// Transfer toggled state from old lookup into new.
toggledEntries.TryGetValue(entry.Path, out var toggled);
toggled = toggled || all || entry.Staged;
newToggledEntries[entry.Path] = toggled;
// Create a new data item for the entry.
var item = new ChangeEntryData { Entry = entry, Toggled = toggled };
newEntryData.Add(entry.Path, item);
// Update counts.
if (toggled)
{
toggledCount++;
}
if (entry.Unmerged)
{
conflicted.Add(item);
}
}
// Store the new containers.
entryData = newEntryData;
toggledEntries = newToggledEntries;
ToggledCount = toggledCount;
m_Conflicted = conflicted;
UpdateAllItemToggle();
}
/// <inheritdoc />
public virtual bool UpdateEntryToggle(string path, bool toggled)
{
var entry = (ChangeEntryData)entryData[path];
// Toggle all items if needed.
if (entry.All)
{
return ToggleAllEntries(toggled);
}
// Update the toggled count.
if (entry.Toggled && !toggled)
{
ToggledCount--;
}
else if (!entry.Toggled && toggled)
{
ToggledCount++;
}
// Store the value in the dictionary and data item.
toggledEntries[entry.Entry.Path] = toggled;
entry.Toggled = toggled;
// Update the "All" option if needed.
return UpdateAllItemToggle();
}
/// <inheritdoc />
public IReadOnlyList<IChangeEntryData> GetToggledEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => !e.All && e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList();
}
/// <inheritdoc />
public IReadOnlyList<IChangeEntryData> GetUntoggledEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => !e.All && !e.Toggled && e.Entry.Path.ToLower().Contains(query)).ToList();
}
/// <inheritdoc />
public IReadOnlyList<IChangeEntryData> GetAllEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => e.Entry.Path.ToLower().Contains(query)).ToList();
}
/// <inheritdoc />
public IReadOnlyList<IChangeEntryData> GetConflictedEntries(string query = null)
{
// Filter items by search query
query = StringUtility.TrimAndToLower(query);
return entryData.Values.Where(e => !e.All && e.Conflicted && e.Entry.Path.ToLower().Contains(query))
.ToList();
}
/// <summary>
/// Update the state of the "All" entry. If all entries are toggled, then "All" should be toggled too;
/// otherwise, "All" should be untoggled.
/// </summary>
/// <returns>True if the "All" entry was modified.</returns>
bool UpdateAllItemToggle()
{
// Update state of the "All" option
var allItemToggled = m_AllItem.Toggled;
if (entryData.Count == 0) return false;
if (ToggledCount == entryData.Count - 1)
{
// If every entry is toggled, then set AllItem as toggled.
toggledEntries[m_AllItem.Entry.Path] = true;
m_AllItem.Toggled = true;
return !allItemToggled;
}
// Otherwise, set AllItem as not toggled.
toggledEntries[m_AllItem.Entry.Path] = false;
m_AllItem.Toggled = false;
return allItemToggled;
}
/// <summary>
/// Toggle on or off all entries in the list.
/// </summary>
/// <param name="toggled">Whether to toggle off or on.</param>
/// <returns>True if the list has been modified.</returns>
bool ToggleAllEntries(bool toggled)
{
// Update all values in the dictionary.
toggledEntries.Keys.ToList().ForEach(x => toggledEntries[x] = toggled);
// Compute the number of toggled items (excluding the single All).
if (toggled)
{
ToggledCount = entryData.Count - 1;
}
else
{
ToggledCount = 0;
}
// Update all values in the list.
foreach (var kv in entryData)
{
((ChangeEntryData)kv.Value).Toggled = toggled;
}
return true;
}
/// <summary>
/// Add a started request.
/// </summary>
/// <param name="requestId">Id of the request to add.</param>
/// <returns>False if the request already exists.</returns>
bool AddRequest(string requestId)
{
if (m_Requests.Contains(requestId)) return false;
m_Requests.Add(requestId);
// Signal background activity if this is the only thing running.
if (m_Requests.Count == 1)
BusyStatusUpdated?.Invoke(true);
return true;
}
/// <summary>
/// Remove a finished request.
/// </summary>
/// <param name="requestId">Id of the request to remove.</param>
void RemoveRequest(string requestId)
{
Assert.IsTrue(m_Requests.Contains(requestId), $"Expects request to have first been made for it to have been finished: {requestId}");
m_Requests.Remove(requestId);
// Signal no background activity if no requests in progress
if (m_Requests.Count == 0)
BusyStatusUpdated?.Invoke(false);
}
/// <inheritdoc />
public void RequestInitialData()
{
// Only one request at a time.
if (!AddRequest(k_RequestNewList)) return;
m_Provider.RequestChangeList(OnReceivedChangeList);
}
/// <inheritdoc />
public void RequestDiffChanges(string path)
{
m_Provider.RequestDiffChanges(path);
}
/// <inheritdoc />
public void RequestDiscard(IChangeEntry entry)
{
m_Provider.RequestDiscard(entry);
}
/// <inheritdoc />
public void RequestBulkDiscard(IReadOnlyList<IChangeEntry> entries)
{
m_Provider.RequestBulkDiscard(entries);
}
/// <inheritdoc />
public void RequestPublish(string message, IReadOnlyList<IChangeEntry> changes)
{
m_Provider.RequestPublish(message, changes);
}
/// <inheritdoc />
public void RequestShowConflictedDifferences(string path)
{
m_Provider.RequestShowConflictedDifferences(path);
}
/// <inheritdoc />
public void RequestChooseMerge(string path)
{
m_Provider.RequestChooseMerge(path);
}
/// <inheritdoc />
public void RequestChooseMine(string[] paths)
{
m_Provider.RequestChooseMine(paths);
}
/// <inheritdoc />
public void RequestChooseRemote(string[] paths)
{
m_Provider.RequestChooseRemote(paths);
}
/// <summary>
/// Implementation of IChangeEntryData with each field given a setter so that the data can be updated.
/// </summary>
class ChangeEntryData : IChangeEntryData
{
/// <inheritdoc />
public IChangeEntry Entry { get; set; }
/// <inheritdoc />
public bool Toggled { get; set; }
/// <inheritdoc />
public bool All { get; set; }
/// <inheritdoc />
public bool ToggleReadOnly => Entry.Staged;
/// <inheritdoc />
public bool Conflicted => Entry.Unmerged;
}
}
}
namespace Unity.Cloud.Collaborate.Models.Enums
{
internal enum ProjectStatus
{
Unbound,
Offline,
Maintenance,
LoggedOut,
NoSeat,
Bound,
Loading,
Ready
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Api;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEngine;
using UnityEngine.Assertions;
namespace Unity.Cloud.Collaborate.Models
{
internal class HistoryModel : IHistoryModel
{
[NotNull]
readonly ISourceControlProvider m_Provider;
[NotNull]
readonly HashSet<string> m_Requests;
const string k_RequestPage = "request-page";
const string k_RequestEntry = "request-entry";
const string k_RequestEntryNumber = "request-entry-number";
/// <inheritdoc />
public event Action HistoryListUpdated;
/// <inheritdoc />
public event Action<IReadOnlyList<IHistoryEntry>> HistoryListReceived;
/// <inheritdoc />
public event Action<IHistoryEntry> SelectedRevisionReceived;
/// <inheritdoc />
public event Action<bool> BusyStatusUpdated;
/// <inheritdoc />
public event Action<int?> EntryCountUpdated;
/// <inheritdoc />
public event Action StateChanged;
public HistoryModel([NotNull] ISourceControlProvider provider)
{
m_Provider = provider;
m_Requests = new HashSet<string>();
SelectedRevisionId = string.Empty;
SavedRevisionId = string.Empty;
}
/// <inheritdoc />
public void OnStart()
{
// Setup events
m_Provider.UpdatedHistoryEntries += OnUpdatedHistoryEntries;
}
/// <inheritdoc />
public void OnStop()
{
// Clean up.
m_Provider.UpdatedHistoryEntries -= OnUpdatedHistoryEntries;
}
/// <inheritdoc />
public void RestoreState(IWindowCache cache)
{
// Populate data.
PageNumber = cache.HistoryPageNumber;
SavedRevisionId = cache.SelectedHistoryRevision;
StateChanged?.Invoke();
}
/// <inheritdoc />
public void SaveState(IWindowCache cache)
{
// Update cache.
cache.HistoryPageNumber = PageNumber;
cache.SelectedHistoryRevision = SelectedRevisionId;
}
/// <summary>
/// Event handler for when the requested history entry count has been received.
/// </summary>
/// <param name="entryCount">Received entry count.</param>
void OnReceivedHistoryEntryCount(int? entryCount)
{
RemoveRequest(k_RequestEntryNumber);
EntryCountUpdated?.Invoke(entryCount);
}
/// <summary>
/// Event handler for when the requested page of history entries has been received.
/// </summary>
/// <param name="list">Received list of entries.</param>
void OnReceivedHistoryPage(IReadOnlyList<IHistoryEntry> list)
{
RemoveRequest(k_RequestPage);
HistoryListReceived?.Invoke(list);
}
/// <summary>
/// Event handler for when a requested single history entry has been received.
/// </summary>
/// <param name="entry">Received entry.</param>
void OnReceivedHistoryEntry(IHistoryEntry entry)
{
RemoveRequest(k_RequestEntry);
SelectedRevisionReceived?.Invoke(entry);
}
/// <summary>
/// Event handler for when the provider has received an updated history list.
/// </summary>
void OnUpdatedHistoryEntries()
{
HistoryListUpdated?.Invoke();
}
/// <inheritdoc />
public void RequestPageOfRevisions(int pageSize)
{
// Only one request at a time.
if (!AddRequest(k_RequestPage)) return;
SelectedRevisionId = string.Empty;
m_Provider.RequestHistoryPage(PageNumber * pageSize, pageSize, OnReceivedHistoryPage);
}
/// <inheritdoc />
public void RequestSingleRevision(string revisionId)
{
// Only one request at a time.
if (!AddRequest(k_RequestEntry)) return;
SavedRevisionId = string.Empty;
SelectedRevisionId = revisionId;
m_Provider.RequestHistoryEntry(revisionId, OnReceivedHistoryEntry);
}
/// <inheritdoc />
public void RequestEntryNumber()
{
// Only one request at a time.
if (!AddRequest(k_RequestEntryNumber)) return;
m_Provider.RequestHistoryCount(OnReceivedHistoryEntryCount);
}
/// <inheritdoc />
public void RequestUpdateTo(string revisionId)
{
m_Provider.RequestUpdateTo(revisionId);
}
/// <inheritdoc />
public void RequestRestoreTo(string revisionId)
{
m_Provider.RequestRestoreTo(revisionId);
}
/// <inheritdoc />
public void RequestGoBackTo(string revisionId)
{
m_Provider.RequestGoBackTo(revisionId);
}
/// <inheritdoc />
public bool SupportsRevert => m_Provider.SupportsRevert;
/// <inheritdoc />
public void RequestRevert(string revisionId, IReadOnlyList<string> files)
{
m_Provider.RequestRevert(revisionId, files);
}
/// <summary>
/// Add a started request.
/// </summary>
/// <param name="requestId">Id of the request to add.</param>
/// <returns>False if the request already exists.</returns>
bool AddRequest([NotNull] string requestId)
{
if (m_Requests.Contains(requestId)) return false;
m_Requests.Add(requestId);
// Signal background activity if this is the only thing running.
if (m_Requests.Count == 1)
BusyStatusUpdated?.Invoke(true);
return true;
}
/// <summary>
/// Remove a finished request.
/// </summary>
/// <param name="requestId">Id of the request to remove.</param>
void RemoveRequest([NotNull] string requestId)
{
Assert.IsTrue(m_Requests.Contains(requestId), $"Expects request to have first been made for it to have been finished: {requestId}");
m_Requests.Remove(requestId);
// Signal no background activity if no requests in progress
if (m_Requests.Count == 0)
BusyStatusUpdated?.Invoke(false);
}
/// <inheritdoc />
public bool Busy => m_Requests.Count != 0;
/// <inheritdoc />
public int PageNumber { get; set; }
/// <inheritdoc />
public string SelectedRevisionId { get; private set; }
/// <inheritdoc />
public string SavedRevisionId { get; private set; }
/// <inheritdoc />
public bool IsRevisionSelected => !string.IsNullOrEmpty(SelectedRevisionId);
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Structures;
namespace Unity.Cloud.Collaborate.Models
{
internal interface IChangesModel : IModel
{
/// <summary>
/// Event triggered when an updated change list is available.
/// </summary>
event Action UpdatedChangeList;
/// <summary>
/// Event triggered when an updated selection of change list is available.
/// </summary>
event Action OnUpdatedSelectedChanges;
/// <summary>
/// Event triggered when the busy status changes.
/// </summary>
event Action<bool> BusyStatusUpdated;
/// <summary>
/// Stored revision summary.
/// </summary>
[NotNull]
string SavedRevisionSummary { get; set; }
/// <summary>
/// Stored search query.
/// </summary>
[NotNull]
string SavedSearchQuery { get; set; }
/// <summary>
/// Number of toggled entries.
/// </summary>
int ToggledCount { get; }
/// <summary>
/// Total number of entries.
/// </summary>
int TotalCount { get; }
/// <summary>
/// Number of conflicted entries.
/// </summary>
int ConflictedCount { get; }
/// <summary>
/// Whether or not conflicts exist.
/// </summary>
bool Conflicted { get; }
/// <summary>
/// Whether or not the model is busy with a request.
/// </summary>
bool Busy { get; }
/// <summary>
/// Request initial data to populate the change list.
/// </summary>
void RequestInitialData();
/// <summary>
/// Set the value of the toggle for the given path.
/// </summary>
/// <param name="path">Path to modify the toggle for.</param>
/// <param name="toggled">Value to set the toggle to.</param>
/// <returns>True if more than one entry has had its value change.</returns>
bool UpdateEntryToggle([NotNull] string path, bool toggled);
/// <summary>
/// Get the list of toggled entries. Can be long running in the case of a large change list.
/// </summary>
/// <param name="query">Query to filter the entries via.</param>
/// <returns>The filtered toggled list.</returns>
[NotNull]
IReadOnlyList<IChangeEntryData> GetToggledEntries([CanBeNull] string query = null);
/// <summary>
/// Get the list of untoggled entries. Can be long running in the case of a large change list.
/// </summary>
/// <param name="query">Query to filter the entries via.</param>
/// <returns>The filtered untoggled list.</returns>
[NotNull]
IReadOnlyList<IChangeEntryData> GetUntoggledEntries([CanBeNull] string query = null);
/// <summary>
/// Get full list of changes. Can be long running in the case of a large change list.
/// </summary>
/// <param name="query">Query to filter the changes with</param>
/// <returns>The filtered change list.</returns>
[NotNull]
IReadOnlyList<IChangeEntryData> GetAllEntries([CanBeNull] string query = null);
/// <summary>
/// Get the list of conflicted entries. Can be long running in the case of a large change list.
/// </summary>
/// <param name="query">Query to filter the entries via.</param>
/// <returns>The filtered conflicted list.</returns>
[NotNull]
IReadOnlyList<IChangeEntryData> GetConflictedEntries([CanBeNull] string query = null);
/// <summary>
/// Request diff of the file at the given path.
/// </summary>
/// <param name="path">Path to file to diff.</param>
void RequestDiffChanges([NotNull] string path);
/// <summary>
/// Request discard of the file at the given path.
/// </summary>
/// <param name="entry">Entry to discard.</param>
void RequestDiscard([NotNull] IChangeEntry entry);
/// <summary>
/// Request discard of the given list of files.
/// </summary>
/// <param name="entries">List of entries to discard.</param>
void RequestBulkDiscard([NotNull] IReadOnlyList<IChangeEntry> entries);
/// <summary>
/// Request publish with the given message and list of files.
/// </summary>
/// <param name="message">Message for the revision.</param>
/// <param name="changes">Changes to publish.</param>
void RequestPublish([NotNull] string message, [NotNull] IReadOnlyList<IChangeEntry> changes);
/// <summary>
/// Show the difference between both version of a conflicted file.
/// </summary>
/// <param name="path">Path of the file to show.</param>
void RequestShowConflictedDifferences([NotNull] string path);
/// <summary>
/// Request to choose merge for the provided conflict.
/// </summary>
/// <param name="path">Path of the file to choose merge for.</param>
void RequestChooseMerge([NotNull] string path);
/// <summary>
/// Request to choose mine for the provided conflict.
/// </summary>
/// <param name="paths">Paths of the files to choose mine for.</param>
void RequestChooseMine([NotNull] string[] paths);
/// <summary>
/// Request to choose remote for the provided conflict.
/// </summary>
/// <param name="paths">Paths of the files to choose remote for.</param>
void RequestChooseRemote([NotNull] string[] paths);
}
}
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Structures;
namespace Unity.Cloud.Collaborate.Models
{
internal interface IHistoryModel : IModel
{
/// <summary>
/// Event triggered when the history list has been updated.
/// </summary>
event Action HistoryListUpdated;
/// <summary>
/// Event triggered when the requested page of revisions is received.
/// </summary>
event Action<IReadOnlyList<IHistoryEntry>> HistoryListReceived;
/// <summary>
/// Event triggered when the requested revision is received.
/// </summary>
event Action<IHistoryEntry> SelectedRevisionReceived;
/// <summary>
/// Event triggered when the busy status changes.
/// </summary>
event Action<bool> BusyStatusUpdated;
/// <summary>
/// Event triggered when the requested entry count is received.
/// </summary>
event Action<int?> EntryCountUpdated;
/// <summary>
/// Whether or not the model is busy with a request.
/// </summary>
bool Busy { get; }
/// <summary>
/// Current page number.
/// </summary>
int PageNumber { get; set; }
/// <summary>
/// Currently selected revision id.
/// </summary>
[NotNull]
string SelectedRevisionId { get; }
/// <summary>
/// Revision saved before domain reload.
/// </summary>
[NotNull]
string SavedRevisionId { get; }
/// <summary>
/// True if a revision is currently selected.
/// </summary>
bool IsRevisionSelected { get; }
/// <summary>
/// Request the current page of given size. Result returns via the HistoryListReceived event.
/// </summary>
/// <param name="pageSize"></param>
void RequestPageOfRevisions(int pageSize);
/// <summary>
/// Request the revision with the given id. Result returned via the SelectedRevisionReceived event.
/// </summary>
/// <param name="revisionId"></param>
void RequestSingleRevision([NotNull] string revisionId);
/// <summary>
/// Request the count of entries. Result returned via the EntryCountUpdated event.
/// </summary>
void RequestEntryNumber();
/// <summary>
/// Request to update the state of the project to a new provided revision.
/// </summary>
/// <param name="revisionId">New revision id of the project to go to.</param>
void RequestUpdateTo([NotNull] string revisionId);
/// <summary>
/// Request to take the state of the project back to the given (and current) revision.
/// </summary>
/// <param name="revisionId">Current revision id of the project to go back to.</param>
void RequestRestoreTo([NotNull] string revisionId);
/// <summary>
/// Request to take the state of the project back to the given revision, but do not change the current revision or history.
/// </summary>
/// <param name="revisionId">Revision id to go back to.</param>
void RequestGoBackTo([NotNull] string revisionId);
/// <summary>
/// Returns true if revert is supported.
/// </summary>
bool SupportsRevert { get; }
/// <summary>
/// Request to revert the specified files to the given revision.
/// </summary>
/// <param name="revisionId">Revision to revert the files back to.</param>
/// <param name="files">Files to revert back.</param>
void RequestRevert([NotNull] string revisionId, [NotNull] IReadOnlyList<string> files);
}
}
using System;
using Unity.Cloud.Collaborate.Models.Structures;
using JetBrains.Annotations;
namespace Unity.Cloud.Collaborate.Models
{
internal interface IMainModel : IModel
{
/// <summary>
/// Signal when the local state switches between conflicted or not.
/// </summary>
event Action<bool> ConflictStatusChange;
/// <summary>
/// Signal when an operation with progress has started or stopped.
/// </summary>
event Action<bool> OperationStatusChange;
/// <summary>
/// Signal with incremental details of the operation in progress.
/// </summary>
event Action<IProgressInfo> OperationProgressChange;
/// <summary>
/// Signal when an error has occurred.
/// </summary>
event Action<IErrorInfo> ErrorOccurred;
/// <summary>
/// Signal when the error has cleared.
/// </summary>
event Action ErrorCleared;
/// <summary>
/// Signal whether or not the there are remote revisions to be fetched.
/// </summary>
event Action<bool> RemoteRevisionsAvailabilityChange;
/// <summary>
/// Signal when the state of the back button is updated. For example: clearing it or showing a new one.
/// The string included is the new label for the back navigation button. If that value is null, clear the back
/// navigation.
/// </summary>
event Action<string> BackButtonStateUpdated;
/// <summary>
/// Returns true if there are remote revisions available.
/// </summary>
bool RemoteRevisionsAvailable { get; }
/// <summary>
/// Returns true if there's a conflict locally.
/// </summary>
bool Conflicted { get; }
/// <summary>
/// Returns progress info if there is any.
/// </summary>
[CanBeNull]
IProgressInfo ProgressInfo { get; }
/// <summary>
/// Returns error info if there is any.
/// </summary>
[CanBeNull]
IErrorInfo ErrorInfo { get; }
/// <summary>
/// Current tab index being displayed.
/// </summary>
int CurrentTabIndex { get; set; }
/// <summary>
/// Returns a history model.
/// </summary>
/// <returns>Singleton history model for this main model.</returns>
[NotNull]
IHistoryModel ConstructHistoryModel();
/// <summary>
/// Returns a Changes model.
/// </summary>
/// <returns>Singleton change model for this main model.</returns>
[NotNull]
IChangesModel ConstructChangesModel();
/// <summary>
/// Clears any set error.
/// </summary>
void ClearError();
/// <summary>
/// Sync to latest revision.
/// </summary>
void RequestSync();
/// <summary>
/// Request cancel current job.
/// </summary>
void RequestCancelJob();
/// <summary>
/// Returns the current back navigation. Null if none exists presently.
/// </summary>
/// <returns>Current back navigation id, text, and action.</returns>
(string id, string text, Action backAction)? GetBackNavigation();
/// <summary>
/// Register back navigation to be made available to the user to navigate backwards in the UI.
/// </summary>
/// <param name="id">Id for the back event.</param>
/// <param name="text">Text for the back label.</param>
/// <param name="backAction">Action to perform to go back.</param>
void RegisterBackNavigation(string id, string text, Action backAction);
/// <summary>
/// Unregister back navigation if the given id matches the currently displayed back navigation.
/// </summary>
/// <param name="id">Id for the back event.</param>
/// <returns>True if id matched.</returns>
bool UnregisterBackNavigation(string id);
}
}
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.UserInterface;
namespace Unity.Cloud.Collaborate.Models
{
internal interface IModel
{
/// <summary>
/// Inform the presenter that the state of the model has changed.
/// </summary>
event Action StateChanged;
/// <summary>
/// Called when the model is started and the model should setup events and fetch data.
/// </summary>
void OnStart();
/// <summary>
/// Called when the model should be stopped and data and events should closed.
/// </summary>
void OnStop();
/// <summary>
/// Restores the state of the model from the provide cache. Must be called after OnStart.
/// </summary>
/// <param name="cache">Cache to read the state from.</param>
void RestoreState([NotNull] IWindowCache cache);
/// <summary>
/// Saves the state of the model into the cache. Must be called before OnStop.
/// </summary>
/// <param name="cache">Cache to save the state into.</param>
void SaveState([NotNull] IWindowCache cache);
}
}
using System;
using Unity.Cloud.Collaborate.Models.Enums;
namespace Unity.Cloud.Collaborate.Models
{
internal interface IStartModel : IModel
{
/// <summary>
/// Event that is triggered when the project status changes.
/// </summary>
event Action<ProjectStatus> ProjectStatusChanged;
/// <summary>
/// Returns the current project status.
/// </summary>
ProjectStatus ProjectStatus { get; }
/// <summary>
/// Request to turn on the service.
/// </summary>
void RequestTurnOnService();
/// <summary>
/// Show the service page.
/// </summary>
void ShowServicePage();
/// <summary>
/// Show login page.
/// </summary>
void ShowLoginPage();
/// <summary>
/// Show no seat page.
/// </summary>
void ShowNoSeatPage();
}
}
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Api;
using Unity.Cloud.Collaborate.Models.Structures;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEngine.Assertions;
namespace Unity.Cloud.Collaborate.Models
{
internal class MainModel : IMainModel
{
[NotNull]
readonly ISourceControlProvider m_Provider;
/// <inheritdoc />
public event Action<bool> ConflictStatusChange;
/// <inheritdoc />
public event Action<bool> OperationStatusChange;
/// <inheritdoc />
public event Action<IProgressInfo> OperationProgressChange;
/// <inheritdoc />
public event Action<IErrorInfo> ErrorOccurred;
/// <inheritdoc />
public event Action ErrorCleared;
/// <inheritdoc />
public event Action<bool> RemoteRevisionsAvailabilityChange;
/// <inheritdoc />
public event Action<string> BackButtonStateUpdated;
/// <inheritdoc />
public event Action StateChanged;
[NotNull]
readonly IHistoryModel m_HistoryModel;
[NotNull]
readonly IChangesModel m_ChangesModel;
(string id, string text, Action backEvent)? m_BackNavigation;
public MainModel([NotNull] ISourceControlProvider provider)
{
m_Provider = provider;
m_HistoryModel = new HistoryModel(m_Provider);
m_ChangesModel = new ChangesModel(m_Provider);
}
/// <inheritdoc />
public void OnStart()
{
// Setup events
m_Provider.UpdatedOperationStatus += OnUpdatedOperationStatus;
m_Provider.UpdatedOperationProgress += OnUpdatedOperationProgress;
m_Provider.ErrorOccurred += OnErrorOccurred;
m_Provider.ErrorCleared += OnErrorCleared;
m_Provider.UpdatedConflictState += OnUpdatedConflictState;
m_Provider.UpdatedRemoteRevisionsAvailability += OnUpdatedRemoteRevisionsAvailability;
// Propagate event to "child" models.
m_HistoryModel.OnStart();
m_ChangesModel.OnStart();
}
/// <inheritdoc />
public void OnStop()
{
// Clean up.
m_Provider.UpdatedOperationStatus -= OnUpdatedOperationStatus;
m_Provider.UpdatedOperationProgress -= OnUpdatedOperationProgress;
m_Provider.ErrorOccurred -= OnErrorOccurred;
m_Provider.ErrorCleared -= OnErrorCleared;
m_Provider.UpdatedConflictState -= OnUpdatedConflictState;
m_Provider.UpdatedRemoteRevisionsAvailability -= OnUpdatedRemoteRevisionsAvailability;
// Propagate event to "child" models.
m_HistoryModel.OnStop();
m_ChangesModel.OnStop();
}
/// <inheritdoc />
public void RestoreState(IWindowCache cache)
{
// Read in cached data.
CurrentTabIndex = cache.TabIndex;
StateChanged?.Invoke();
// Propagate restore call to "child" models.
m_HistoryModel.RestoreState(cache);
m_ChangesModel.RestoreState(cache);
}
/// <inheritdoc />
public void SaveState(IWindowCache cache)
{
// Cache data.
cache.TabIndex = CurrentTabIndex;
// Propagate save call to "child" models.
m_HistoryModel.SaveState(cache);
m_ChangesModel.SaveState(cache);
}
/// <inheritdoc />
public bool RemoteRevisionsAvailable => m_Provider.GetRemoteRevisionAvailability();
/// <inheritdoc />
public bool Conflicted => m_Provider.GetConflictedState();
/// <inheritdoc />
public IProgressInfo ProgressInfo => m_Provider.GetProgressState();
/// <inheritdoc />
public IErrorInfo ErrorInfo => m_Provider.GetErrorState();
/// <inheritdoc />
public int CurrentTabIndex { get; set; }
/// <inheritdoc />
public IHistoryModel ConstructHistoryModel()
{
return m_HistoryModel;
}
/// <inheritdoc />
public IChangesModel ConstructChangesModel()
{
return m_ChangesModel;
}
/// <inheritdoc />
public void ClearError()
{
m_Provider.ClearError();
}
/// <inheritdoc />
public void RequestSync()
{
m_Provider.RequestSync();
}
/// <inheritdoc />
public void RequestCancelJob()
{
m_Provider.RequestCancelJob();
}
/// <inheritdoc />
public (string id, string text, Action backAction)? GetBackNavigation()
{
return m_BackNavigation;
}
/// <inheritdoc />
public void RegisterBackNavigation(string id, string text, Action backAction)
{
Assert.IsTrue(m_BackNavigation == null, "There should only be one back navigation registered at a time.");
m_BackNavigation = (id, text, backAction);
BackButtonStateUpdated?.Invoke(text);
}
/// <inheritdoc />
public bool UnregisterBackNavigation(string id)
{
if (m_BackNavigation?.id != id) return false;
m_BackNavigation = null;
BackButtonStateUpdated?.Invoke(null);
return true;
}
/// <summary>
/// Event handler for when the availability of remote revisions changes.
/// </summary>
/// <param name="available">New availability status.</param>
void OnUpdatedRemoteRevisionsAvailability(bool available)
{
RemoteRevisionsAvailabilityChange?.Invoke(available);
}
/// <summary>
/// Event handler for when the conflicted status changes.
/// </summary>
/// <param name="conflicted">New conflicted status.</param>
void OnUpdatedConflictState(bool conflicted)
{
ConflictStatusChange?.Invoke(conflicted);
}
void OnUpdatedOperationStatus(bool inProgress)
{
OperationStatusChange?.Invoke(inProgress);
}
void OnUpdatedOperationProgress(IProgressInfo progressInfo)
{
OperationProgressChange?.Invoke(progressInfo);
}
void OnErrorOccurred(IErrorInfo errorInfo)
{
ErrorOccurred?.Invoke(errorInfo);
}
void OnErrorCleared()
{
ErrorCleared?.Invoke();
}
}
}
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;
/// <inheritdoc />
public event Action UpdatedChangeList;
/// <inheritdoc />
public event Action<IReadOnlyList<string>> UpdatedSelectedChangeList;
/// <inheritdoc />
public event Action<bool> UpdatedConflictState;
/// <inheritdoc />
public event Action<bool> UpdatedRemoteRevisionsAvailability;
/// <inheritdoc />
public event Action<ProjectStatus> UpdatedProjectStatus;
/// <inheritdoc />
public event Action<bool> UpdatedOperationStatus;
/// <inheritdoc />
public event Action<IProgressInfo> UpdatedOperationProgress;
/// <inheritdoc />
public event Action<IErrorInfo> ErrorOccurred;
/// <inheritdoc />
public event Action ErrorCleared;
readonly List<IChangeEntry> m_Changes;
bool m_ConflictCachedState;
bool m_RemoteRevisionsAvailableState;
// History entry requesting bits and bobs.
readonly Queue<(int offset, int size, Action<int?, IReadOnlyList<IHistoryEntry>>)> m_HistoryRequests;
[NotNull]
IReadOnlyList<IHistoryEntry> 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<IChangeEntry>();
m_HistoryEntries = new List<IHistoryEntry>();
m_HistoryRequests = new Queue<(int offset, int size, Action<int?, IReadOnlyList<IHistoryEntry>>)>();
// 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();
}
/// <summary>
/// Setup events for the provider.
/// </summary>
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
/// <summary>
/// Event handler for when the change list has changed.
/// </summary>
/// <param name="changes">New change list.</param>
/// <param name="isFiltered">Whether or not the list is filtered. Should always be false.</param>
void OnChangeItemsChanged(ChangeItem[] changes, bool isFiltered)
{
UpdateChanges(changes);
UpdatedChangeList?.Invoke();
}
/// <summary>
/// WIP method to handle partial publish in collab.
/// </summary>
/// <param name="changes">Received changes.</param>
/// <param name="isFiltered">Whether or not it's a partial publish. Should always be true.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="info">New collab info.</param>
/// <param name="rev">New revision id.</param>
/// <param name="action">Action that occured.</param>
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();
}
/// <summary>
/// On receiving history result, remove the oldest request, send the received data, then make the next request.
/// </summary>
/// <param name="revisionsResult">Result from the history request.</param>
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();
}
/// <summary>
/// Event handler for receiving unity connect project state changes.
/// </summary>
/// <param name="projectInfo">New project info.</param>
void OnUnityConnectProjectStateChanged(ProjectInfo projectInfo)
{
UpdateProjectStatus(instance.collabInfo, UnityConnect.instance.connectInfo, projectInfo);
}
/// <summary>
/// Event handler for receiving collab state changes.
/// </summary>
/// <param name="info">New collab state.</param>
void OnCollabStateChanged(CollabInfo info)
{
OnCollabInfoChanged(info);
}
/// <summary>
/// Event handler for receiving collab state changes.
/// </summary>
/// <param name="connectInfo">UnityConnect connect info.</param>
void OnUnityConnectStateChanged(ConnectInfo connectInfo)
{
UpdateProjectStatus(instance.collabInfo, connectInfo, UnityConnect.instance.projectInfo);
}
/// <summary>
/// Update cached ready value and send event if it has changed.
/// </summary>
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);
}
/// <summary>
/// Returns the current project status.
/// </summary>
/// <returns>Current status of this project.</returns>
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;
}
/// <summary>
/// Consume the next entry on the history queue.
/// </summary>
/// <param name="afterEnqueue">True if an entry was just inserted. Starts the consumption cycle.</param>
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);
}
}
/// <summary>
/// Make a history request.
/// </summary>
/// <param name="offset">Offset for the request to start from.</param>
/// <param name="size">Target length of the resultant list.</param>
/// <param name="callback">Callback for the result.</param>
void QueueHistoryRequest(int offset, int size, Action<int?, IReadOnlyList<IHistoryEntry>> callback)
{
m_HistoryRequests.Enqueue((offset, size, callback));
ConsumeHistoryQueue(true);
}
/// <summary>
/// Update cache of converted change entries from provided collab changes.
/// </summary>
/// <param name="changes">Received list of changes from collab.</param>
void UpdateChanges(IEnumerable<Change> 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<IChangeEntry>());
}
/// <summary>
/// Update cache of converted change entries from provided collab changes.
/// </summary>
/// <param name="changes">Received list of changes from collab.</param>
void UpdateChanges(IEnumerable<ChangeItem> 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<IChangeEntry>());
}
/// <inheritdoc />
public bool GetRemoteRevisionAvailability()
{
// Return cached value.
return m_RemoteRevisionsAvailableState;
}
/// <inheritdoc />
public bool GetConflictedState()
{
// Return cached value.
return m_ConflictCachedState;
}
/// <inheritdoc />
public IProgressInfo GetProgressState()
{
// Return cached value.
return m_ProgressInfo;
}
/// <inheritdoc />
public IErrorInfo GetErrorState()
{
return m_ErrorInfo;
}
/// <inheritdoc />
public virtual ProjectStatus GetProjectStatus()
{
return m_ProjectStatus;
}
/// <inheritdoc />
public void RequestChangeList(Action<IReadOnlyList<IChangeEntry>> 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));
}
}
/// <inheritdoc />
public void RequestPublish(string message, IReadOnlyList<IChangeEntry> changeEntries = null)
{
var changeItems = changeEntries?.Select(EntryToChangeItem).ToArray();
instance.PublishAssetsAsync(message, changeItems);
ChangeItem EntryToChangeItem(IChangeEntry entry)
{
return entry.Tag as ChangeItem;
}
}
#endregion
#region SourceControlHistoryCommands
/// <inheritdoc />
public event Action UpdatedHistoryEntries;
/// <inheritdoc />
public void RequestHistoryEntry(string revisionId, Action<IHistoryEntry> 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()));
}
}
/// <inheritdoc />
public void RequestHistoryPage(int offset, int pageSize, Action<IReadOnlyList<IHistoryEntry>> 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));
}
/// <inheritdoc />
public void RequestHistoryCount(Action<int?> callback)
{
// Return cached value if possible.
if (m_HistoryEntryCountCache != null)
{
callback(m_HistoryEntryCountCache);
return;
}
QueueHistoryRequest(0, 0, (c, _) => callback(c));
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
public void RequestBulkDiscard(IReadOnlyList<IChangeEntry> entries)
{
var revertEntries = new List<ChangeItem>();
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);
}
/// <inheritdoc />
public void RequestDiffChanges(string path)
{
instance.ShowDifferences(path);
}
/// <inheritdoc />
public bool SupportsRevert { get; } = false;
/// <inheritdoc />
public void RequestRevert(string revisionId, IReadOnlyList<string> files)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public void RequestUpdateTo(string revisionId)
{
instance.Update(revisionId, true);
}
/// <inheritdoc />
public void RequestRestoreTo(string revisionId)
{
instance.ResyncToRevision(revisionId);
}
/// <inheritdoc />
public void RequestGoBackTo(string revisionId)
{
instance.GoBackToRevision(revisionId, false);
}
/// <inheritdoc />
public void ClearError()
{
instance.ClearErrors();
}
/// <inheritdoc />
public void RequestShowConflictedDifferences(string path)
{
if (UnityEditor.Collaboration.Collab.IsDiffToolsAvailable())
{
instance.ShowConflictDifferences(path);
}
else
{
Debug.Log(StringAssets.noMergeToolIsConfigured);
}
}
/// <inheritdoc />
public void RequestChooseMerge(string path)
{
if (UnityEditor.Collaboration.Collab.IsDiffToolsAvailable())
{
instance.LaunchConflictExternalMerge(path);
}
else
{
Debug.Log(StringAssets.noMergeToolIsConfigured);
}
}
/// <inheritdoc />
public void RequestChooseMine(string[] paths)
{
instance.SetConflictsResolvedMine(paths);
}
/// <inheritdoc />
public void RequestChooseRemote(string[] paths)
{
instance.SetConflictsResolvedTheirs(paths);
}
/// <inheritdoc />
public void RequestSync()
{
QueueHistoryRequest(0, 1, Callback);
void Callback(int? count, IReadOnlyList<IHistoryEntry> revisions)
{
if (revisions != null && revisions.Count > 0)
{
instance.Update(revisions[0].RevisionId, true);
}
else
{
Debug.LogError("Remote revision id is unknown. Please try again.");
}
}
}
/// <inheritdoc />
public void RequestCancelJob()
{
instance.CancelJob(0);
}
/// <inheritdoc />
public virtual void ShowServicePage()
{
SettingsService.OpenProjectSettings("Project/Services/Collaborate");
}
/// <inheritdoc />
public void ShowLoginPage()
{
UnityConnect.instance.ShowLogin();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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<HttpResponseMessage> 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
/// <summary>
/// Converts a Collab Revision to an IHistoryEntry.
/// </summary>
/// <param name="revision">Revision to convert.</param>
/// <returns>Resultant IHistoryEntry</returns>
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);
}
/// <summary>
/// Converts a Collab ChangeAction to an IChangeEntry.
/// </summary>
/// <param name="action">ChangeAction to convert.</param>
/// <returns>Resultant IChangeEntry</returns>
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);
}
/// <summary>
/// Converts a Collab CollabStates to an ChangeEntryStatus.
/// Note that CollabStates is a bitwise flag, while
/// ChangeEntryStatus is an enum, so ordering matters.
/// </summary>
/// <param name="state">ChangeAction to convert.</param>
/// <returns>Resultant ChangeEntryStatus</returns>
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;
}
/// <summary>
/// Checks the state of a flag in CollabStates.
/// </summary>
/// <param name="state">State to check from.</param>
/// <param name="flag">Flag to check in the state.</param>
/// <returns>True if flag is set.</returns>
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
}
}
}
# Models
In this directory, we have all of the interfaces and implementations of the Models in the package's **MVP** architecture.
In `Api/` we have the interfaces that the Models take to communicate with the backend.
In `Enums/` and `Structures/` we have the supporting data structures for the interfaces in `Api/`.
In `Providers/` we have the implementations of the interfaces in `Api/`. At this moment only `Collab.cs` exists.
using System;
using JetBrains.Annotations;
using Unity.Cloud.Collaborate.Models.Api;
using Unity.Cloud.Collaborate.Models.Enums;
using Unity.Cloud.Collaborate.UserInterface;
using UnityEngine;
namespace Unity.Cloud.Collaborate.Models
{
internal class StartModel : IStartModel
{
[NotNull]
readonly ISourceControlProvider m_Provider;
/// <inheritdoc />
public event Action<ProjectStatus> ProjectStatusChanged;
/// <inheritdoc />
public event Action StateChanged;
public StartModel([NotNull] ISourceControlProvider provider)
{
m_Provider = provider;
}
/// <inheritdoc />
public void OnStart()
{
m_Provider.UpdatedProjectStatus += OnUpdatedProjectStatus;
}
/// <inheritdoc />
public void OnStop()
{
m_Provider.UpdatedProjectStatus -= OnUpdatedProjectStatus;
}
/// <inheritdoc />
public void RestoreState(IWindowCache cache)
{
StateChanged?.Invoke();
}
/// <inheritdoc />
public void SaveState(IWindowCache cache)
{
}
/// <inheritdoc />
public ProjectStatus ProjectStatus => m_Provider.GetProjectStatus();
/// <inheritdoc />
public void RequestTurnOnService()
{
m_Provider.RequestTurnOnService();
}
/// <inheritdoc />
public void ShowServicePage()
{
m_Provider.ShowServicePage();
}
/// <inheritdoc />
public void ShowLoginPage()
{
m_Provider.ShowLoginPage();
}
/// <inheritdoc />
public void ShowNoSeatPage()
{
m_Provider.ShowNoSeatPage();
}
void OnUpdatedProjectStatus(ProjectStatus status)
{
ProjectStatusChanged?.Invoke(status);
}
}
}
using System;
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal struct ChangeEntry : IChangeEntry
{
public ChangeEntry(string path = default, string originalPath = default, ChangeEntryStatus status = default, bool staged = default, bool unmerged = default, object tag = default)
{
Path = path;
OriginalPath = originalPath;
Status = status;
Staged = staged;
Unmerged = unmerged;
Tag = tag;
}
public string Path { get; }
public string OriginalPath { get; }
public ChangeEntryStatus Status { get; }
public bool Staged { get; }
public bool Unmerged { get; }
public object Tag { get; }
/// <inheritdoc />
public string StatusToString()
{
switch (Status)
{
case ChangeEntryStatus.Added:
case ChangeEntryStatus.Untracked:
return "added";
case ChangeEntryStatus.Modified:
case ChangeEntryStatus.TypeChange:
return "edited";
case ChangeEntryStatus.Deleted:
return "deleted";
case ChangeEntryStatus.Renamed:
case ChangeEntryStatus.Copied:
return "moved";
case ChangeEntryStatus.Unmerged:
return "conflicted";
case ChangeEntryStatus.None:
break;
case ChangeEntryStatus.Ignored:
break;
case ChangeEntryStatus.Unknown:
break;
case ChangeEntryStatus.Broken:
break;
default:
throw new ArgumentOutOfRangeException();
}
// TODO: find a way to handle/display the unexpected/broken status types.
return null;
}
}
}
using System;
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal struct ErrorInfo : IErrorInfo
{
public ErrorInfo(int code = default, int priority = default, int behaviour = default, string message = default, string shortMessage = default, string codeString = default)
{
Code = code;
Priority = (ErrorInfoPriority)priority;
Behaviour = (ErrorInfoBehavior)behaviour;
Message = message;
ShortMessage = shortMessage;
CodeString = codeString;
}
public int Code { get; }
public ErrorInfoPriority Priority { get; }
public ErrorInfoBehavior Behaviour { get; }
public string Message { get; }
public string ShortMessage { get; }
public string CodeString { get; }
}
}
using System;
using System.Collections.Generic;
using Unity.Cloud.Collaborate.Assets;
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal struct HistoryEntry : IHistoryEntry
{
public HistoryEntry(string revisionId = default, HistoryEntryStatus status = HistoryEntryStatus.Behind, string authorName = default, string message = default, DateTimeOffset time = default, IReadOnlyList<IChangeEntry> changes = default)
{
Status = status;
RevisionId = revisionId;
AuthorName = authorName;
Message = message;
Time = time;
Changes = changes;
}
public HistoryEntryStatus Status { get; }
public string RevisionId { get; }
public string AuthorName { get; }
public string Message { get; }
public DateTimeOffset Time { get; }
public IReadOnlyList<IChangeEntry> Changes { get; }
public string GetGotoText()
{
switch (Status)
{
case HistoryEntryStatus.Ahead:
return StringAssets.update;
case HistoryEntryStatus.Current:
return StringAssets.restore;
case HistoryEntryStatus.Behind:
return StringAssets.goBackTo;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal enum ChangeEntryStatus
{
None,
Untracked,
Ignored,
Modified,
Added,
Deleted,
Renamed,
Copied,
TypeChange,
Unmerged,
Unknown,
Broken
}
internal interface IChangeEntry
{
string Path { get; }
string OriginalPath { get; }
ChangeEntryStatus Status { get; }
bool Staged { get; }
bool Unmerged { get; }
object Tag { get; }
/// <summary>
/// Returns the string name of the status of this entry. Returns null if the status isn't used at present.
/// </summary>
/// <returns>String of used status. Null otherwise.</returns>
string StatusToString();
}
}
using System;
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal interface IChangeEntryData
{
IChangeEntry Entry { get; }
bool Toggled { get; }
bool All { get; }
bool ToggleReadOnly { get; }
bool Conflicted { get; }
}
}
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal interface IErrorInfo
{
int Code { get; }
ErrorInfoPriority Priority { get; }
ErrorInfoBehavior Behaviour { get; }
string Message { get; }
string ShortMessage { get; }
string CodeString { get; }
}
internal enum ErrorInfoPriority
{
Critical = 0,
Error,
Warning,
Info,
None
}
internal enum ErrorInfoBehavior
{
Alert = 0,
Automatic,
Hidden,
ConsoleOnly,
Reconnect
}
}
using System;
using System.Collections.Generic;
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal enum HistoryEntryStatus
{
Ahead,
Current,
Behind
}
internal interface IHistoryEntry
{
HistoryEntryStatus Status { get; }
string RevisionId { get; }
string AuthorName { get; }
string Message { get; }
DateTimeOffset Time { get; }
IReadOnlyList<IChangeEntry> Changes { get; }
string GetGotoText();
}
}
namespace Unity.Cloud.Collaborate.Models.Structures
{
internal interface IProgressInfo
{
string Title { get; }
string Details { get; }
int CurrentCount { get; }
int TotalCount { get; }
string LastErrorString { get; }
ulong LastError { get; }
bool CanCancel { get; }
bool PercentageProgressType { get; }
int PercentageComplete { get; }
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment