Collab.cs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Net;
  6. using System.Net.Http;
  7. using System.Net.Http.Headers;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. using JetBrains.Annotations;
  11. using Unity.Cloud.Collaborate.Assets;
  12. using Unity.Cloud.Collaborate.Models.Api;
  13. using Unity.Cloud.Collaborate.Models.Enums;
  14. using Unity.Cloud.Collaborate.Models.Structures;
  15. using Unity.Cloud.Collaborate.Utilities;
  16. using UnityEditor;
  17. using UnityEditor.Collaboration;
  18. using UnityEditor.Connect;
  19. using UnityEditorInternal;
  20. using UnityEngine;
  21. using UnityEngine.Assertions;
  22. using static UnityEditor.Collaboration.Collab;
  23. using ProgressInfo = UnityEditor.Collaboration.ProgressInfo;
  24. namespace Unity.Cloud.Collaborate.Models.Providers
  25. {
  26. internal class Collab : ISourceControlProvider
  27. {
  28. const string k_KServiceUrl = "developer.cloud.unity3d.com";
  29. readonly RevisionsService m_RevisionsService;
  30. /// <inheritdoc />
  31. public event Action UpdatedChangeList;
  32. /// <inheritdoc />
  33. public event Action<IReadOnlyList<string>> UpdatedSelectedChangeList;
  34. /// <inheritdoc />
  35. public event Action<bool> UpdatedConflictState;
  36. /// <inheritdoc />
  37. public event Action<bool> UpdatedRemoteRevisionsAvailability;
  38. /// <inheritdoc />
  39. public event Action<ProjectStatus> UpdatedProjectStatus;
  40. /// <inheritdoc />
  41. public event Action<bool> UpdatedOperationStatus;
  42. /// <inheritdoc />
  43. public event Action<IProgressInfo> UpdatedOperationProgress;
  44. /// <inheritdoc />
  45. public event Action<IErrorInfo> ErrorOccurred;
  46. /// <inheritdoc />
  47. public event Action ErrorCleared;
  48. readonly List<IChangeEntry> m_Changes;
  49. bool m_ConflictCachedState;
  50. bool m_RemoteRevisionsAvailableState;
  51. // History entry requesting bits and bobs.
  52. readonly Queue<(int offset, int size, Action<int?, IReadOnlyList<IHistoryEntry>>)> m_HistoryRequests;
  53. [NotNull]
  54. IReadOnlyList<IHistoryEntry> m_HistoryEntries;
  55. (int offset, int size)? m_HistoryEntriesCache;
  56. [CanBeNull]
  57. IHistoryEntry m_HistoryEntryCache;
  58. int? m_HistoryEntryCountCache;
  59. string m_TipCache;
  60. [CanBeNull]
  61. IErrorInfo m_ErrorInfo;
  62. [CanBeNull]
  63. IProgressInfo m_ProgressInfo;
  64. ProjectStatus m_ProjectStatus;
  65. public Collab()
  66. {
  67. m_RevisionsService = new RevisionsService(instance, UnityConnect.instance);
  68. m_Changes = new List<IChangeEntry>();
  69. m_HistoryEntries = new List<IHistoryEntry>();
  70. m_HistoryRequests = new Queue<(int offset, int size, Action<int?, IReadOnlyList<IHistoryEntry>>)>();
  71. // Get initial values.
  72. var info = instance.collabInfo;
  73. m_ConflictCachedState = info.conflict;
  74. m_RemoteRevisionsAvailableState = info.update;
  75. m_TipCache = info.tip;
  76. m_ProgressInfo = info.inProgress ? ProgressInfoFromCollab(instance.GetJobProgress(0)) : null;
  77. m_ErrorInfo = instance.GetError(UnityConnect.UnityErrorFilter.ByContext | UnityConnect.UnityErrorFilter.ByChild, out var errInfo)
  78. ? ErrorInfoFromUnity(errInfo)
  79. : null;
  80. m_ProjectStatus = GetNewProjectStatus(info, UnityConnect.instance.connectInfo, UnityConnect.instance.projectInfo);
  81. SetupEvents();
  82. }
  83. /// <summary>
  84. /// Setup events for the provider.
  85. /// </summary>
  86. void SetupEvents()
  87. {
  88. // just connect notifier events.
  89. instance.ChangeItemsChanged += OnChangeItemsChanged;
  90. instance.SelectedChangeItemsChanged += OnSelectedChangeItemsChanged;
  91. instance.RevisionUpdated_V2 += OnRevisionUpdated;
  92. instance.CollabInfoChanged += OnCollabInfoChanged;
  93. instance.JobsCompleted += OnJobsCompleted;
  94. instance.ErrorOccurred_V2 += OnErrorOccurred;
  95. instance.ErrorCleared += OnErrorCleared;
  96. instance.StateChanged += OnCollabStateChanged;
  97. UnityConnect.instance.StateChanged += OnUnityConnectStateChanged;
  98. UnityConnect.instance.ProjectStateChanged += OnUnityConnectProjectStateChanged;
  99. m_RevisionsService.FetchRevisionsCallback += OnReceiveHistoryEntries;
  100. }
  101. #region Callback & Helper Methods
  102. /// <summary>
  103. /// Event handler for when the change list has changed.
  104. /// </summary>
  105. /// <param name="changes">New change list.</param>
  106. /// <param name="isFiltered">Whether or not the list is filtered. Should always be false.</param>
  107. void OnChangeItemsChanged(ChangeItem[] changes, bool isFiltered)
  108. {
  109. UpdateChanges(changes);
  110. UpdatedChangeList?.Invoke();
  111. }
  112. /// <summary>
  113. /// WIP method to handle partial publish in collab.
  114. /// </summary>
  115. /// <param name="changes">Received changes.</param>
  116. /// <param name="isFiltered">Whether or not it's a partial publish. Should always be true.</param>
  117. void OnSelectedChangeItemsChanged(ChangeItem[] changes, bool isFiltered)
  118. {
  119. // This is used by selective commit. Assert all API calls to here are setting isFiltered to true !
  120. Debug.Assert(isFiltered);
  121. var selectedChanges = changes.Select(e => e.Path).ToList();
  122. UpdatedSelectedChangeList?.Invoke(selectedChanges);
  123. }
  124. /// <summary>
  125. /// Event handler for when a revision has been created or updated. It's not called 100% of the time when a user
  126. /// publishes a new revision.
  127. /// </summary>
  128. /// <param name="info">New collab info.</param>
  129. /// <param name="rev">New revision id.</param>
  130. /// <param name="action">Action that occured.</param>
  131. void OnRevisionUpdated(CollabInfo info, string rev, string action)
  132. {
  133. // Invalidate the cache.
  134. m_HistoryEntriesCache = null;
  135. m_HistoryEntryCache = null;
  136. m_HistoryEntryCountCache = null;
  137. // Send update event.
  138. UpdatedHistoryEntries?.Invoke();
  139. OnCollabInfoChanged(info);
  140. }
  141. void OnCollabInfoChanged(CollabInfo info)
  142. {
  143. // Update conflict state.
  144. if (m_ConflictCachedState != info.conflict)
  145. {
  146. m_ConflictCachedState = info.conflict;
  147. UpdatedConflictState?.Invoke(info.conflict);
  148. }
  149. // Update revisions available state.
  150. if (m_RemoteRevisionsAvailableState != info.update)
  151. {
  152. m_RemoteRevisionsAvailableState = info.update;
  153. UpdatedRemoteRevisionsAvailability?.Invoke(info.update);
  154. }
  155. // Update history list if the tip has changed.
  156. if (m_TipCache != info.tip)
  157. {
  158. m_TipCache = info.tip;
  159. // Invalidate the cache.
  160. m_HistoryEntriesCache = null;
  161. m_HistoryEntryCache = null;
  162. m_HistoryEntryCountCache = null;
  163. // Send update event.
  164. UpdatedHistoryEntries?.Invoke();
  165. }
  166. // Update project state
  167. UpdateProjectStatus(info, UnityConnect.instance.connectInfo, UnityConnect.instance.projectInfo);
  168. // Update progress state.
  169. if (info.inProgress)
  170. {
  171. // Get progress info.
  172. var progressInfo = instance.GetJobProgress(0);
  173. Assert.IsNotNull(progressInfo);
  174. // Trigger start operation if not already known.
  175. if (m_ProgressInfo == null)
  176. {
  177. UpdatedOperationStatus?.Invoke(true);
  178. }
  179. // Send progress info.
  180. m_ProgressInfo = ProgressInfoFromCollab(progressInfo);
  181. UpdatedOperationProgress?.Invoke(m_ProgressInfo);
  182. }
  183. else if (m_ProgressInfo != null)
  184. {
  185. // Signal end of job if job still exists
  186. m_ProgressInfo = null;
  187. UpdatedOperationStatus?.Invoke(false);
  188. }
  189. }
  190. void OnJobsCompleted(CollabInfo info)
  191. {
  192. // NOTE: The first start of collab sends a completion event with no prior progress info.
  193. // To handle this, skip sending completion event if there has been no start event.
  194. if (m_ProgressInfo == null) return;
  195. Assert.IsFalse(info.inProgress);
  196. m_ProgressInfo = null;
  197. UpdatedOperationStatus?.Invoke(false);
  198. }
  199. void OnErrorOccurred(UnityErrorInfo error)
  200. {
  201. if (m_ErrorInfo?.Code == error.code) return;
  202. m_ErrorInfo = ErrorInfoFromUnity(error);
  203. ErrorOccurred?.Invoke(m_ErrorInfo);
  204. }
  205. void OnErrorCleared()
  206. {
  207. m_ErrorInfo = null;
  208. ErrorCleared?.Invoke();
  209. }
  210. /// <summary>
  211. /// On receiving history result, remove the oldest request, send the received data, then make the next request.
  212. /// </summary>
  213. /// <param name="revisionsResult">Result from the history request.</param>
  214. void OnReceiveHistoryEntries(RevisionsResult revisionsResult)
  215. {
  216. Assert.AreNotEqual(0, m_HistoryRequests.Count, "There should be a history request.");
  217. var (offset, size, callback) = m_HistoryRequests.Dequeue();
  218. // Get results, cache, then send them.
  219. var results = revisionsResult?.Revisions.Select(RevisionToHistoryEntry).ToList();
  220. if (results != null)
  221. {
  222. m_HistoryEntries = results;
  223. m_HistoryEntriesCache = (offset, size);
  224. m_HistoryEntryCountCache = revisionsResult.RevisionsInRepo;
  225. callback(revisionsResult.RevisionsInRepo, m_HistoryEntries);
  226. }
  227. // Start the next request --> has to be outside of the callback.
  228. EditorApplication.delayCall += () => ConsumeHistoryQueue();
  229. }
  230. /// <summary>
  231. /// Event handler for receiving unity connect project state changes.
  232. /// </summary>
  233. /// <param name="projectInfo">New project info.</param>
  234. void OnUnityConnectProjectStateChanged(ProjectInfo projectInfo)
  235. {
  236. UpdateProjectStatus(instance.collabInfo, UnityConnect.instance.connectInfo, projectInfo);
  237. }
  238. /// <summary>
  239. /// Event handler for receiving collab state changes.
  240. /// </summary>
  241. /// <param name="info">New collab state.</param>
  242. void OnCollabStateChanged(CollabInfo info)
  243. {
  244. OnCollabInfoChanged(info);
  245. }
  246. /// <summary>
  247. /// Event handler for receiving collab state changes.
  248. /// </summary>
  249. /// <param name="connectInfo">UnityConnect connect info.</param>
  250. void OnUnityConnectStateChanged(ConnectInfo connectInfo)
  251. {
  252. UpdateProjectStatus(instance.collabInfo, connectInfo, UnityConnect.instance.projectInfo);
  253. }
  254. /// <summary>
  255. /// Update cached ready value and send event if it has changed.
  256. /// </summary>
  257. void UpdateProjectStatus(CollabInfo collabInfo, ConnectInfo connectInfo, ProjectInfo projectInfo)
  258. {
  259. var currentStatus = GetNewProjectStatus(collabInfo, connectInfo, projectInfo);
  260. if (m_ProjectStatus == currentStatus) return;
  261. m_ProjectStatus = currentStatus;
  262. UpdatedProjectStatus?.Invoke(m_ProjectStatus);
  263. }
  264. /// <summary>
  265. /// Returns the current project status.
  266. /// </summary>
  267. /// <returns>Current status of this project.</returns>
  268. static ProjectStatus GetNewProjectStatus(CollabInfo collabInfo, ConnectInfo connectInfo, ProjectInfo projectInfo)
  269. {
  270. // No UPID.
  271. if (!projectInfo.projectBound)
  272. {
  273. return ProjectStatus.Unbound;
  274. }
  275. if (!connectInfo.online)
  276. {
  277. return ProjectStatus.Offline;
  278. }
  279. if (connectInfo.maintenance || collabInfo.maintenance)
  280. {
  281. return ProjectStatus.Maintenance;
  282. }
  283. if (!connectInfo.loggedIn)
  284. {
  285. return ProjectStatus.LoggedOut;
  286. }
  287. if (!collabInfo.seat)
  288. {
  289. return ProjectStatus.NoSeat;
  290. }
  291. // UPID exists, but collab off.
  292. if (!instance.IsCollabEnabledForCurrentProject())
  293. {
  294. return ProjectStatus.Bound;
  295. }
  296. // Waiting for collab to connect and be ready.
  297. if (!instance.IsConnected() || !collabInfo.ready)
  298. {
  299. return ProjectStatus.Loading;
  300. }
  301. return ProjectStatus.Ready;
  302. }
  303. /// <summary>
  304. /// Consume the next entry on the history queue.
  305. /// </summary>
  306. /// <param name="afterEnqueue">True if an entry was just inserted. Starts the consumption cycle.</param>
  307. void ConsumeHistoryQueue(bool afterEnqueue = false)
  308. {
  309. // Start consuming the queue if the first entry was just enqueued.
  310. if (afterEnqueue && m_HistoryRequests.Count != 1) return;
  311. // Can't consume an empty queue.
  312. if (m_HistoryRequests.Count == 0) return;
  313. var (offset, size, callback) = m_HistoryRequests.Peek();
  314. // Execute next request. Discard if exception.
  315. try
  316. {
  317. m_RevisionsService.GetRevisions(offset, size);
  318. }
  319. catch (Exception e)
  320. {
  321. Debug.LogException(e);
  322. // Remove request and send failure callback.
  323. m_HistoryRequests.Dequeue();
  324. callback(null, null);
  325. }
  326. }
  327. /// <summary>
  328. /// Make a history request.
  329. /// </summary>
  330. /// <param name="offset">Offset for the request to start from.</param>
  331. /// <param name="size">Target length of the resultant list.</param>
  332. /// <param name="callback">Callback for the result.</param>
  333. void QueueHistoryRequest(int offset, int size, Action<int?, IReadOnlyList<IHistoryEntry>> callback)
  334. {
  335. m_HistoryRequests.Enqueue((offset, size, callback));
  336. ConsumeHistoryQueue(true);
  337. }
  338. /// <summary>
  339. /// Update cache of converted change entries from provided collab changes.
  340. /// </summary>
  341. /// <param name="changes">Received list of changes from collab.</param>
  342. void UpdateChanges(IEnumerable<Change> changes)
  343. {
  344. m_Changes.Clear();
  345. m_Changes.AddRange(changes.Select(change =>
  346. new ChangeEntry(change.path, change.path, ChangeEntryStatusFromCollabState(change.state),
  347. false, IsCollabStateFlagSet(change.state, CollabStates.kCollabConflicted | CollabStates.kCollabPendingMerge), change))
  348. .Cast<IChangeEntry>());
  349. }
  350. /// <summary>
  351. /// Update cache of converted change entries from provided collab changes.
  352. /// </summary>
  353. /// <param name="changes">Received list of changes from collab.</param>
  354. void UpdateChanges(IEnumerable<ChangeItem> changes)
  355. {
  356. m_Changes.Clear();
  357. m_Changes.AddRange(changes.Select(change =>
  358. new ChangeEntry(change.Path, change.Path, ChangeEntryStatusFromCollabState(change.State),
  359. false, IsCollabStateFlagSet(change.State, CollabStates.kCollabConflicted | CollabStates.kCollabPendingMerge), change))
  360. .Cast<IChangeEntry>());
  361. }
  362. /// <inheritdoc />
  363. public bool GetRemoteRevisionAvailability()
  364. {
  365. // Return cached value.
  366. return m_RemoteRevisionsAvailableState;
  367. }
  368. /// <inheritdoc />
  369. public bool GetConflictedState()
  370. {
  371. // Return cached value.
  372. return m_ConflictCachedState;
  373. }
  374. /// <inheritdoc />
  375. public IProgressInfo GetProgressState()
  376. {
  377. // Return cached value.
  378. return m_ProgressInfo;
  379. }
  380. /// <inheritdoc />
  381. public IErrorInfo GetErrorState()
  382. {
  383. return m_ErrorInfo;
  384. }
  385. /// <inheritdoc />
  386. public virtual ProjectStatus GetProjectStatus()
  387. {
  388. return m_ProjectStatus;
  389. }
  390. /// <inheritdoc />
  391. public void RequestChangeList(Action<IReadOnlyList<IChangeEntry>> callback)
  392. {
  393. var changes = instance.GetChangesToPublish_V2().changes;
  394. UpdateChanges(changes);
  395. callback(m_Changes);
  396. // Also check for errors.
  397. if (instance.GetError(UnityConnect.UnityErrorFilter.All, out var error) &&
  398. (CollabErrorCode)error.code != CollabErrorCode.Collab_ErrNone)
  399. {
  400. ErrorOccurred?.Invoke(ErrorInfoFromUnity(error));
  401. }
  402. }
  403. /// <inheritdoc />
  404. public void RequestPublish(string message, IReadOnlyList<IChangeEntry> changeEntries = null)
  405. {
  406. var changeItems = changeEntries?.Select(EntryToChangeItem).ToArray();
  407. instance.PublishAssetsAsync(message, changeItems);
  408. ChangeItem EntryToChangeItem(IChangeEntry entry)
  409. {
  410. return entry.Tag as ChangeItem;
  411. }
  412. }
  413. #endregion
  414. #region SourceControlHistoryCommands
  415. /// <inheritdoc />
  416. public event Action UpdatedHistoryEntries;
  417. /// <inheritdoc />
  418. public void RequestHistoryEntry(string revisionId, Action<IHistoryEntry> callback)
  419. {
  420. // Return cached entry if possible.
  421. if (m_HistoryEntryCache?.RevisionId == revisionId)
  422. {
  423. callback(m_HistoryEntryCache);
  424. return;
  425. }
  426. // Ensure that a cleanup occurs in the case of an exception.
  427. m_RevisionsService.FetchSingleRevisionCallback += OnFetchRevisionCallback;
  428. try
  429. {
  430. m_RevisionsService.GetRevision(revisionId);
  431. }
  432. catch (Exception e)
  433. {
  434. Debug.LogException(e);
  435. m_RevisionsService.FetchSingleRevisionCallback -= OnFetchRevisionCallback;
  436. callback(null);
  437. }
  438. void OnFetchRevisionCallback(Revision? revision)
  439. {
  440. m_RevisionsService.FetchSingleRevisionCallback -= OnFetchRevisionCallback;
  441. // Failing to find the revision can result in a null revision or an empty revisionID.
  442. callback(string.IsNullOrEmpty(revision?.revisionID)
  443. ? null
  444. : RevisionToHistoryEntry(revision.GetValueOrDefault()));
  445. }
  446. }
  447. /// <inheritdoc />
  448. public void RequestHistoryPage(int offset, int pageSize, Action<IReadOnlyList<IHistoryEntry>> callback)
  449. {
  450. // Return cached entry is possible.
  451. if (m_HistoryEntriesCache?.offset == offset && m_HistoryEntriesCache?.size == pageSize)
  452. {
  453. callback(m_HistoryEntries);
  454. return;
  455. }
  456. // Queue up the request.
  457. QueueHistoryRequest(offset, pageSize, (_, r) => callback(r));
  458. }
  459. /// <inheritdoc />
  460. public void RequestHistoryCount(Action<int?> callback)
  461. {
  462. // Return cached value if possible.
  463. if (m_HistoryEntryCountCache != null)
  464. {
  465. callback(m_HistoryEntryCountCache);
  466. return;
  467. }
  468. QueueHistoryRequest(0, 0, (c, _) => callback(c));
  469. }
  470. /// <inheritdoc />
  471. public void RequestDiscard(IChangeEntry entry)
  472. {
  473. // Collab cannot revert a new file as it has nothing to go back to. So, instead we delete them.
  474. if (entry.Status == ChangeEntryStatus.Added)
  475. {
  476. File.Delete(entry.Path);
  477. // Notify ADB to refresh since a change has been made.
  478. AssetDatabase.Refresh();
  479. }
  480. else
  481. {
  482. instance.RevertFile(entry.Path, true);
  483. }
  484. }
  485. /// <inheritdoc />
  486. public void RequestBulkDiscard(IReadOnlyList<IChangeEntry> entries)
  487. {
  488. var revertEntries = new List<ChangeItem>();
  489. var deleteOccured = false;
  490. foreach (var entry in entries)
  491. {
  492. // Collab cannot revert a new file as it has nothing to go back to. So, instead we delete them.
  493. if (entry.Status == ChangeEntryStatus.Added)
  494. {
  495. File.Delete(entry.Path);
  496. deleteOccured = true;
  497. }
  498. else
  499. {
  500. revertEntries.Add((ChangeItem)entry.Tag);
  501. }
  502. }
  503. // If a change has been made, notify the ADB to refresh.
  504. if (deleteOccured)
  505. {
  506. AssetDatabase.Refresh();
  507. }
  508. instance.RevertFiles(revertEntries.ToArray(), true);
  509. }
  510. /// <inheritdoc />
  511. public void RequestDiffChanges(string path)
  512. {
  513. instance.ShowDifferences(path);
  514. }
  515. /// <inheritdoc />
  516. public bool SupportsRevert { get; } = false;
  517. /// <inheritdoc />
  518. public void RequestRevert(string revisionId, IReadOnlyList<string> files)
  519. {
  520. throw new NotImplementedException();
  521. }
  522. /// <inheritdoc />
  523. public void RequestUpdateTo(string revisionId)
  524. {
  525. instance.Update(revisionId, true);
  526. }
  527. /// <inheritdoc />
  528. public void RequestRestoreTo(string revisionId)
  529. {
  530. instance.ResyncToRevision(revisionId);
  531. }
  532. /// <inheritdoc />
  533. public void RequestGoBackTo(string revisionId)
  534. {
  535. instance.GoBackToRevision(revisionId, false);
  536. }
  537. /// <inheritdoc />
  538. public void ClearError()
  539. {
  540. instance.ClearErrors();
  541. }
  542. /// <inheritdoc />
  543. public void RequestShowConflictedDifferences(string path)
  544. {
  545. if (UnityEditor.Collaboration.Collab.IsDiffToolsAvailable())
  546. {
  547. instance.ShowConflictDifferences(path);
  548. }
  549. else
  550. {
  551. Debug.Log(StringAssets.noMergeToolIsConfigured);
  552. }
  553. }
  554. /// <inheritdoc />
  555. public void RequestChooseMerge(string path)
  556. {
  557. if (UnityEditor.Collaboration.Collab.IsDiffToolsAvailable())
  558. {
  559. instance.LaunchConflictExternalMerge(path);
  560. }
  561. else
  562. {
  563. Debug.Log(StringAssets.noMergeToolIsConfigured);
  564. }
  565. }
  566. /// <inheritdoc />
  567. public void RequestChooseMine(string[] paths)
  568. {
  569. instance.SetConflictsResolvedMine(paths);
  570. }
  571. /// <inheritdoc />
  572. public void RequestChooseRemote(string[] paths)
  573. {
  574. instance.SetConflictsResolvedTheirs(paths);
  575. }
  576. /// <inheritdoc />
  577. public void RequestSync()
  578. {
  579. QueueHistoryRequest(0, 1, Callback);
  580. void Callback(int? count, IReadOnlyList<IHistoryEntry> revisions)
  581. {
  582. if (revisions != null && revisions.Count > 0)
  583. {
  584. instance.Update(revisions[0].RevisionId, true);
  585. }
  586. else
  587. {
  588. Debug.LogError("Remote revision id is unknown. Please try again.");
  589. }
  590. }
  591. }
  592. /// <inheritdoc />
  593. public void RequestCancelJob()
  594. {
  595. instance.CancelJob(0);
  596. }
  597. /// <inheritdoc />
  598. public virtual void ShowServicePage()
  599. {
  600. SettingsService.OpenProjectSettings("Project/Services/Collaborate");
  601. }
  602. /// <inheritdoc />
  603. public void ShowLoginPage()
  604. {
  605. UnityConnect.instance.ShowLogin();
  606. }
  607. /// <inheritdoc />
  608. public void ShowNoSeatPage()
  609. {
  610. var unityConnect = UnityConnect.instance;
  611. var env = unityConnect.GetEnvironment();
  612. // Map environment to url - prod is special
  613. if (env == "production")
  614. env = "";
  615. else
  616. env += "-";
  617. var url = "https://" + env + k_KServiceUrl
  618. + "/orgs/" + unityConnect.GetOrganizationId()
  619. + "/projects/" + unityConnect.GetProjectName()
  620. + "/unity-teams/";
  621. Application.OpenURL(url);
  622. }
  623. /// <inheritdoc />
  624. public async void RequestTurnOnService()
  625. {
  626. try
  627. {
  628. await RequestTurnOnServiceInternal();
  629. }
  630. catch (Exception e)
  631. {
  632. Debug.LogException(e);
  633. }
  634. }
  635. protected async Task RequestTurnOnServiceInternal()
  636. {
  637. Assert.IsTrue(Threading.IsMainThread, "This must be run on the main thread.");
  638. // Fire up the update Genesis service flag request.
  639. var http = new HttpClientHandler { CookieContainer = new CookieContainer() };
  640. var client = new HttpClient(http);
  641. var projectGuid = UnityConnect.instance.projectInfo.projectGUID;
  642. var accessToken = UnityConnect.instance.GetAccessToken();
  643. client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
  644. client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
  645. client.DefaultRequestHeaders.TryAddWithoutValidation("X-UNITY-VERSION", InternalEditorUtility.GetFullUnityVersion());
  646. var fullUrl = $"{UnityConnect.instance.GetConfigurationURL(CloudConfigUrl.CloudCore)}/api/projects/{projectGuid}/service_flags";
  647. const string json = @"{ ""service_flags"": { ""collab"" : true} }";
  648. var content = new StringContent(json, Encoding.UTF8, "application/json");
  649. var response = await PutAsync(client, fullUrl, content);
  650. // Success.
  651. if (response?.StatusCode == HttpStatusCode.OK)
  652. {
  653. SaveAssets();
  654. TurnOnCollabInternal();
  655. }
  656. // Error.
  657. else if (response?.StatusCode == HttpStatusCode.Forbidden)
  658. {
  659. ShowCredentialsError();
  660. }
  661. else
  662. {
  663. ShowGeneralError();
  664. }
  665. }
  666. protected virtual void SaveAssets()
  667. {
  668. instance.SaveAssets();
  669. }
  670. protected virtual Task<HttpResponseMessage> PutAsync(HttpClient client, string fullUrl, StringContent content)
  671. {
  672. return client.PutAsync(fullUrl, content);
  673. }
  674. protected virtual void TurnOnCollabInternal()
  675. {
  676. // enable the server from the client..
  677. instance.SetCollabEnabledForCurrentProject(true);
  678. // persist by marking collab on in settings
  679. PlayerSettings.SetCloudServiceEnabled("Collab", true);
  680. }
  681. protected virtual void ShowCredentialsError()
  682. {
  683. // TODO - ahmad :- Show an Error UI.
  684. Debug.LogError("You need owner privilege to enable or disable collab.");
  685. }
  686. protected virtual void ShowGeneralError()
  687. {
  688. // TODO - ahmad :- Show an Error UI.
  689. Debug.LogError("cannot enable collab");
  690. }
  691. #endregion
  692. #region Static Helper Methods
  693. /// <summary>
  694. /// Converts a Collab Revision to an IHistoryEntry.
  695. /// </summary>
  696. /// <param name="revision">Revision to convert.</param>
  697. /// <returns>Resultant IHistoryEntry</returns>
  698. IHistoryEntry RevisionToHistoryEntry(Revision revision)
  699. {
  700. var time = DateTimeOffset.FromUnixTimeSeconds((long)revision.timeStamp);
  701. var entries = revision.entries.Select(ChangeActionToChangeEntry).ToList();
  702. var status = HistoryEntryStatus.Ahead;
  703. if (revision.isObtained)
  704. status = HistoryEntryStatus.Behind;
  705. if (revision.revisionID == m_RevisionsService.tipRevision)
  706. status = HistoryEntryStatus.Current;
  707. return new HistoryEntry(revision.revisionID, status, revision.author, revision.comment, time, entries);
  708. }
  709. /// <summary>
  710. /// Converts a Collab ChangeAction to an IChangeEntry.
  711. /// </summary>
  712. /// <param name="action">ChangeAction to convert.</param>
  713. /// <returns>Resultant IChangeEntry</returns>
  714. static IChangeEntry ChangeActionToChangeEntry(ChangeAction action)
  715. {
  716. var unmerged = false;
  717. var status = ChangeEntryStatus.None;
  718. switch (action.action.ToLower())
  719. {
  720. case "added":
  721. status = ChangeEntryStatus.Added;
  722. break;
  723. case "conflict":
  724. status = ChangeEntryStatus.Unmerged;
  725. unmerged = true;
  726. break;
  727. case "deleted":
  728. status = ChangeEntryStatus.Deleted;
  729. break;
  730. case "ignored":
  731. status = ChangeEntryStatus.Ignored;
  732. break;
  733. case "renamed":
  734. case "moved":
  735. status = ChangeEntryStatus.Renamed;
  736. break;
  737. case "updated":
  738. status = ChangeEntryStatus.Modified;
  739. break;
  740. default:
  741. Debug.LogError($"Unknown file status: {action.action}");
  742. break;
  743. }
  744. return new ChangeEntry(action.path, status: status, unmerged: unmerged);
  745. }
  746. /// <summary>
  747. /// Converts a Collab CollabStates to an ChangeEntryStatus.
  748. /// Note that CollabStates is a bitwise flag, while
  749. /// ChangeEntryStatus is an enum, so ordering matters.
  750. /// </summary>
  751. /// <param name="state">ChangeAction to convert.</param>
  752. /// <returns>Resultant ChangeEntryStatus</returns>
  753. static ChangeEntryStatus ChangeEntryStatusFromCollabState(CollabStates state)
  754. {
  755. if (IsCollabStateFlagSet(state, CollabStates.kCollabIgnored))
  756. {
  757. return ChangeEntryStatus.Ignored;
  758. }
  759. if (IsCollabStateFlagSet(state, CollabStates.kCollabConflicted | CollabStates.kCollabPendingMerge))
  760. {
  761. return ChangeEntryStatus.Unmerged;
  762. }
  763. if (IsCollabStateFlagSet(state, CollabStates.kCollabAddedLocal))
  764. {
  765. return ChangeEntryStatus.Added;
  766. }
  767. if (IsCollabStateFlagSet(state, CollabStates.kCollabMovedLocal))
  768. {
  769. return ChangeEntryStatus.Renamed;
  770. }
  771. if (IsCollabStateFlagSet(state, CollabStates.kCollabDeletedLocal))
  772. {
  773. return ChangeEntryStatus.Deleted;
  774. }
  775. if (IsCollabStateFlagSet(state, CollabStates.kCollabCheckedOutLocal))
  776. {
  777. return ChangeEntryStatus.Modified;
  778. }
  779. return ChangeEntryStatus.Unknown;
  780. }
  781. /// <summary>
  782. /// Checks the state of a flag in CollabStates.
  783. /// </summary>
  784. /// <param name="state">State to check from.</param>
  785. /// <param name="flag">Flag to check in the state.</param>
  786. /// <returns>True if flag is set.</returns>
  787. static bool IsCollabStateFlagSet(CollabStates state, CollabStates flag)
  788. {
  789. return (state & flag) != 0;
  790. }
  791. static IProgressInfo ProgressInfoFromCollab([CanBeNull] ProgressInfo collabProgress)
  792. {
  793. if (collabProgress == null) return null;
  794. return new Structures.ProgressInfo(
  795. collabProgress.title,
  796. collabProgress.extraInfo,
  797. collabProgress.currentCount,
  798. collabProgress.totalCount,
  799. collabProgress.lastErrorString,
  800. collabProgress.lastError,
  801. collabProgress.canCancel,
  802. collabProgress.isProgressTypePercent,
  803. collabProgress.percentComplete);
  804. }
  805. static IErrorInfo ErrorInfoFromUnity(UnityErrorInfo error)
  806. {
  807. return new ErrorInfo(
  808. error.code,
  809. error.priority,
  810. error.behaviour,
  811. error.msg,
  812. error.shortMsg,
  813. error.codeStr);
  814. }
  815. #endregion
  816. enum CollabErrorCode
  817. {
  818. Collab_ErrNone = 0,
  819. Collab_Error,
  820. Collab_ErrProjectNotLinked,
  821. Collab_ErrNoSuchRepository,
  822. Collab_ErrNotLoggedIn,
  823. Collab_ErrNotConnected,
  824. Collab_ErrLocalCache,
  825. Collab_ErrNotUpToDate,
  826. Collab_ErrCannotGetRevision,
  827. Collab_ErrCannotGetRemote,
  828. Collab_ErrCannotGetLocal,
  829. Collab_ErrInvalidHost,
  830. Collab_ErrInvalidPort,
  831. Collab_ErrInvalidRevision,
  832. Collab_ErrNotSnapshot,
  833. Collab_ErrNoSuchRemoteFile,
  834. Collab_ErrNoSuchLocalFile,
  835. Collab_ErrJobNotDefined,
  836. Collab_ErrJobAlreadyRunning,
  837. Collab_ErrAlreadyUpToDate,
  838. Collab_ErrJobNotRunning,
  839. Collab_ErrNotSupported,
  840. Collab_ErrJobCancelled,
  841. Collab_ErrCannotSubmitChanges,
  842. Collab_ErrMD5DoesNotMatch,
  843. Collab_ErrRemoteChanged,
  844. Collab_ErrCannotCreateTempDir,
  845. Collab_ErrCannotDownloadEntry,
  846. Collab_ErrCannotCreatePath,
  847. Collab_ErrCannotCreateFile,
  848. Collab_ErrCannotCopyFile,
  849. Collab_ErrCannotMoveFile,
  850. Collab_ErrCannotDeleteFile,
  851. Collab_ErrCannotGetProjects,
  852. Collab_ErrCannotRestoreSnapshot,
  853. Collab_ErrFileWasAddedLocally,
  854. Collab_ErrFileIsModified,
  855. Collab_ErrFileIsMissing,
  856. Collab_ErrFileAlreadyExists,
  857. Collab_ErrAutomaticMergeBaseIsMissing,
  858. Collab_ErrSmartMergeConflicts,
  859. Collab_ErrTextMergeConflicts,
  860. Collab_ErrAutomaticMerge,
  861. Collab_ErrSmartMerge,
  862. Collab_ErrTextMerge,
  863. Collab_ErrExternalDiff,
  864. Collab_ErrExternalMerge,
  865. Collab_ErrParseJson,
  866. Collab_ErrWrongSerializationMode,
  867. Collab_ErrNoDiffRevisions,
  868. Collab_ErrWorkspaceChanged,
  869. Collab_ErrRefreshChannelAccess,
  870. Collab_ErrUpdateInProgress,
  871. Collab_ErrSoftLocksJobRunning,
  872. Collab_ErrCannotGetSoftLocks,
  873. Collab_ErrPostSoftLocks,
  874. Collab_ErrRequestCancelled,
  875. Collab_ErrCollabInErrorState,
  876. Collab_ErrUsageExceeded,
  877. Collab_ErrRepositoryLocked,
  878. Collab_ErrJobWaitingForSubTasks,
  879. Collab_ErrBadRequest = 400,
  880. Collab_ErrNotAuthorized = 401,
  881. Collab_ErrInternalServerError = 500,
  882. Collab_ErrBadGateway = 502,
  883. Collab_ErrServerUnavailable = 503,
  884. Collab_ErrSmartMergeSetConflictState,
  885. Collab_ErrTextMergeSetConflictState,
  886. Collab_ErrExternalMergeSetConflictState,
  887. Collab_ErrNoDiffMergeToolsConfigured,
  888. Collab_ErrUnsupportedDiffMergeToolConfigured,
  889. Collab_ErrNoSeat,
  890. Collab_ErrNoSeatHidden
  891. }
  892. }
  893. }