From 131f313fd4e59de0c92f0cd2171e82126329630a Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 25 Jan 2018 15:06:59 -0500 Subject: [PATCH 001/567] Adding several new data points to track in Unity --- .../Application/ApplicationManagerBase.cs | 3 +- src/GitHub.Api/Metrics/IUsageTracker.cs | 28 +++- src/GitHub.Api/Metrics/UsageModel.cs | 10 ++ src/GitHub.Api/Metrics/UsageTracker.cs | 138 ++++++++++++++++-- .../GitHub.Unity/UI/AuthenticationView.cs | 5 + .../Editor/GitHub.Unity/UI/BranchesView.cs | 5 +- .../Editor/GitHub.Unity/UI/ChangesView.cs | 5 + .../Editor/GitHub.Unity/UI/HistoryView.cs | 6 + 8 files changed, 181 insertions(+), 19 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 63c4d67bc..2c53665fc 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -120,6 +120,7 @@ public ITask InitializeRepository() Environment.InitializeRepository(); RestartRepository(); }) + .ThenInUI(UsageTracker.IncrementNumberOfProjectsInitialized) .ThenInUI(InitializeUI); return task; } @@ -179,7 +180,7 @@ protected void SetupMetrics(string unityVersion, bool firstRun) if (firstRun) { - UsageTracker.IncrementLaunchCount(); + UsageTracker.IncrementNumberOfStartups(); } } diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index 18b4584ee..0c5522b71 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -6,15 +6,33 @@ namespace GitHub.Unity public interface IUsageTracker { bool Enabled { get; set; } - void IncrementLaunchCount(); + void IncrementNumberOfStartups(); + void IncrementNumberOfCommits(); + void IncrementNumberOfFetches(); + void IncrementNumberOfPushes(); + void IncrementNumberOfPulls(); + void IncrementNumberOfAuthentications(); + void IncrementNumberOfProjectsInitialized(); + void IncrementNumberOfLocalBranchCreations(); + void IncrementNumberOfLocalBranchDeletions(); + void IncrementNumberOfLocalBranchCheckouts(); + void IncrementNumberOfRemoteBranchCheckouts(); } class NullUsageTracker : IUsageTracker { public bool Enabled { get; set; } - - public void IncrementLaunchCount(){ } - public void SetMetricsService(IMetricsService instance) - { } + public void IncrementNumberOfStartups() { } + public void IncrementNumberOfCommits() { } + public void IncrementNumberOfFetches() { } + public void IncrementNumberOfPushes() { } + public void IncrementNumberOfPulls() { } + public void IncrementNumberOfAuthentications() { } + public void IncrementNumberOfProjectsInitialized() { } + public void IncrementNumberOfLocalBranchCreations() { } + public void IncrementNumberOfLocalBranchDeletions() { } + public void IncrementNumberOfLocalBranchCheckouts() { } + public void IncrementNumberOfRemoteBranchCheckouts() { } + public void SetMetricsService(IMetricsService instance) { } } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 66ae88362..2d9011115 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -12,6 +12,16 @@ public class Usage public string UnityVersion { get; set; } public string Lang { get; set; } public int NumberOfStartups { get; set; } + public int NumberOfCommits { get; set; } + public int NumberOfFetches { get; set; } + public int NumberOfPushes { get; set; } + public int NumberOfPulls { get; set; } + public int NumberOfProjectsInitialized { get; set; } + public int NumberOfAuthentications { get; set; } + public int NumberOfLocalBranchCreations { get; set; } + public int NumberOfLocalBranchDeletion { get; set; } + public int NumberOfLocalBranchCheckouts { get; set; } + public int NumberOfRemoteBranchCheckouts { get; set; } } class UsageModel diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 30e90e44e..9d38d4eaa 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -104,12 +104,11 @@ private void RunTimer(int seconds) }, null, seconds * 1000, Timeout.Infinite); } - private async Task SendUsage() { Logger.Trace("SendUsage"); - var usage = LoadUsage(); + var usageStore = LoadUsage(); if (metricsService == null) { @@ -117,7 +116,7 @@ private async Task SendUsage() return; } - if (usage.LastUpdated.Date != DateTimeOffset.UtcNow.Date) + if (usageStore.LastUpdated.Date != DateTimeOffset.UtcNow.Date) { Logger.Trace("Sending Usage"); @@ -125,7 +124,7 @@ private async Task SendUsage() var beforeDate = currentTimeOffset.Date; var success = false; - var extractReports = usage.Model.SelectReports(beforeDate); + var extractReports = usageStore.Model.SelectReports(beforeDate); if (!extractReports.Any()) { Logger.Trace("No items to send"); @@ -151,24 +150,139 @@ private async Task SendUsage() if (success) { - usage.Model.RemoveReports(beforeDate); - usage.LastUpdated = currentTimeOffset; - SaveUsage(usage); + usageStore.Model.RemoveReports(beforeDate); + usageStore.LastUpdated = currentTimeOffset; + SaveUsage(usageStore); } } } - public void IncrementLaunchCount() + private Usage GetCurrentUsage(UsageStore usageStore) { - var usageStore = LoadUsage(); - var usage = usageStore.Model.GetCurrentUsage(); - usage.NumberOfStartups++; usage.UnityVersion = unityVersion; usage.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; usage.AppVersion = AppConfiguration.AssemblyName.Version.ToString(); + return usage; + } + + public void IncrementNumberOfStartups() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfStartups++; + Logger.Trace("NumberOfStartups:{0} Date:{1}", usage.NumberOfStartups, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfCommits() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfCommits++; + Logger.Trace("NumberOfCommits:{0} Date:{1}", usage.NumberOfCommits, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfFetches() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfFetches++; + Logger.Trace("NumberOfFetches:{0} Date:{1}", usage.NumberOfFetches, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfPushes() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfPushes++; + Logger.Trace("NumberOfPushes:{0} Date:{1}", usage.NumberOfPushes, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfProjectsInitialized() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfProjectsInitialized++; + Logger.Trace("NumberOfProjectsInitialized:{0} Date:{1}", usage.NumberOfProjectsInitialized, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfLocalBranchCreations() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfLocalBranchCreations++; + Logger.Trace("NumberOfLocalBranchCreations:{0} Date:{1}", usage.NumberOfLocalBranchCreations, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfLocalBranchDeletions() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfLocalBranchDeletion++; + Logger.Trace("NumberOfLocalBranchDeletion:{0} Date:{1}", usage.NumberOfLocalBranchDeletion, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfLocalBranchCheckouts() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfLocalBranchCheckouts++; + Logger.Trace("NumberOfLocalBranchCheckouts:{0} Date:{1}", usage.NumberOfLocalBranchCheckouts, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfRemoteBranchCheckouts() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfRemoteBranchCheckouts++; + Logger.Trace("NumberOfRemoteBranchCheckouts:{0} Date:{1}", usage.NumberOfRemoteBranchCheckouts, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfPulls() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); + + usage.NumberOfPulls++; + Logger.Trace("NumberOfPulls:{0} Date:{1}", usage.NumberOfPulls, usage.Date); + + SaveUsage(usageStore); + } + + public void IncrementNumberOfAuthentications() + { + var usageStore = LoadUsage(); + var usage = GetCurrentUsage(usageStore); - Logger.Trace("IncrementLaunchCount Date:{0} NumberOfStartups:{1}", usage.Date, usage.NumberOfStartups); + usage.NumberOfAuthentications++; + Logger.Trace("NumberOfAuthentications:{0} Date:{1}", usage.NumberOfAuthentications, usage.Date); SaveUsage(usageStore); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index c7795c05a..b99f6bf2e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using UnityEngine; using UnityEditor; @@ -213,6 +214,10 @@ private void DoResult(bool success, string msg) if (success == true) { + new ActionTask(CancellationToken.None, () => { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfAuthentications(); + }) { Affinity = TaskAffinity.UI }.Start(); + Clear(); Finish(true); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index a4bfff22e..0dbbd93e2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -272,6 +272,7 @@ private void OnButtonBarGUI() { if (success) { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchCreations(); Redraw(); } else @@ -439,6 +440,7 @@ private void CheckoutRemoteBranch(string branch) GitClient.CreateBranch(branchName, branch).FinallyInUI((success, e) => { if (success) { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfRemoteBranchCheckouts(); Redraw(); } else @@ -459,6 +461,7 @@ private void SwitchBranch(string branch) GitClient.SwitchBranch(branch).FinallyInUI((success, e) => { if (success) { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchCheckouts(); Redraw(); } else @@ -475,7 +478,7 @@ private void DeleteLocalBranch(string branch) var dialogMessage = string.Format(DeleteBranchMessageFormatString, branch); if (EditorUtility.DisplayDialog(DeleteBranchTitle, dialogMessage, DeleteBranchButton, CancelButtonLabel)) { - GitClient.DeleteBranch(branch, true).Start(); + GitClient.DeleteBranch(branch, true).ThenInUI(EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchDeletions).Start(); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 0acdfb56b..86cf8c858 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -318,6 +318,11 @@ private void Commit() addTask .FinallyInUI((b, exception) => { + if (b) + { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfCommits(); + } + commitMessage = ""; commitBody = ""; SetBusy(false); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index b2da0f974..658df2a25 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -681,6 +681,8 @@ private void Pull() .FinallyInUI((success, e) => { if (success) { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfPulls(); + EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), Localization.Ok); @@ -703,6 +705,8 @@ private void Push() .FinallyInUI((success, e) => { if (success) { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfPushes(); + EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), Localization.Ok); @@ -722,6 +726,8 @@ private void Fetch() Repository .Fetch() .FinallyInUI((success, e) => { + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfFetches(); + if (!success) { EditorUtility.DisplayDialog(FetchActionTitle, FetchFailureDescription, From 861a27fde2dbcde776276ddbfaaf2ba0504c5426 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 25 Jan 2018 15:07:53 -0500 Subject: [PATCH 002/567] If developing data won't send but it should still save --- src/GitHub.Api/Metrics/UsageTracker.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 9d38d4eaa..b66e7c204 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -71,11 +71,6 @@ private UsageStore LoadUsage() private void SaveUsage(UsageStore store) { - if (!Enabled) - { - return; - } - var pathString = storePath.ToString(); Logger.Trace("SaveUsage: \"{0}\"", pathString); From 481a56e52b2c55454a1a5aa4abf8c77ea883e88e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 25 Jan 2018 16:52:50 -0500 Subject: [PATCH 003/567] Adding CurrentLang --- src/GitHub.Api/Metrics/UsageModel.cs | 1 + src/GitHub.Api/Metrics/UsageTracker.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 2d9011115..7ee9cb670 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -11,6 +11,7 @@ public class Usage public string AppVersion { get; set; } public string UnityVersion { get; set; } public string Lang { get; set; } + public string CurrentLang { get; set; } public int NumberOfStartups { get; set; } public int NumberOfCommits { get; set; } public int NumberOfFetches { get; set; } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index b66e7c204..9f2cd9e61 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -158,6 +158,7 @@ private Usage GetCurrentUsage(UsageStore usageStore) usage.UnityVersion = unityVersion; usage.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; usage.AppVersion = AppConfiguration.AssemblyName.Version.ToString(); + usage.CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag; return usage; } From c8d6db5e2942464de19998ca0f26d2c2a748019c Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 25 Jan 2018 16:53:50 -0500 Subject: [PATCH 004/567] Grouping data records by GitHub for Unity version and Unity version --- src/GitHub.Api/Metrics/UsageModel.cs | 17 ++++++++++++++--- src/GitHub.Api/Metrics/UsageTracker.cs | 4 +--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 7ee9cb670..e057fbac1 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -32,12 +32,18 @@ class UsageModel private Usage currentUsage; - public Usage GetCurrentUsage() + public Usage GetCurrentUsage(string appVersion, string unityVersion) { + Guard.ArgumentNotNullOrWhiteSpace(appVersion, "appVersion"); + Guard.ArgumentNotNullOrWhiteSpace(unityVersion, "unityVersion"); + var date = DateTime.UtcNow.Date; if (currentUsage == null) { - currentUsage = Reports.FirstOrDefault(usage => usage.Date == date); + currentUsage = Reports + .FirstOrDefault(usage => usage.Date == date + && usage.AppVersion == appVersion + && usage.UnityVersion == unityVersion); } if (currentUsage?.Date == date) @@ -48,7 +54,12 @@ public Usage GetCurrentUsage() } else { - currentUsage = new Usage { Date = date, Guid = Guid }; + currentUsage = new Usage { + Date = date, + Guid = Guid, + AppVersion = appVersion, + UnityVersion = unityVersion, + }; Reports.Add(currentUsage); } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 9f2cd9e61..dc624ed6f 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -154,10 +154,8 @@ private async Task SendUsage() private Usage GetCurrentUsage(UsageStore usageStore) { - var usage = usageStore.Model.GetCurrentUsage(); - usage.UnityVersion = unityVersion; + var usage = usageStore.Model.GetCurrentUsage(AppConfiguration.AssemblyName.Version.ToString(), unityVersion); usage.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; - usage.AppVersion = AppConfiguration.AssemblyName.Version.ToString(); usage.CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag; return usage; } From 0f4efef28caaf9c71a99e96a6e355221d66e2fee Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 25 Jan 2018 16:54:09 -0500 Subject: [PATCH 005/567] Adding Unity version to the log file --- src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 51aa84a09..924a9e7f6 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -62,7 +62,7 @@ private static void Initialize() } Logging.LogAdapter = new FileLogAdapter(logPath); - Logging.Info("Initializing GitHub for Unity version " + ApplicationInfo.Version); + Logging.Info("Initializing GitHubForUnity:v{0} Unity:v{1}", ApplicationInfo.Version, Environment.UnityVersion); ApplicationManager.Run(ApplicationCache.Instance.FirstRun); } From c69cf53465d9786d5e6802fb7cbdd0087ba4c603 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 26 Jan 2018 09:26:58 -0500 Subject: [PATCH 006/567] Formatting fix --- .../Assets/Editor/GitHub.Unity/UI/BranchesView.cs | 5 ++++- .../Assets/Editor/GitHub.Unity/UI/HistoryView.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 0dbbd93e2..a8328629b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -478,7 +478,10 @@ private void DeleteLocalBranch(string branch) var dialogMessage = string.Format(DeleteBranchMessageFormatString, branch); if (EditorUtility.DisplayDialog(DeleteBranchTitle, dialogMessage, DeleteBranchButton, CancelButtonLabel)) { - GitClient.DeleteBranch(branch, true).ThenInUI(EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchDeletions).Start(); + GitClient + .DeleteBranch(branch, true) + .ThenInUI(EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchDeletions) + .Start(); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index 658df2a25..abd1a78da 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -681,7 +681,7 @@ private void Pull() .FinallyInUI((success, e) => { if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfPulls(); + EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfPulls(); EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), From 096d1699debdce6f9f6cefbb0467c31428e597b0 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 26 Jan 2018 12:22:21 -0500 Subject: [PATCH 007/567] Adding version for easier parsing --- src/GitHub.Api/Metrics/UsageModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index e057fbac1..6bca9effa 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -8,6 +8,7 @@ public class Usage { public string Guid { get; set; } public DateTime Date { get; set; } + public int Version { get; set; } public string AppVersion { get; set; } public string UnityVersion { get; set; } public string Lang { get; set; } @@ -55,6 +56,7 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) else { currentUsage = new Usage { + Version = 2, Date = date, Guid = Guid, AppVersion = appVersion, From 4eff8ae4b2d49ecf61f8d4870fccaf72fe7a4655 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 31 Jan 2018 09:17:03 -0500 Subject: [PATCH 008/567] Making the log entry a bit more parseable --- src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 924a9e7f6..0d94f8f6d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -62,7 +62,7 @@ private static void Initialize() } Logging.LogAdapter = new FileLogAdapter(logPath); - Logging.Info("Initializing GitHubForUnity:v{0} Unity:v{1}", ApplicationInfo.Version, Environment.UnityVersion); + Logging.Info("Initializing GitHubForUnity:'v{0}' Unity:'v{1}'", ApplicationInfo.Version, Environment.UnityVersion); ApplicationManager.Run(ApplicationCache.Instance.FirstRun); } From 9157a894bc63d4c2e92d4f46a0e199d508f2e7de Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 31 Jan 2018 09:18:26 -0500 Subject: [PATCH 009/567] Removing switch to main thread This code is already running on the main thread --- .../Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index b99f6bf2e..8362ccfd0 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -212,11 +212,9 @@ private void DoResult(bool success, string msg) isBusy = false; - if (success == true) + if (success) { - new ActionTask(CancellationToken.None, () => { EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfAuthentications(); - }) { Affinity = TaskAffinity.UI }.Start(); Clear(); Finish(true); From d36f5ea8c64d2fdb7b9d1a97b44d3a3171c515b3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 31 Jan 2018 09:19:13 -0500 Subject: [PATCH 010/567] Using the Manager member variable --- .../Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/BranchesView.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 8362ccfd0..9061ab05d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -214,7 +214,7 @@ private void DoResult(bool success, string msg) if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfAuthentications(); + Manager.UsageTracker.IncrementNumberOfAuthentications(); Clear(); Finish(true); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index a8328629b..e278e6d22 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -272,7 +272,7 @@ private void OnButtonBarGUI() { if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchCreations(); + Manager.UsageTracker.IncrementNumberOfLocalBranchCreations(); Redraw(); } else @@ -440,7 +440,7 @@ private void CheckoutRemoteBranch(string branch) GitClient.CreateBranch(branchName, branch).FinallyInUI((success, e) => { if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfRemoteBranchCheckouts(); + Manager.UsageTracker.IncrementNumberOfRemoteBranchCheckouts(); Redraw(); } else @@ -461,7 +461,7 @@ private void SwitchBranch(string branch) GitClient.SwitchBranch(branch).FinallyInUI((success, e) => { if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchCheckouts(); + Manager.UsageTracker.IncrementNumberOfLocalBranchCheckouts(); Redraw(); } else @@ -480,7 +480,7 @@ private void DeleteLocalBranch(string branch) { GitClient .DeleteBranch(branch, true) - .ThenInUI(EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfLocalBranchDeletions) + .ThenInUI(Manager.UsageTracker.IncrementNumberOfLocalBranchDeletions) .Start(); } } From 5d15cb81f9d29ec0c99fc7a9f52fabd10598b971 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 12 Apr 2018 10:37:11 -0400 Subject: [PATCH 011/567] Calling CacheHasBeenInvalidated instead of RefreshCache At this point we know these values have already been marked as invalid. There is no reason to call RefreshCache on them and go through that pipeline again. Furthermore that pipeline is currently waiting for valid data, so it will not fire off the invalidation events a second time. --- src/GitHub.Api/Git/Repository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 4101e5caa..b5e4d7da7 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -92,7 +92,7 @@ public void Start() { foreach (var cacheType in cacheInvalidationRequests) { - RefreshCache(cacheType); + CacheHasBeenInvalidated(cacheType); } } From 09d91d3572977c56f3f8f85258d0e21e09fa11d1 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 12 Apr 2018 12:40:39 -0400 Subject: [PATCH 012/567] Don't save the keychain before completing 2FA --- src/GitHub.Api/Authentication/LoginManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index ce0a85ad2..94373ece5 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -84,8 +84,11 @@ public async Task Login( throw new InvalidOperationException("Returned token is null or empty"); } - keychain.SetToken(host, loginResultData.Token); - await keychain.Save(host); + if (loginResultData.Code == LoginResultCodes.Success) + { + keychain.SetToken(host, loginResultData.Token); + await keychain.Save(host); + } return loginResultData; } @@ -106,7 +109,7 @@ public async Task ContinueLogin(LoginResultData loginResultData var host = loginResultData.Host; var keychainAdapter = keychain.Connect(host); var username = keychainAdapter.Credential.Username; - var password = keychainAdapter.Credential.Token; + var password = loginResultData.Token; try { logger.Trace("2FA Continue"); From 80f2ba8c566c6f0e6f9c267761dae9b0c9f3a1f2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 12 Apr 2018 12:40:54 -0400 Subject: [PATCH 013/567] Removing unused LoginAsync method --- src/GitHub.Api/Application/ApiClient.cs | 49 ------------------------ src/GitHub.Api/Application/IApiClient.cs | 1 - 2 files changed, 50 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index e6a35e10d..2ea877a3f 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -131,55 +131,6 @@ public async Task ContinueLogin(LoginResult loginResult, string code) loginResult.Callback(result.Code == LoginResultCodes.Success, result.Message); } - public async Task LoginAsync(string username, string password, Func need2faCode) - { - Guard.ArgumentNotNull(need2faCode, "need2faCode"); - - LoginResultData res = null; - try - { - res = await loginManager.Login(OriginalUrl, username, password); - } - catch (Exception) - { - return false; - } - - if (res.Code == LoginResultCodes.CodeRequired) - { - var resultCache = new LoginResult(res, null, null); - var code = need2faCode(resultCache); - return await ContinueLoginAsync(resultCache, need2faCode, code); - } - else - { - return res.Code == LoginResultCodes.Success; - } - } - - public async Task ContinueLoginAsync(LoginResult loginResult, Func need2faCode, string code) - { - LoginResultData result = null; - try - { - result = await loginManager.ContinueLogin(loginResult.Data, code); - } - catch (Exception) - { - return false; - } - - if (result.Code == LoginResultCodes.CodeFailed) - { - var resultCache = new LoginResult(result, null, null); - code = need2faCode(resultCache); - if (String.IsNullOrEmpty(code)) - return false; - return await ContinueLoginAsync(resultCache, need2faCode, code); - } - return result.Code == LoginResultCodes.Success; - } - private async Task GetCurrentUser() { //TODO: ONE_USER_LOGIN This assumes we only support one login diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index 0ab28ceaf..40ed46ce8 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -12,7 +12,6 @@ Task CreateRepository(string name, string description, bool isPrivate, Task GetOrganizations(Action onSuccess, Action onError = null); Task Login(string username, string password, Action need2faCode, Action result); Task ContinueLogin(LoginResult loginResult, string code); - Task LoginAsync(string username, string password, Func need2faCode); Task Logout(UriString host); Task GetCurrentUser(Action onSuccess, Action onError = null); } From c37f87105397c6212f0516626100f41eec227ebb Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 12 Apr 2018 13:51:05 -0400 Subject: [PATCH 014/567] Bump version to 0.31.7 --- common/SolutionInfo.cs | 2 +- vim.exe.stackdump | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 vim.exe.stackdump diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 93fe06d45..cc531f6dc 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -31,6 +31,6 @@ namespace System { internal static class AssemblyVersionInformation { - internal const string Version = "0.31.6"; + internal const string Version = "0.31.7"; } } diff --git a/vim.exe.stackdump b/vim.exe.stackdump new file mode 100644 index 000000000..9f5fdd5ea --- /dev/null +++ b/vim.exe.stackdump @@ -0,0 +1,19 @@ +Stack trace: +Frame Function Args +00180238000 0018005D19E (00180223639, 00180223C39, 001802342F0, 000FFFFB720) +00180238000 001800463F9 (C0C0C000008080, FF000000808080, FFFF000000FF00, FF00FF000000FF) +00180238000 00180046432 (00180223616, 000000001E7, 001802342F0, 80808000C0C0C0) +00180238000 001800431E3 (00000000000, 00180238000, 7FF9F466D9EE, 001800004EC) +00180238000 0018006B101 (C0C0C000008080, FF000000808080, FFFF000000FF00, FF00FF000000FF) +00180238000 0018006BF4C (00000000000, 0010065D548, 00000000000, 00000000000) +00180238000 0018006E066 (00000000000, 00000000008, 005FCB3E280, 00000000000) +00000000001 00180135376 (0010065D540, 00000000008, 00000000000, 00000000000) +00000000001 0018011C6FB (0010065D540, 00000000008, 00000000000, 00000000000) +00000000001 001004F77E4 (00100575E0A, 00100663D08, 00000000000, 00100663D0C) +00000000001 001005820F3 (00000000008, 00100664140, 00000000000, 00000000000) +00000000001 00100576D86 (000241E9F28, 0005A6893D4, 000241E9F28, 00000010000) +00000000001 001005D4CEB (000FFFFCB98, 001801CA1F0, 00100000001, 00000000001) +0060004AFE0 001005E218A (00000000020, 30001000000FF00, 00180047986, 00180046990) +000FFFFCCB0 001800479F7 (00000000000, 00000000000, 00000000000, 00000000000) +00000000000 00180045663 (00000000000, 00000000000, 00000000000, 00000000000) +End of stack trace (more stack frames may be present) From 86c0b818add278fbec9e7cf37496bdd7a8446631 Mon Sep 17 00:00:00 2001 From: Joel Kuntz Date: Sat, 14 Apr 2018 15:25:44 -0300 Subject: [PATCH 015/567] Add options discussed in #248 --- .editorconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig b/.editorconfig index 1252530c4..cf2bb0aab 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,3 +3,5 @@ root = true [*.cs] indent_style = space indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true From 853cdfb879087a4d512d77f441825f3ded943616 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 16 Apr 2018 18:11:45 +0200 Subject: [PATCH 016/567] Add contact information to the README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 280831465..72813f9fe 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # [GitHub for Unity](https://unity.github.com) -## Notices +The GitHub for Unity extension brings [Git](https://git-scm.com/) and GitHub into [Unity](https://unity3d.com/), integrating source control into your work with friendly and accessible tools and workflows. -From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugins/GitHub`. If you have version 0.18 or lower, you need to delete the `Assets/Editor/GitHub` folder before you install newer versions. You should exit Unity and delete the folder from Explorer/Finder, as Unity will not unload native libraries while it's running. Also, remember to update your `.gitignore` file. +You can reach the team right here by opening a [new issue](https://github.com/github-for-unity/Unity/issues/new), or by joining one of the chats below. You can also email us at unity@github.com, or tweet at [@GitHubUnity](https://twitter.com/GitHubUnity) ![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true) @@ -10,11 +10,12 @@ From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugi [![Join the chat at https://discord.gg/5zH8hVx](https://img.shields.io/badge/discord-join%20chat-7289DA.svg)](https://discord.gg/5zH8hVx) [![GitHub for Unity live coding on Twitch](https://img.shields.io/badge/twitch-live%20coding-6441A4.svg)](https://www.twitch.tv/sh4na) -## About -The GitHub for Unity extension brings [Git](https://git-scm.com/) and GitHub into [Unity](https://unity3d.com/), integrating source control into your work with friendly and accessible tools and workflows. +## Notices + +This software is currently alpha quality. Please refer to the [list of known issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aissue+is%3Aopen+label%3Abug), and make sure you have backups of your work before trying it out. -**Please note:** this software is currently alpha quality. Please refer to the [list of known issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aissue+is%3Aopen+label%3Abug), and make sure you have backups of your work before trying it out. +From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugins/GitHub`. If you have version 0.18 or lower, you need to delete the `Assets/Editor/GitHub` folder before you install newer versions. You should exit Unity and delete the folder from Explorer/Finder, as Unity will not unload native libraries while it's running. Also, remember to update your `.gitignore` file. #### Table Of Contents From 37adad3e36282b085374fc9648e06ed8e38de217 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 16 Apr 2018 18:15:31 +0200 Subject: [PATCH 017/567] Fix link to build badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72813f9fe..64eda3403 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The GitHub for Unity extension brings [Git](https://git-scm.com/) and GitHub int You can reach the team right here by opening a [new issue](https://github.com/github-for-unity/Unity/issues/new), or by joining one of the chats below. You can also email us at unity@github.com, or tweet at [@GitHubUnity](https://twitter.com/GitHubUnity) -![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true) +[![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true)](https://ci.appveyor.com/project/github-windows/unity) [![Join the chat at https://gitter.im/github-for-unity/Unity](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github-for-unity/Unity?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://discord.gg/5zH8hVx](https://img.shields.io/badge/discord-join%20chat-7289DA.svg)](https://discord.gg/5zH8hVx) From 352ec598ad591079e2321456050df28d086696eb Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 16 Apr 2018 17:41:54 -0400 Subject: [PATCH 018/567] Allowing guids to be populated correctly Instead of a dictionary as in the Changes view. Here `guids` is used as a positional marker in the entries array. It should be populated with all entries regardless if they are assets or not. --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index c342f93b3..340992c0d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -183,25 +183,9 @@ private static void OnStatusUpdate() guids.Clear(); for (var index = 0; index < entries.Count; ++index) { - var gitStatusEntry = entries[index]; - - var path = gitStatusEntry.ProjectPath; - if (gitStatusEntry.Status == GitFileStatus.Ignored) - { - continue; - } - - if (!path.StartsWith("Assets", StringComparison.CurrentCultureIgnoreCase)) - { - continue; - } - - if (path.EndsWith(".meta", StringComparison.CurrentCultureIgnoreCase)) - { - continue; - } - + var path = entries[index].ProjectPath; var guid = AssetDatabase.AssetPathToGUID(path); + guids.Add(guid); } From 8fd5932d8c6aaadfc61c14193dd02b11a9218010 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 17 Apr 2018 10:53:45 -0400 Subject: [PATCH 019/567] Sorting GitStatus entries --- src/GitHub.Api/Git/Tasks/GitStatusTask.cs | 2 +- src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs | 8 ++++++-- src/tests/IntegrationTests/ProcessManagerExtensions.cs | 2 +- src/tests/UnitTests/IO/StatusOutputProcessorTests.cs | 2 +- src/tests/UnitTests/ProcessManagerExtensions.cs | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/GitHub.Api/Git/Tasks/GitStatusTask.cs b/src/GitHub.Api/Git/Tasks/GitStatusTask.cs index da66e2c98..f4c524641 100644 --- a/src/GitHub.Api/Git/Tasks/GitStatusTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitStatusTask.cs @@ -8,7 +8,7 @@ class GitStatusTask : ProcessTask public GitStatusTask(IGitObjectFactory gitObjectFactory, CancellationToken token, IOutputProcessor processor = null) - : base(token, processor ?? new StatusOutputProcessor(gitObjectFactory)) + : base(token, processor ?? new GitStatusOutputProcessor(gitObjectFactory)) { Name = TaskName; } diff --git a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs index 1b95daca4..ad1e866d9 100644 --- a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { - class StatusOutputProcessor : BaseOutputProcessor + class GitStatusOutputProcessor : BaseOutputProcessor { private static readonly Regex branchTrackedAndDelta = new Regex(@"(.*)\.\.\.(.*)\s\[(.*)\]", RegexOptions.Compiled); @@ -13,7 +13,7 @@ class StatusOutputProcessor : BaseOutputProcessor private readonly IGitObjectFactory gitObjectFactory; GitStatus gitStatus; - public StatusOutputProcessor(IGitObjectFactory gitObjectFactory) + public GitStatusOutputProcessor(IGitObjectFactory gitObjectFactory) { Guard.ArgumentNotNull(gitObjectFactory, "gitObjectFactory"); this.gitObjectFactory = gitObjectFactory; @@ -190,6 +190,10 @@ private void ReturnStatus() if (gitStatus.Entries == null) return; + gitStatus.Entries = gitStatus.Entries + .OrderBy(entry => entry.Path) + .ToList(); + RaiseOnEntry(gitStatus); gitStatus = new GitStatus(); diff --git a/src/tests/IntegrationTests/ProcessManagerExtensions.cs b/src/tests/IntegrationTests/ProcessManagerExtensions.cs index 8d6b32055..622006c8b 100644 --- a/src/tests/IntegrationTests/ProcessManagerExtensions.cs +++ b/src/tests/IntegrationTests/ProcessManagerExtensions.cs @@ -50,7 +50,7 @@ public static ITask GetGitStatus(this IProcessManager processManager, NPath? gitPath = null) { var gitStatusEntryFactory = new GitObjectFactory(environment); - var processor = new StatusOutputProcessor(gitStatusEntryFactory); + var processor = new GitStatusOutputProcessor(gitStatusEntryFactory); NPath path = gitPath ?? defaultGitPath; diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index a3dce440b..dd6cb7aa5 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -250,7 +250,7 @@ private void AssertProcessOutput(IEnumerable lines, GitStatus expected) var gitObjectFactory = SubstituteFactory.CreateGitObjectFactory(TestRootPath); GitStatus? result = null; - var outputProcessor = new StatusOutputProcessor(gitObjectFactory); + var outputProcessor = new GitStatusOutputProcessor(gitObjectFactory); outputProcessor.OnEntry += status => { result = status; }; foreach (var line in lines) diff --git a/src/tests/UnitTests/ProcessManagerExtensions.cs b/src/tests/UnitTests/ProcessManagerExtensions.cs index 4920d2c1b..c4043551f 100644 --- a/src/tests/UnitTests/ProcessManagerExtensions.cs +++ b/src/tests/UnitTests/ProcessManagerExtensions.cs @@ -58,7 +58,7 @@ public static async Task GetGitStatus(this ProcessManager processMana NPath? gitPath = null) { var gitStatusEntryFactory = new GitObjectFactory(environment); - var processor = new StatusOutputProcessor(gitStatusEntryFactory); + var processor = new GitStatusOutputProcessor(gitStatusEntryFactory); NPath path = gitPath ?? defaultGitPath; From aa3533d359a4eefbfd0025f97b4a487faabc9bef Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 17 Apr 2018 11:11:05 -0400 Subject: [PATCH 020/567] Bump version to 0.31.8 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index cc531f6dc..0b3b14672 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -31,6 +31,6 @@ namespace System { internal static class AssemblyVersionInformation { - internal const string Version = "0.31.7"; + internal const string Version = "0.31.8"; } } From 488512e3fc3be6be01132232727444c39d0ac7d8 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 18 Apr 2018 09:57:48 -0400 Subject: [PATCH 021/567] Removing space --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 340992c0d..d79cb1a95 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -185,7 +185,6 @@ private static void OnStatusUpdate() { var path = entries[index].ProjectPath; var guid = AssetDatabase.AssetPathToGUID(path); - guids.Add(guid); } From 326324124cb33cfee0f1f3c28accf38f601db035 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 18 Apr 2018 10:48:07 -0400 Subject: [PATCH 022/567] Fixing GitStatusOutputProcessorTests --- src/tests/UnitTests/IO/StatusOutputProcessorTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index dd6cb7aa5..da18ec51c 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -6,7 +6,7 @@ namespace UnitTests { [TestFixture] - class StatusOutputProcessorTests : BaseOutputProcessorTests + class GitStatusOutputProcessorTests : BaseOutputProcessorTests { [Test] @@ -28,9 +28,9 @@ public void ShouldParseDirtyWorkingTreeUntracked() LocalBranch = "master", Entries = new List { + new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("GitHubVS.sln", TestRootPath + @"\GitHubVS.sln", null, GitFileStatus.Modified), new GitStatusEntry("README2.md", TestRootPath + @"\README2.md", null, GitFileStatus.Renamed, "README.md", true), - new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("something added.txt", TestRootPath + @"\something added.txt", null, GitFileStatus.Added, staged: true), new GitStatusEntry("something.txt", TestRootPath + @"\something.txt", null, GitFileStatus.Untracked), } @@ -59,9 +59,9 @@ public void ShouldParseDirtyWorkingTreeTrackedAhead1Behind1() Behind = 1, Entries = new List { + new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("GitHubVS.sln", TestRootPath + @"\GitHubVS.sln", null, GitFileStatus.Modified), new GitStatusEntry("README2.md", TestRootPath + @"\README2.md", null, GitFileStatus.Renamed, "README.md", true), - new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("something added.txt", TestRootPath + @"\something added.txt", null, GitFileStatus.Added, staged: true), new GitStatusEntry("something.txt", TestRootPath + @"\something.txt", null, GitFileStatus.Untracked), } @@ -89,9 +89,9 @@ public void ShouldParseDirtyWorkingTreeTrackedAhead1() Ahead = 1, Entries = new List { + new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("GitHubVS.sln", TestRootPath + @"\GitHubVS.sln", null, GitFileStatus.Modified), new GitStatusEntry("README2.md", TestRootPath + @"\README2.md", null, GitFileStatus.Renamed, "README.md", true), - new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("something added.txt", TestRootPath + @"\something added.txt", null, GitFileStatus.Added, staged: true), new GitStatusEntry("something.txt", TestRootPath + @"\something.txt", null, GitFileStatus.Untracked), } @@ -119,9 +119,9 @@ public void ShouldParseDirtyWorkingTreeTrackedBehind1() Behind = 1, Entries = new List { + new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("GitHubVS.sln", TestRootPath + @"\GitHubVS.sln", null, GitFileStatus.Modified), new GitStatusEntry("README2.md", TestRootPath + @"\README2.md", null, GitFileStatus.Renamed, "README.md", true), - new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("something added.txt", TestRootPath + @"\something added.txt", null, GitFileStatus.Added, staged: true), new GitStatusEntry("something.txt", TestRootPath + @"\something.txt", null, GitFileStatus.Untracked), } @@ -148,9 +148,9 @@ public void ShouldParseDirtyWorkingTreeTracked() RemoteBranch = "origin/master", Entries = new List { + new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("GitHubVS.sln", TestRootPath + @"\GitHubVS.sln", null, GitFileStatus.Modified), new GitStatusEntry("README2.md", TestRootPath + @"\README2.md", null, GitFileStatus.Renamed, "README.md", true), - new GitStatusEntry("deploy.cmd", TestRootPath + @"\deploy.cmd", null, GitFileStatus.Deleted), new GitStatusEntry("something added.txt", TestRootPath + @"\something added.txt", null, GitFileStatus.Added, staged: true), new GitStatusEntry("something.txt", TestRootPath + @"\something.txt", null, GitFileStatus.Untracked), } From 944eb70ebebbce37e40546d450b3ede08bde83f1 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 19 Apr 2018 11:50:16 -0400 Subject: [PATCH 023/567] We only need to avoid saving the keychain --- src/GitHub.Api/Authentication/LoginManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 94373ece5..03c7d8bd3 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -84,9 +84,10 @@ public async Task Login( throw new InvalidOperationException("Returned token is null or empty"); } + keychain.SetToken(host, loginResultData.Token); + if (loginResultData.Code == LoginResultCodes.Success) { - keychain.SetToken(host, loginResultData.Token); await keychain.Save(host); } @@ -109,7 +110,7 @@ public async Task ContinueLogin(LoginResultData loginResultData var host = loginResultData.Host; var keychainAdapter = keychain.Connect(host); var username = keychainAdapter.Credential.Username; - var password = loginResultData.Token; + var password = keychainAdapter.Credential.Token; try { logger.Trace("2FA Continue"); From 526ec0826da8e2635366fd2a4323391832f0479e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 19 Apr 2018 12:04:56 -0400 Subject: [PATCH 024/567] Fixing error after merge --- src/GitHub.Api/Metrics/UsageTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index f9370bfd7..aa734d45e 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -154,7 +154,7 @@ private async Task SendUsage() private Usage GetCurrentUsage(UsageStore usageStore) { - var usage = usageStore.Model.GetCurrentUsage(AppConfiguration.AssemblyName.Version.ToString(), unityVersion); + var usage = usageStore.Model.GetCurrentUsage(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); usage.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; usage.CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag; return usage; From 6af8afd5118013c91aa029a04474796e6e372396 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 19 Apr 2018 15:17:44 -0400 Subject: [PATCH 025/567] Restoring functionality to revert a commit --- .../Editor/GitHub.Unity/UI/HistoryView.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index 31cb30ab0..4a907a24e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -459,8 +459,13 @@ public override void OnGUI() selectedEntry = entry; BuildTree(); }, - entry => { }, - entry => { }); + entry => { }, entry => { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Revert"), false, RevertCommit); + menu.ShowAsContext(); + + Event.current.Use(); + }); if (requiresRepaint) Redraw(); @@ -546,6 +551,26 @@ private void HistoryDetailsEntry(GitLogEntry entry) GUILayout.EndVertical(); } + private void RevertCommit() + { + var dialogTitle = "Revert commit"; + var dialogBody = string.Format(@"Are you sure you want to revert the following commit:""{0}""", selectedEntry.Summary); + + if (EditorUtility.DisplayDialog(dialogTitle, dialogBody, "Revert", "Cancel")) + { + Repository + .Revert(selectedEntry.CommitID) + .FinallyInUI((success, e) => { + if (!success) + { + EditorUtility.DisplayDialog(dialogTitle, + "Error reverting commit: " + e.Message, Localization.Cancel); + } + }) + .Start(); + } + } + private void RepositoryTrackingOnStatusChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastAheadBehindChangedEvent.Equals(cacheUpdateEvent)) From f43ad033facd4d02781439fdcae8b83fe9c15db7 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 19 Apr 2018 15:41:28 -0400 Subject: [PATCH 026/567] Removing event use --- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index 4a907a24e..13e69b802 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -463,8 +463,6 @@ public override void OnGUI() GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent("Revert"), false, RevertCommit); menu.ShowAsContext(); - - Event.current.Use(); }); if (requiresRepaint) From 2e7d9f6bbbc35b707f94d54dde4bb7f5426aa760 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 20 Apr 2018 11:37:10 +0200 Subject: [PATCH 027/567] Take out the trash --- .gitignore | 3 ++- vim.exe.stackdump | 19 ------------------- 2 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 vim.exe.stackdump diff --git a/.gitignore b/.gitignore index bcd8c5dcd..eeacaff9b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ _NCrunch_GitHub.Unity .DS_Store build/ TestResult.xml -submodules/ \ No newline at end of file +submodules/ +*.stackdump \ No newline at end of file diff --git a/vim.exe.stackdump b/vim.exe.stackdump deleted file mode 100644 index 9f5fdd5ea..000000000 --- a/vim.exe.stackdump +++ /dev/null @@ -1,19 +0,0 @@ -Stack trace: -Frame Function Args -00180238000 0018005D19E (00180223639, 00180223C39, 001802342F0, 000FFFFB720) -00180238000 001800463F9 (C0C0C000008080, FF000000808080, FFFF000000FF00, FF00FF000000FF) -00180238000 00180046432 (00180223616, 000000001E7, 001802342F0, 80808000C0C0C0) -00180238000 001800431E3 (00000000000, 00180238000, 7FF9F466D9EE, 001800004EC) -00180238000 0018006B101 (C0C0C000008080, FF000000808080, FFFF000000FF00, FF00FF000000FF) -00180238000 0018006BF4C (00000000000, 0010065D548, 00000000000, 00000000000) -00180238000 0018006E066 (00000000000, 00000000008, 005FCB3E280, 00000000000) -00000000001 00180135376 (0010065D540, 00000000008, 00000000000, 00000000000) -00000000001 0018011C6FB (0010065D540, 00000000008, 00000000000, 00000000000) -00000000001 001004F77E4 (00100575E0A, 00100663D08, 00000000000, 00100663D0C) -00000000001 001005820F3 (00000000008, 00100664140, 00000000000, 00000000000) -00000000001 00100576D86 (000241E9F28, 0005A6893D4, 000241E9F28, 00000010000) -00000000001 001005D4CEB (000FFFFCB98, 001801CA1F0, 00100000001, 00000000001) -0060004AFE0 001005E218A (00000000020, 30001000000FF00, 00180047986, 00180046990) -000FFFFCCB0 001800479F7 (00000000000, 00000000000, 00000000000, 00000000000) -00000000000 00180045663 (00000000000, 00000000000, 00000000000, 00000000000) -End of stack trace (more stack frames may be present) From 85d0a919e9d46e6c8ce6324dc78f0b69321d68fd Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 20 Apr 2018 10:32:37 -0400 Subject: [PATCH 028/567] Writing the commit message to a file before processing (#703) * Writing the commit message to a file before processing * Reducing repetition * Using string interpolation for clarity * Using nameof for sanity * Using NPath for sanity * Cleaning up after ourselves * Writing the message to a file in the GitCommitTask * Tasks shouldn't do anything after OnEnd is raised --- src/GitHub.Api/Git/Tasks/GitCommitTask.cs | 26 +++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Git/Tasks/GitCommitTask.cs b/src/GitHub.Api/Git/Tasks/GitCommitTask.cs index b6d17809b..b00f09b83 100644 --- a/src/GitHub.Api/Git/Tasks/GitCommitTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCommitTask.cs @@ -6,19 +6,37 @@ namespace GitHub.Unity class GitCommitTask : ProcessTask { private const string TaskName = "git commit"; + + private readonly string message; + private readonly string body; private readonly string arguments; + private NPath tempFile; + public GitCommitTask(string message, string body, CancellationToken token, IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) { Guard.ArgumentNotNullOrWhiteSpace(message, "message"); + this.message = message; + this.body = body ?? string.Empty; + Name = TaskName; - arguments = "-c i18n.commitencoding=utf8 commit "; - arguments += String.Format(" -m \"{0}\"", message); - if (!String.IsNullOrEmpty(body)) - arguments += String.Format(" -m \"{0}\"", body); + tempFile = NPath.GetTempFilename("GitCommitTask"); + arguments = $"-c i18n.commitencoding=utf8 commit --file \"{tempFile}\""; + } + + protected override void RaiseOnStart() + { + base.RaiseOnStart(); + tempFile.WriteAllLines(new [] { message, Environment.NewLine, body }); + } + + protected override void RaiseOnEnd() + { + tempFile.DeleteIfExists(); + base.RaiseOnEnd(); } public override string ProcessArguments { get { return arguments; } } From addbcf57f7a88c474d8ebf773cd9f631241ef9c1 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 20 Apr 2018 12:33:37 -0400 Subject: [PATCH 029/567] Allow login with email (#690) * Validating the authentication result in order to ensure that we have the username * Removing the UpdateToken method because it is never used * Making username required in SetToken * Centralize function to retrieve username * Getting username for regular logins as well * Removing unused fields * Making username required * Fixing unit test --- src/GitHub.Api/Application/ApiClient.cs | 2 +- src/GitHub.Api/Authentication/Credential.cs | 3 +- .../Authentication/ICredentialManager.cs | 2 +- src/GitHub.Api/Authentication/IKeychain.cs | 3 +- src/GitHub.Api/Authentication/Keychain.cs | 37 +++++++++-------- .../Authentication/KeychainAdapter.cs | 4 +- src/GitHub.Api/Authentication/LoginManager.cs | 41 +++++++++++-------- .../UnitTests/Authentication/KeychainTests.cs | 2 +- 8 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 2ea877a3f..067c05c76 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -33,7 +33,7 @@ public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processM this.taskManager = taskManager; this.nodeJsExecutablePath = nodeJsExecutablePath; this.octorunScriptPath = octorunScriptPath; - loginManager = new LoginManager(keychain, ApplicationInfo.ClientId, ApplicationInfo.ClientSecret, + loginManager = new LoginManager(keychain, processManager: processManager, taskManager: taskManager, nodeJsExecutablePath: nodeJsExecutablePath, diff --git a/src/GitHub.Api/Authentication/Credential.cs b/src/GitHub.Api/Authentication/Credential.cs index f7c74d3a5..2e31f9838 100644 --- a/src/GitHub.Api/Authentication/Credential.cs +++ b/src/GitHub.Api/Authentication/Credential.cs @@ -16,9 +16,10 @@ public Credential(UriString host, string username, string token) this.Token = token; } - public void UpdateToken(string token) + public void UpdateToken(string token, string username) { this.Token = token; + this.Username = username; } public UriString Host { get; private set; } diff --git a/src/GitHub.Api/Authentication/ICredentialManager.cs b/src/GitHub.Api/Authentication/ICredentialManager.cs index 28742a6c7..b601d3633 100644 --- a/src/GitHub.Api/Authentication/ICredentialManager.cs +++ b/src/GitHub.Api/Authentication/ICredentialManager.cs @@ -8,7 +8,7 @@ public interface ICredential : IDisposable UriString Host { get; } string Username { get; } string Token { get; } - void UpdateToken(string token); + void UpdateToken(string token, string username); } public interface ICredentialManager diff --git a/src/GitHub.Api/Authentication/IKeychain.cs b/src/GitHub.Api/Authentication/IKeychain.cs index 4aa1bcb97..a05a748dc 100644 --- a/src/GitHub.Api/Authentication/IKeychain.cs +++ b/src/GitHub.Api/Authentication/IKeychain.cs @@ -10,13 +10,12 @@ public interface IKeychain Task Load(UriString host); Task Clear(UriString host, bool deleteFromCredentialManager); Task Save(UriString host); - void UpdateToken(UriString host, string token); void SetCredentials(ICredential credential); void Initialize(); Connection[] Connections { get; } IList Hosts { get; } bool HasKeys { get; } - void SetToken(UriString host, string token); + void SetToken(UriString host, string token, string username); event Action ConnectionsChanged; } diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index 2b3c933e2..77289a5ba 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -95,11 +95,15 @@ public Keychain(IEnvironment environment, ICredentialManager credentialManager) public IKeychainAdapter Connect(UriString host) { + Guard.ArgumentNotNull(host, nameof(host)); + return FindOrCreateAdapter(host); } public async Task Load(UriString host) { + Guard.ArgumentNotNull(host, nameof(host)); + var keychainAdapter = FindOrCreateAdapter(host); var connection = GetConnection(host); @@ -145,6 +149,9 @@ public void Initialize() public async Task Clear(UriString host, bool deleteFromCredentialManager) { //logger.Trace("Clear Host:{0}", host); + + Guard.ArgumentNotNull(host, nameof(host)); + //clear octokit credentials await RemoveCredential(host, deleteFromCredentialManager); RemoveConnection(host); @@ -153,6 +160,9 @@ public async Task Clear(UriString host, bool deleteFromCredentialManager) public async Task Save(UriString host) { //logger.Trace("Save: {0}", host); + + Guard.ArgumentNotNull(host, nameof(host)); + var keychainAdapter = await AddCredential(host); AddConnection(new Connection(host, keychainAdapter.Credential.Username)); } @@ -160,23 +170,23 @@ public async Task Save(UriString host) public void SetCredentials(ICredential credential) { //logger.Trace("SetCredentials Host:{0}", credential.Host); + + Guard.ArgumentNotNull(credential, nameof(credential)); + var keychainAdapter = GetKeychainAdapter(credential.Host); keychainAdapter.Set(credential); } - public void SetToken(UriString host, string token) + public void SetToken(UriString host, string token, string username) { //logger.Trace("SetToken Host:{0}", host); - var keychainAdapter = GetKeychainAdapter(host); - keychainAdapter.UpdateToken(token); - } - public void UpdateToken(UriString host, string token) - { - //logger.Trace("UpdateToken Host:{0}", host); + Guard.ArgumentNotNull(host, nameof(host)); + Guard.ArgumentNotNull(token, nameof(token)); + Guard.ArgumentNotNull(username, nameof(username)); + var keychainAdapter = GetKeychainAdapter(host); - var keychainItem = keychainAdapter.Credential; - keychainItem.UpdateToken(token); + keychainAdapter.UpdateToken(token, username); } private void LoadConnectionsFromDisk() @@ -290,15 +300,6 @@ private void RemoveConnection(UriString host) } } - private void RemoveAllConnections() - { - if (connections.Count > 0) - { - connections.Clear(); - SaveConnectionsToDisk(); - } - } - private void UpdateConnections(Connection[] conns) { var updated = false; diff --git a/src/GitHub.Api/Authentication/KeychainAdapter.cs b/src/GitHub.Api/Authentication/KeychainAdapter.cs index 3fdde60b3..abbe9895e 100644 --- a/src/GitHub.Api/Authentication/KeychainAdapter.cs +++ b/src/GitHub.Api/Authentication/KeychainAdapter.cs @@ -9,9 +9,9 @@ public void Set(ICredential credential) Credential = credential; } - public void UpdateToken(string token) + public void UpdateToken(string token, string username) { - Credential.UpdateToken(token); + Credential.UpdateToken(token, username); } public void Clear() diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 03c7d8bd3..0dfa49a9e 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using System.Net; using System.Threading.Tasks; using GitHub.Logging; @@ -23,8 +21,6 @@ class LoginManager : ILoginManager private readonly ILogging logger = LogHelper.GetLogger(); private readonly IKeychain keychain; - private readonly string clientId; - private readonly string clientSecret; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; private readonly NPath? nodeJsExecutablePath; @@ -34,25 +30,17 @@ class LoginManager : ILoginManager /// Initializes a new instance of the class. /// /// - /// The application's client API ID. - /// The application's client API secret. /// /// /// /// public LoginManager( - IKeychain keychain, - string clientId, - string clientSecret, - IProcessManager processManager = null, ITaskManager taskManager = null, NPath? nodeJsExecutablePath = null, NPath? octorunScript = null) + IKeychain keychain, IProcessManager processManager = null, ITaskManager taskManager = null, + NPath? nodeJsExecutablePath = null, NPath? octorunScript = null) { Guard.ArgumentNotNull(keychain, nameof(keychain)); - Guard.ArgumentNotNullOrWhiteSpace(clientId, nameof(clientId)); - Guard.ArgumentNotNullOrWhiteSpace(clientSecret, nameof(clientSecret)); this.keychain = keychain; - this.clientId = clientId; - this.clientSecret = clientSecret; this.processManager = processManager; this.taskManager = taskManager; this.nodeJsExecutablePath = nodeJsExecutablePath; @@ -84,7 +72,8 @@ public async Task Login( throw new InvalidOperationException("Returned token is null or empty"); } - keychain.SetToken(host, loginResultData.Token); + username = await RetrieveUsername(loginResultData, username); + keychain.SetToken(host, loginResultData.Token, username); if (loginResultData.Code == LoginResultCodes.Success) { @@ -123,7 +112,8 @@ public async Task ContinueLogin(LoginResultData loginResultData throw new InvalidOperationException("Returned token is null or empty"); } - keychain.SetToken(host, loginResultData.Token); + username = await RetrieveUsername(loginResultData, username); + keychain.SetToken(host, loginResultData.Token, username); await keychain.Save(host); return loginResultData; @@ -199,6 +189,25 @@ private async Task TryLogin( return new LoginResultData(LoginResultCodes.Failed, ret.GetApiErrorMessage() ?? "Failed.", host); } + + private async Task RetrieveUsername(LoginResultData loginResultData, string username) + { + if (!username.Contains("@")) + { + return username; + } + + var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, "validate", + user: username, userToken: loginResultData.Token).Configure(processManager); + + var validateResult = await octorunTask.StartAsAsync(); + if (!validateResult.IsSuccess) + { + throw new InvalidOperationException("Authentication validation failed"); + } + + return validateResult.Output[1]; + } } class LoginResultData diff --git a/src/tests/UnitTests/Authentication/KeychainTests.cs b/src/tests/UnitTests/Authentication/KeychainTests.cs index ba5244abd..23786626b 100644 --- a/src/tests/UnitTests/Authentication/KeychainTests.cs +++ b/src/tests/UnitTests/Authentication/KeychainTests.cs @@ -292,7 +292,7 @@ public void ShouldConnectSetCredentialsTokenAndSave() keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(password); - keychain.SetToken(hostUri, token); + keychain.SetToken(hostUri, token, username); keychainAdapter.Credential.Should().NotBeNull(); keychainAdapter.Credential.Host.Should().Be(hostUri); From 4d09872f21c5791d4fcbee65d325fe188a65da08 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 20 Apr 2018 12:43:21 -0400 Subject: [PATCH 030/567] Bump version to 0.32.0 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 0b3b14672..c809d7145 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -31,6 +31,6 @@ namespace System { internal static class AssemblyVersionInformation { - internal const string Version = "0.31.8"; + internal const string Version = "0.32.0"; } } From c2a1483b33c2f9f5ee908e5c5c396cfd9ba1b14f Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 20 Apr 2018 16:15:01 -0400 Subject: [PATCH 031/567] Avoiding the update of the username in we still need 2fa --- src/GitHub.Api/Authentication/LoginManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 0dfa49a9e..608c99383 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -72,7 +72,11 @@ public async Task Login( throw new InvalidOperationException("Returned token is null or empty"); } - username = await RetrieveUsername(loginResultData, username); + if (loginResultData.Code == LoginResultCodes.Success) + { + username = await RetrieveUsername(loginResultData, username); + } + keychain.SetToken(host, loginResultData.Token, username); if (loginResultData.Code == LoginResultCodes.Success) From 624754152f554d86e8ca7fe2821101de6c20d8e1 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 23 Apr 2018 10:06:11 -0400 Subject: [PATCH 032/567] Updating the UsageModel --- src/GitHub.Api/Metrics/UsageModel.cs | 41 ++++++++++++++-------- src/GitHub.Api/Metrics/UsageTracker.cs | 48 +++++++++++++------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 6bca9effa..67b053193 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -5,14 +5,23 @@ namespace GitHub.Unity { public class Usage + { + public Dimensions Dimensions { get; set; } = new Dimensions(); + public Measures Measures { get; set; } = new Measures(); + } + + public class Dimensions { public string Guid { get; set; } public DateTime Date { get; set; } - public int Version { get; set; } public string AppVersion { get; set; } public string UnityVersion { get; set; } public string Lang { get; set; } public string CurrentLang { get; set; } + } + + public class Measures + { public int NumberOfStartups { get; set; } public int NumberOfCommits { get; set; } public int NumberOfFetches { get; set; } @@ -42,25 +51,27 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) if (currentUsage == null) { currentUsage = Reports - .FirstOrDefault(usage => usage.Date == date - && usage.AppVersion == appVersion - && usage.UnityVersion == unityVersion); + .FirstOrDefault(usage => usage.Dimensions.Date == date + && usage.Dimensions.AppVersion == appVersion + && usage.Dimensions.UnityVersion == unityVersion); } - if (currentUsage?.Date == date) + if (currentUsage?.Dimensions.Date == date) { // update any fields that might be missing, if we've changed the format - if (currentUsage.Guid != Guid) - currentUsage.Guid = Guid; + if (currentUsage.Dimensions.Guid != Guid) + currentUsage.Dimensions.Guid = Guid; } else { - currentUsage = new Usage { - Version = 2, - Date = date, - Guid = Guid, - AppVersion = appVersion, - UnityVersion = unityVersion, + currentUsage = new Usage + { + Dimensions = { + Date = date, + Guid = Guid, + AppVersion = appVersion, + UnityVersion = unityVersion + } }; Reports.Add(currentUsage); } @@ -70,12 +81,12 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) public List SelectReports(DateTime beforeDate) { - return Reports.Where(usage => usage.Date.Date != beforeDate.Date).ToList(); + return Reports.Where(usage => usage.Dimensions.Date.Date != beforeDate.Date).ToList(); } public void RemoveReports(DateTime beforeDate) { - Reports.RemoveAll(usage => usage.Date.Date != beforeDate.Date); + Reports.RemoveAll(usage => usage.Dimensions.Date.Date != beforeDate.Date); } } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index aa734d45e..2bc9ce582 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -155,8 +155,8 @@ private async Task SendUsage() private Usage GetCurrentUsage(UsageStore usageStore) { var usage = usageStore.Model.GetCurrentUsage(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); - usage.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; - usage.CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag; + usage.Dimensions.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; + usage.Dimensions.CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag; return usage; } @@ -165,8 +165,8 @@ public void IncrementNumberOfStartups() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfStartups++; - Logger.Trace("NumberOfStartups:{0} Date:{1}", usage.NumberOfStartups, usage.Date); + usage.Measures.NumberOfStartups++; + Logger.Trace("NumberOfStartups:{0} Date:{1}", usage.Measures.NumberOfStartups, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -176,8 +176,8 @@ public void IncrementNumberOfCommits() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfCommits++; - Logger.Trace("NumberOfCommits:{0} Date:{1}", usage.NumberOfCommits, usage.Date); + usage.Measures.NumberOfCommits++; + Logger.Trace("NumberOfCommits:{0} Date:{1}", usage.Measures.NumberOfCommits, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -187,8 +187,8 @@ public void IncrementNumberOfFetches() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfFetches++; - Logger.Trace("NumberOfFetches:{0} Date:{1}", usage.NumberOfFetches, usage.Date); + usage.Measures.NumberOfFetches++; + Logger.Trace("NumberOfFetches:{0} Date:{1}", usage.Measures.NumberOfFetches, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -198,8 +198,8 @@ public void IncrementNumberOfPushes() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfPushes++; - Logger.Trace("NumberOfPushes:{0} Date:{1}", usage.NumberOfPushes, usage.Date); + usage.Measures.NumberOfPushes++; + Logger.Trace("NumberOfPushes:{0} Date:{1}", usage.Measures.NumberOfPushes, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -209,8 +209,8 @@ public void IncrementNumberOfProjectsInitialized() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfProjectsInitialized++; - Logger.Trace("NumberOfProjectsInitialized:{0} Date:{1}", usage.NumberOfProjectsInitialized, usage.Date); + usage.Measures.NumberOfProjectsInitialized++; + Logger.Trace("NumberOfProjectsInitialized:{0} Date:{1}", usage.Measures.NumberOfProjectsInitialized, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -220,8 +220,8 @@ public void IncrementNumberOfLocalBranchCreations() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfLocalBranchCreations++; - Logger.Trace("NumberOfLocalBranchCreations:{0} Date:{1}", usage.NumberOfLocalBranchCreations, usage.Date); + usage.Measures.NumberOfLocalBranchCreations++; + Logger.Trace("NumberOfLocalBranchCreations:{0} Date:{1}", usage.Measures.NumberOfLocalBranchCreations, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -231,8 +231,8 @@ public void IncrementNumberOfLocalBranchDeletions() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfLocalBranchDeletion++; - Logger.Trace("NumberOfLocalBranchDeletion:{0} Date:{1}", usage.NumberOfLocalBranchDeletion, usage.Date); + usage.Measures.NumberOfLocalBranchDeletion++; + Logger.Trace("NumberOfLocalBranchDeletion:{0} Date:{1}", usage.Measures.NumberOfLocalBranchDeletion, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -242,8 +242,8 @@ public void IncrementNumberOfLocalBranchCheckouts() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfLocalBranchCheckouts++; - Logger.Trace("NumberOfLocalBranchCheckouts:{0} Date:{1}", usage.NumberOfLocalBranchCheckouts, usage.Date); + usage.Measures.NumberOfLocalBranchCheckouts++; + Logger.Trace("NumberOfLocalBranchCheckouts:{0} Date:{1}", usage.Measures.NumberOfLocalBranchCheckouts, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -253,8 +253,8 @@ public void IncrementNumberOfRemoteBranchCheckouts() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfRemoteBranchCheckouts++; - Logger.Trace("NumberOfRemoteBranchCheckouts:{0} Date:{1}", usage.NumberOfRemoteBranchCheckouts, usage.Date); + usage.Measures.NumberOfRemoteBranchCheckouts++; + Logger.Trace("NumberOfRemoteBranchCheckouts:{0} Date:{1}", usage.Measures.NumberOfRemoteBranchCheckouts, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -264,8 +264,8 @@ public void IncrementNumberOfPulls() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfPulls++; - Logger.Trace("NumberOfPulls:{0} Date:{1}", usage.NumberOfPulls, usage.Date); + usage.Measures.NumberOfPulls++; + Logger.Trace("NumberOfPulls:{0} Date:{1}", usage.Measures.NumberOfPulls, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -275,8 +275,8 @@ public void IncrementNumberOfAuthentications() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.NumberOfAuthentications++; - Logger.Trace("NumberOfAuthentications:{0} Date:{1}", usage.NumberOfAuthentications, usage.Date); + usage.Measures.NumberOfAuthentications++; + Logger.Trace("NumberOfAuthentications:{0} Date:{1}", usage.Measures.NumberOfAuthentications, usage.Dimensions.Date); SaveUsage(usageStore); } From 5388c25bbe79bffc49c047d8e2fa50217f2145f4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 23 Apr 2018 11:05:43 -0400 Subject: [PATCH 033/567] Fix to cache updating After we get the invalidation event form the cache, the cache will not send another invalidation event until it gets a new data set. User should retrieve the data after it has been initialized if the cache has previously signaled the data is invalid. --- src/GitHub.Api/Git/Repository.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index b5e4d7da7..34ac47be5 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -409,7 +409,10 @@ public void Initialize(IGitClient client) Guard.ArgumentNotNull(client, nameof(client)); gitClient = client; if (needsRefresh) - cacheContainer.GitUserCache.InvalidateData(); + { + needsRefresh = false; + UpdateUserAndEmail(); + } } public void SetNameAndEmail(string name, string email) From 9ac303a3fab729a2953291d2b077fe876e33c11a Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 24 Apr 2018 14:46:13 -0400 Subject: [PATCH 034/567] Clearing the keychain before we remove the credential (#707) * Clearing the keychain before we remove the credential --- src/GitHub.Api/Authentication/Keychain.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index 77289a5ba..ed169b7e5 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -152,9 +152,10 @@ public async Task Clear(UriString host, bool deleteFromCredentialManager) Guard.ArgumentNotNull(host, nameof(host)); + RemoveConnection(host); + //clear octokit credentials await RemoveCredential(host, deleteFromCredentialManager); - RemoveConnection(host); } public async Task Save(UriString host) From 532bfe04d888101db17ec7d7b0aeb30d11496a57 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 24 Apr 2018 14:59:36 -0400 Subject: [PATCH 035/567] We should call the same method that the event handler does --- src/GitHub.Api/Git/Repository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 34ac47be5..e4828db4c 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -411,7 +411,7 @@ public void Initialize(IGitClient client) if (needsRefresh) { needsRefresh = false; - UpdateUserAndEmail(); + GitUserCacheOnCacheInvalidated(); } } From 430f0ba4d781c3ce68096238c9135572b919bd44 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 11:30:18 -0400 Subject: [PATCH 036/567] Changing default file name --- src/GitHub.Api/Helpers/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Helpers/Constants.cs b/src/GitHub.Api/Helpers/Constants.cs index 7f064b382..9cb14fb2a 100644 --- a/src/GitHub.Api/Helpers/Constants.cs +++ b/src/GitHub.Api/Helpers/Constants.cs @@ -6,7 +6,7 @@ static class Constants { public const string GuidKey = "Guid"; public const string MetricsKey = "MetricsEnabled"; - public const string UsageFile = "usage.json"; + public const string UsageFile = "metrics.json"; public const string GitInstallPathKey = "GitInstallPath"; public const string TraceLoggingKey = "EnableTraceLogging"; public const string WebTimeoutKey = "WebTimeout"; From b69500eaff6bcba13d8aacdaa2554c54963e99a4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 13:09:26 -0400 Subject: [PATCH 037/567] Calling UsageTracker from other threads --- .../Application/ApplicationManagerBase.cs | 4 +- src/GitHub.Api/Tasks/ITaskManager.cs | 1 + src/GitHub.Api/Tasks/TaskManager.cs | 5 ++ .../GitHub.Unity/UI/AuthenticationView.cs | 2 +- .../Editor/GitHub.Unity/UI/BranchesView.cs | 52 ++++++++++--------- .../Editor/GitHub.Unity/UI/ChangesView.cs | 6 +-- .../Editor/GitHub.Unity/UI/HistoryView.cs | 9 ++-- .../Assets/Editor/GitHub.Unity/UI/Subview.cs | 1 + 8 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 75d918573..c1fc927cc 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -166,7 +166,7 @@ public ITask InitializeRepository() Environment.InitializeRepository(); RestartRepository(); }) - .ThenInUI(UsageTracker.IncrementNumberOfProjectsInitialized) + .Then(UsageTracker.IncrementNumberOfProjectsInitialized) .ThenInUI(InitializeUI); return task; } @@ -213,7 +213,7 @@ protected void SetupMetrics(string unityVersion, bool firstRun) if (firstRun) { - UsageTracker.IncrementNumberOfStartups(); + TaskManager.Run(UsageTracker.IncrementNumberOfStartups); } #endif } diff --git a/src/GitHub.Api/Tasks/ITaskManager.cs b/src/GitHub.Api/Tasks/ITaskManager.cs index 03b01469d..27a4c044f 100644 --- a/src/GitHub.Api/Tasks/ITaskManager.cs +++ b/src/GitHub.Api/Tasks/ITaskManager.cs @@ -13,6 +13,7 @@ public interface ITaskManager : IDisposable T Schedule(T task) where T : ITask; Task Wait(); + ITask Run(Action action); ITask RunInUI(Action action); } } \ No newline at end of file diff --git a/src/GitHub.Api/Tasks/TaskManager.cs b/src/GitHub.Api/Tasks/TaskManager.cs index b89d784a7..560540faf 100644 --- a/src/GitHub.Api/Tasks/TaskManager.cs +++ b/src/GitHub.Api/Tasks/TaskManager.cs @@ -51,6 +51,11 @@ public static TaskScheduler GetScheduler(TaskAffinity affinity) } } + public ITask Run(Action action) + { + return new ActionTask(Token, action).Start(); + } + public ITask RunInUI(Action action) { return new ActionTask(Token, action) { Affinity = TaskAffinity.UI }.Start(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index b177feb09..265ef73e6 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -214,7 +214,7 @@ private void DoResult(bool success, string msg) if (success) { - Manager.UsageTracker.IncrementNumberOfAuthentications(); + TaskManager.Run(UsageTracker.IncrementNumberOfAuthentications); Clear(); Finish(true); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 3a4e1048b..4d1344bf3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -305,11 +305,11 @@ private void OnButtonBarGUI() if (createBranch) { GitClient.CreateBranch(newBranchName, treeLocals.SelectedNode.Path) + .Then(UsageTracker.IncrementNumberOfLocalBranchCreations) .FinallyInUI((success, e) => { if (success) { - Manager.UsageTracker.IncrementNumberOfLocalBranchCreations(); Redraw(); } else @@ -474,18 +474,20 @@ private void CheckoutRemoteBranch(string branch) if (confirmCheckout) { - GitClient.CreateBranch(branchName, branch).FinallyInUI((success, e) => { - if (success) + GitClient.CreateBranch(branchName, branch) + .Then(UsageTracker.IncrementNumberOfRemoteBranchCheckouts) + .FinallyInUI((success, e) => { - Manager.UsageTracker.IncrementNumberOfRemoteBranchCheckouts(); - Redraw(); - } - else - { - EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, - String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); - } - }).Start(); + if (success) + { + Redraw(); + } + else + { + EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, + String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); + } + }).Start(); } } } @@ -495,18 +497,20 @@ private void SwitchBranch(string branch) if (EditorUtility.DisplayDialog(ConfirmSwitchTitle, String.Format(ConfirmSwitchMessage, branch), ConfirmSwitchOK, ConfirmSwitchCancel)) { - GitClient.SwitchBranch(branch).FinallyInUI((success, e) => { - if (success) + GitClient.SwitchBranch(branch) + .Then(UsageTracker.IncrementNumberOfLocalBranchCheckouts) + .FinallyInUI((success, e) => { - Manager.UsageTracker.IncrementNumberOfLocalBranchCheckouts(); - Redraw(); - } - else - { - EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, - String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); - } - }).Start(); + if (success) + { + Redraw(); + } + else + { + EditorUtility.DisplayDialog(Localization.SwitchBranchTitle, + String.Format(Localization.SwitchBranchFailedDescription, branch), Localization.Ok); + } + }).Start(); } } @@ -517,7 +521,7 @@ private void DeleteLocalBranch(string branch) { GitClient .DeleteBranch(branch, true) - .ThenInUI(Manager.UsageTracker.IncrementNumberOfLocalBranchDeletions) + .Then(UsageTracker.IncrementNumberOfLocalBranchDeletions) .Start(); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 8065de28f..b6b2be3df 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -353,13 +353,9 @@ private void Commit() } addTask + .Then(UsageTracker.IncrementNumberOfCommits) .FinallyInUI((b, exception) => { - if (b) - { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfCommits(); - } - commitMessage = ""; commitBody = ""; }).Start(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index b997a02c9..e46d5af2a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -702,11 +702,10 @@ private void Pull() // (either git rebase --abort or git merge --abort) } }, runOptions: TaskRunOptions.OnAlways) + .Then(UsageTracker.IncrementNumberOfPulls) .FinallyInUI((success, e) => { if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfPulls(); - EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), Localization.Ok); @@ -726,11 +725,10 @@ private void Push() { Repository .Push() + .Then(UsageTracker.IncrementNumberOfPushes) .FinallyInUI((success, e) => { if (success) { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfPushes(); - EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), Localization.Ok); @@ -749,9 +747,8 @@ private void Fetch() { Repository .Fetch() + .Then(UsageTracker.IncrementNumberOfFetches) .FinallyInUI((success, e) => { - EntryPoint.ApplicationManager.UsageTracker.IncrementNumberOfFetches(); - if (!success) { EditorUtility.DisplayDialog(FetchActionTitle, FetchFailureDescription, diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index 35af289e7..4d557d219 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -68,6 +68,7 @@ public virtual bool IsBusy protected IGitClient GitClient { get { return Manager.GitClient; } } protected IEnvironment Environment { get { return Manager.Environment; } } protected IPlatform Platform { get { return Manager.Platform; } } + protected IUsageTracker UsageTracker { get { return Manager.UsageTracker; } } public Rect Position { get { return Parent.Position; } } public string Title { get; protected set; } public Vector2 Size { get; protected set; } From 71a7e3ea0abb2e2cb87b6eb01f58ecab845d7d79 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 13:33:09 -0400 Subject: [PATCH 038/567] Making calls to UsageTracker independant of the task execution chain --- .../Application/ApplicationManagerBase.cs | 7 +++++-- .../Editor/GitHub.Unity/UI/BranchesView.cs | 17 +++++++++++------ .../Editor/GitHub.Unity/UI/ChangesView.cs | 8 ++++++-- .../Editor/GitHub.Unity/UI/HistoryView.cs | 9 ++++++--- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index c1fc927cc..8968f43b1 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -166,8 +166,11 @@ public ITask InitializeRepository() Environment.InitializeRepository(); RestartRepository(); }) - .Then(UsageTracker.IncrementNumberOfProjectsInitialized) - .ThenInUI(InitializeUI); + .ThenInUI(() => + { + TaskManager.Run(UsageTracker.IncrementNumberOfProjectsInitialized); + InitializeUI(); + }); return task; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 4d1344bf3..1e56ed445 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -305,11 +305,11 @@ private void OnButtonBarGUI() if (createBranch) { GitClient.CreateBranch(newBranchName, treeLocals.SelectedNode.Path) - .Then(UsageTracker.IncrementNumberOfLocalBranchCreations) .FinallyInUI((success, e) => { if (success) { + TaskManager.Run(UsageTracker.IncrementNumberOfLocalBranchCreations); Redraw(); } else @@ -475,11 +475,11 @@ private void CheckoutRemoteBranch(string branch) if (confirmCheckout) { GitClient.CreateBranch(branchName, branch) - .Then(UsageTracker.IncrementNumberOfRemoteBranchCheckouts) .FinallyInUI((success, e) => { if (success) { + TaskManager.Run(UsageTracker.IncrementNumberOfRemoteBranchCheckouts); Redraw(); } else @@ -498,11 +498,11 @@ private void SwitchBranch(string branch) ConfirmSwitchCancel)) { GitClient.SwitchBranch(branch) - .Then(UsageTracker.IncrementNumberOfLocalBranchCheckouts) .FinallyInUI((success, e) => { if (success) { + TaskManager.Run(UsageTracker.IncrementNumberOfLocalBranchCheckouts); Redraw(); } else @@ -519,9 +519,14 @@ private void DeleteLocalBranch(string branch) var dialogMessage = string.Format(DeleteBranchMessageFormatString, branch); if (EditorUtility.DisplayDialog(DeleteBranchTitle, dialogMessage, DeleteBranchButton, CancelButtonLabel)) { - GitClient - .DeleteBranch(branch, true) - .Then(UsageTracker.IncrementNumberOfLocalBranchDeletions) + GitClient.DeleteBranch(branch, true) + .FinallyInUI((success, e) => + { + if (success) + { + TaskManager.Run(UsageTracker.IncrementNumberOfLocalBranchDeletions); + } + }) .Start(); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index b6b2be3df..fbdc6a9cb 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -353,9 +353,13 @@ private void Commit() } addTask - .Then(UsageTracker.IncrementNumberOfCommits) - .FinallyInUI((b, exception) => + .FinallyInUI((success, exception) => { + if (success) + { + TaskManager.Run(UsageTracker.IncrementNumberOfCommits); + } + commitMessage = ""; commitBody = ""; }).Start(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index e46d5af2a..b330dce07 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -702,10 +702,11 @@ private void Pull() // (either git rebase --abort or git merge --abort) } }, runOptions: TaskRunOptions.OnAlways) - .Then(UsageTracker.IncrementNumberOfPulls) .FinallyInUI((success, e) => { if (success) { + TaskManager.Run(UsageTracker.IncrementNumberOfPulls); + EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), Localization.Ok); @@ -725,10 +726,11 @@ private void Push() { Repository .Push() - .Then(UsageTracker.IncrementNumberOfPushes) .FinallyInUI((success, e) => { if (success) { + TaskManager.Run(UsageTracker.IncrementNumberOfPushes); + EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), Localization.Ok); @@ -747,10 +749,11 @@ private void Fetch() { Repository .Fetch() - .Then(UsageTracker.IncrementNumberOfFetches) .FinallyInUI((success, e) => { if (!success) { + TaskManager.Run(UsageTracker.IncrementNumberOfFetches); + EditorUtility.DisplayDialog(FetchActionTitle, FetchFailureDescription, Localization.Ok); } From 51b1543d271032d3715fdc020350bc172671076d Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 13:46:26 -0400 Subject: [PATCH 039/567] Removing superflous logging --- src/GitHub.Api/Metrics/UsageTracker.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 2bc9ce582..30f5151de 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -166,7 +166,6 @@ public void IncrementNumberOfStartups() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfStartups++; - Logger.Trace("NumberOfStartups:{0} Date:{1}", usage.Measures.NumberOfStartups, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -177,7 +176,6 @@ public void IncrementNumberOfCommits() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfCommits++; - Logger.Trace("NumberOfCommits:{0} Date:{1}", usage.Measures.NumberOfCommits, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -188,7 +186,6 @@ public void IncrementNumberOfFetches() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfFetches++; - Logger.Trace("NumberOfFetches:{0} Date:{1}", usage.Measures.NumberOfFetches, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -199,7 +196,6 @@ public void IncrementNumberOfPushes() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfPushes++; - Logger.Trace("NumberOfPushes:{0} Date:{1}", usage.Measures.NumberOfPushes, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -210,7 +206,6 @@ public void IncrementNumberOfProjectsInitialized() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfProjectsInitialized++; - Logger.Trace("NumberOfProjectsInitialized:{0} Date:{1}", usage.Measures.NumberOfProjectsInitialized, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -221,7 +216,6 @@ public void IncrementNumberOfLocalBranchCreations() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfLocalBranchCreations++; - Logger.Trace("NumberOfLocalBranchCreations:{0} Date:{1}", usage.Measures.NumberOfLocalBranchCreations, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -232,7 +226,6 @@ public void IncrementNumberOfLocalBranchDeletions() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfLocalBranchDeletion++; - Logger.Trace("NumberOfLocalBranchDeletion:{0} Date:{1}", usage.Measures.NumberOfLocalBranchDeletion, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -243,7 +236,6 @@ public void IncrementNumberOfLocalBranchCheckouts() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfLocalBranchCheckouts++; - Logger.Trace("NumberOfLocalBranchCheckouts:{0} Date:{1}", usage.Measures.NumberOfLocalBranchCheckouts, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -254,7 +246,6 @@ public void IncrementNumberOfRemoteBranchCheckouts() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfRemoteBranchCheckouts++; - Logger.Trace("NumberOfRemoteBranchCheckouts:{0} Date:{1}", usage.Measures.NumberOfRemoteBranchCheckouts, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -265,7 +256,6 @@ public void IncrementNumberOfPulls() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfPulls++; - Logger.Trace("NumberOfPulls:{0} Date:{1}", usage.Measures.NumberOfPulls, usage.Dimensions.Date); SaveUsage(usageStore); } @@ -276,7 +266,6 @@ public void IncrementNumberOfAuthentications() var usage = GetCurrentUsage(usageStore); usage.Measures.NumberOfAuthentications++; - Logger.Trace("NumberOfAuthentications:{0} Date:{1}", usage.Measures.NumberOfAuthentications, usage.Dimensions.Date); SaveUsage(usageStore); } From e76a6786aa0e45df1df5bf0476ac3647fa1e3e75 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 15:19:08 -0400 Subject: [PATCH 040/567] Functionality to CreateEntry per session of GitHub for Unity --- .../Application/ApplicationManagerBase.cs | 1 + src/GitHub.Api/Metrics/IUsageTracker.cs | 3 ++ src/GitHub.Api/Metrics/UsageModel.cs | 31 ++++++++++++------- src/GitHub.Api/Metrics/UsageTracker.cs | 15 ++++++--- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 8968f43b1..3c17bc34a 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -216,6 +216,7 @@ protected void SetupMetrics(string unityVersion, bool firstRun) if (firstRun) { + UsageTracker.CreateEntry(); TaskManager.Run(UsageTracker.IncrementNumberOfStartups); } #endif diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index 56e1747b6..52d96deda 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -14,6 +14,7 @@ public interface IUsageTracker void IncrementNumberOfLocalBranchDeletions(); void IncrementNumberOfLocalBranchCheckouts(); void IncrementNumberOfRemoteBranchCheckouts(); + void CreateEntry(); } class NullUsageTracker : IUsageTracker @@ -30,6 +31,8 @@ public void IncrementNumberOfLocalBranchCreations() { } public void IncrementNumberOfLocalBranchDeletions() { } public void IncrementNumberOfLocalBranchCheckouts() { } public void IncrementNumberOfRemoteBranchCheckouts() { } + public void CreateEntry() { } + public void SetMetricsService(IMetricsService instance) { } } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 67b053193..bc66f77de 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace GitHub.Unity @@ -51,7 +52,7 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) if (currentUsage == null) { currentUsage = Reports - .FirstOrDefault(usage => usage.Dimensions.Date == date + .Last(usage => usage.Dimensions.Date == date && usage.Dimensions.AppVersion == appVersion && usage.Dimensions.UnityVersion == unityVersion); } @@ -64,16 +65,7 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) } else { - currentUsage = new Usage - { - Dimensions = { - Date = date, - Guid = Guid, - AppVersion = appVersion, - UnityVersion = unityVersion - } - }; - Reports.Add(currentUsage); + throw new InvalidOperationException("Current usage not found"); } return currentUsage; @@ -88,6 +80,23 @@ public void RemoveReports(DateTime beforeDate) { Reports.RemoveAll(usage => usage.Dimensions.Date.Date != beforeDate.Date); } + + public void CreateEntry(string appVersion, string unityVersion) + { + var date = DateTime.UtcNow.Date; + currentUsage = new Usage { + Dimensions = { + Date = date, + Guid = Guid, + AppVersion = appVersion, + UnityVersion = unityVersion, + Lang = CultureInfo.InstalledUICulture.IetfLanguageTag, + CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag + } + }; + + Reports.Add(currentUsage); + } } class UsageStore diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 30f5151de..67b3fa69d 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -154,10 +154,17 @@ private async Task SendUsage() private Usage GetCurrentUsage(UsageStore usageStore) { - var usage = usageStore.Model.GetCurrentUsage(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); - usage.Dimensions.Lang = CultureInfo.InstalledUICulture.IetfLanguageTag; - usage.Dimensions.CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag; - return usage; + return usageStore.Model.GetCurrentUsage(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); + } + + public void CreateEntry() + { + Logger.Trace("CreateEntry: \"{0}\"", storePath); + + var usageStore = LoadUsage(); + usageStore.Model.CreateEntry(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); + + SaveUsage(usageStore); } public void IncrementNumberOfStartups() From dda8f0e83de4afae26916e424479c9b3638395ea Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 15:45:53 -0400 Subject: [PATCH 041/567] Adding an instanceId to applciationCache to identify relevant log messages --- .../Application/ApplicationManagerBase.cs | 15 ++++---- src/GitHub.Api/Metrics/IUsageTracker.cs | 3 -- src/GitHub.Api/Metrics/UsageModel.cs | 37 +++++++------------ src/GitHub.Api/Metrics/UsageTracker.cs | 24 ++++-------- .../Editor/GitHub.Unity/ApplicationCache.cs | 29 +++++++++++++++ .../Editor/GitHub.Unity/ApplicationManager.cs | 2 +- 6 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 3c17bc34a..1ac712387 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -187,22 +187,22 @@ public void RestartRepository() } } - protected void SetupMetrics(string unityVersion, bool firstRun) + protected void SetupMetrics(string unityVersion, bool firstRun, Guid instanceId) { //Logger.Trace("Setup metrics"); var usagePath = Environment.UserCachePath.Combine(Constants.UsageFile); - string id = null; + string userId = null; if (UserSettings.Exists(Constants.GuidKey)) { - id = UserSettings.Get(Constants.GuidKey); + userId = UserSettings.Get(Constants.GuidKey); } - if (String.IsNullOrEmpty(id)) + if (String.IsNullOrEmpty(userId)) { - id = Guid.NewGuid().ToString(); - UserSettings.Set(Constants.GuidKey, id); + userId = Guid.NewGuid().ToString(); + UserSettings.Set(Constants.GuidKey, userId); } #if ENABLE_METRICS @@ -212,11 +212,10 @@ protected void SetupMetrics(string unityVersion, bool firstRun) Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); - UsageTracker = new UsageTracker(metricsService, UserSettings, usagePath, id, unityVersion); + UsageTracker = new UsageTracker(metricsService, UserSettings, usagePath, userId, unityVersion, instanceId.ToString()); if (firstRun) { - UsageTracker.CreateEntry(); TaskManager.Run(UsageTracker.IncrementNumberOfStartups); } #endif diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index 52d96deda..56e1747b6 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -14,7 +14,6 @@ public interface IUsageTracker void IncrementNumberOfLocalBranchDeletions(); void IncrementNumberOfLocalBranchCheckouts(); void IncrementNumberOfRemoteBranchCheckouts(); - void CreateEntry(); } class NullUsageTracker : IUsageTracker @@ -31,8 +30,6 @@ public void IncrementNumberOfLocalBranchCreations() { } public void IncrementNumberOfLocalBranchDeletions() { } public void IncrementNumberOfLocalBranchCheckouts() { } public void IncrementNumberOfRemoteBranchCheckouts() { } - public void CreateEntry() { } - public void SetMetricsService(IMetricsService instance) { } } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index bc66f77de..1324aebf5 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; namespace GitHub.Unity { public class Usage { + public string InstanceId { get; set; } public Dimensions Dimensions { get; set; } = new Dimensions(); public Measures Measures { get; set; } = new Measures(); } @@ -43,7 +43,7 @@ class UsageModel private Usage currentUsage; - public Usage GetCurrentUsage(string appVersion, string unityVersion) + public Usage GetCurrentUsage(string appVersion, string unityVersion, string instanceId) { Guard.ArgumentNotNullOrWhiteSpace(appVersion, "appVersion"); Guard.ArgumentNotNullOrWhiteSpace(unityVersion, "unityVersion"); @@ -52,9 +52,7 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) if (currentUsage == null) { currentUsage = Reports - .Last(usage => usage.Dimensions.Date == date - && usage.Dimensions.AppVersion == appVersion - && usage.Dimensions.UnityVersion == unityVersion); + .FirstOrDefault(usage => usage.InstanceId == instanceId); } if (currentUsage?.Dimensions.Date == date) @@ -65,7 +63,17 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion) } else { - throw new InvalidOperationException("Current usage not found"); + currentUsage = new Usage + { + InstanceId = instanceId, + Dimensions = { + Date = date, + Guid = Guid, + AppVersion = appVersion, + UnityVersion = unityVersion + } + }; + Reports.Add(currentUsage); } return currentUsage; @@ -80,23 +88,6 @@ public void RemoveReports(DateTime beforeDate) { Reports.RemoveAll(usage => usage.Dimensions.Date.Date != beforeDate.Date); } - - public void CreateEntry(string appVersion, string unityVersion) - { - var date = DateTime.UtcNow.Date; - currentUsage = new Usage { - Dimensions = { - Date = date, - Guid = Guid, - AppVersion = appVersion, - UnityVersion = unityVersion, - Lang = CultureInfo.InstalledUICulture.IetfLanguageTag, - CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag - } - }; - - Reports.Add(currentUsage); - } } class UsageStore diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 67b3fa69d..d3442d71e 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -17,19 +17,21 @@ class UsageTracker : IUsageTracker private readonly NPath storePath; private readonly ISettings userSettings; private readonly IMetricsService metricsService; - private readonly string guid; + private readonly string userId; private readonly string unityVersion; + private readonly string instanceId; private Timer timer; - public UsageTracker(IMetricsService metricsService, ISettings userSettings, NPath storePath, string guid, string unityVersion) + public UsageTracker(IMetricsService metricsService, ISettings userSettings, NPath storePath, string userId, string unityVersion, string instanceId) { this.userSettings = userSettings; this.metricsService = metricsService; - this.guid = guid; + this.userId = userId; this.storePath = storePath; this.unityVersion = unityVersion; + this.instanceId = instanceId; - Logger.Trace("guid:{0}", guid); + Logger.Trace("userId:{0} instanceId:{1}", userId, instanceId); if (Enabled) RunTimer(3*60); } @@ -66,7 +68,7 @@ private UsageStore LoadUsage() result = new UsageStore(); if (String.IsNullOrEmpty(result.Model.Guid)) - result.Model.Guid = guid; + result.Model.Guid = userId; return result; } @@ -154,17 +156,7 @@ private async Task SendUsage() private Usage GetCurrentUsage(UsageStore usageStore) { - return usageStore.Model.GetCurrentUsage(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); - } - - public void CreateEntry() - { - Logger.Trace("CreateEntry: \"{0}\"", storePath); - - var usageStore = LoadUsage(); - usageStore.Model.CreateEntry(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion); - - SaveUsage(usageStore); + return usageStore.Model.GetCurrentUsage(ApplicationConfiguration.AssemblyName.Version.ToString(), unityVersion, instanceId); } public void IncrementNumberOfStartups() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs index 352b3e2bb..524cfe1c2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs @@ -14,6 +14,8 @@ sealed class ApplicationCache : ScriptObjectSingleton { [SerializeField] private bool firstRun = true; [SerializeField] public string firstRunAtString; + [SerializeField] public string instanceIdString; + [NonSerialized] private Guid? instanceId; [NonSerialized] private bool? firstRunValue; [NonSerialized] public DateTimeOffset? firstRunAtValue; @@ -60,6 +62,33 @@ private void EnsureFirstRun() Save(true); } } + + public Guid InstanceId + { + get + { + EnsureInstanceId(); + return instanceId.Value; + } + } + + private void EnsureInstanceId() + { + if (instanceId.HasValue) + { + return; + } + + if (string.IsNullOrEmpty(instanceIdString)) + { + instanceId = Guid.NewGuid(); + instanceIdString = instanceId.ToString(); + } + else + { + instanceId = new Guid(instanceIdString); + } + } } sealed class EnvironmentCache : ScriptObjectSingleton diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs index 4bf049cfa..2187ed089 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs @@ -23,7 +23,7 @@ public ApplicationManager(IMainThreadSynchronizationContext synchronizationConte protected override void SetupMetrics() { - SetupMetrics(Environment.UnityVersion, ApplicationCache.Instance.FirstRun); + SetupMetrics(Environment.UnityVersion, ApplicationCache.Instance.FirstRun, ApplicationCache.Instance.InstanceId); } protected override void InitializeUI() From fe35fa77429e2cfd17cb33882a58ca8a70760406 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 16:08:57 -0400 Subject: [PATCH 042/567] Populating Lang and CurrentLang --- src/GitHub.Api/Metrics/UsageModel.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 1324aebf5..6f3752c88 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace GitHub.Unity @@ -55,13 +56,7 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion, string inst .FirstOrDefault(usage => usage.InstanceId == instanceId); } - if (currentUsage?.Dimensions.Date == date) - { - // update any fields that might be missing, if we've changed the format - if (currentUsage.Dimensions.Guid != Guid) - currentUsage.Dimensions.Guid = Guid; - } - else + if (currentUsage == null) { currentUsage = new Usage { @@ -70,7 +65,9 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion, string inst Date = date, Guid = Guid, AppVersion = appVersion, - UnityVersion = unityVersion + UnityVersion = unityVersion, + Lang = CultureInfo.InstalledUICulture.IetfLanguageTag, + CurrentLang = CultureInfo.CurrentCulture.IetfLanguageTag } }; Reports.Add(currentUsage); From 639d6c8ab99f894bc44becc6b48a7869ee8c9c13 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 17:18:17 -0400 Subject: [PATCH 043/567] Removing NumberOf prefix from metrics --- src/GitHub.Api/Metrics/UsageModel.cs | 22 +++++++++++----------- src/GitHub.Api/Metrics/UsageTracker.cs | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 6f3752c88..7c56560bf 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -24,17 +24,17 @@ public class Dimensions public class Measures { - public int NumberOfStartups { get; set; } - public int NumberOfCommits { get; set; } - public int NumberOfFetches { get; set; } - public int NumberOfPushes { get; set; } - public int NumberOfPulls { get; set; } - public int NumberOfProjectsInitialized { get; set; } - public int NumberOfAuthentications { get; set; } - public int NumberOfLocalBranchCreations { get; set; } - public int NumberOfLocalBranchDeletion { get; set; } - public int NumberOfLocalBranchCheckouts { get; set; } - public int NumberOfRemoteBranchCheckouts { get; set; } + public int Startups { get; set; } + public int Commits { get; set; } + public int Fetches { get; set; } + public int Pushes { get; set; } + public int Pulls { get; set; } + public int ProjectsInitialized { get; set; } + public int Authentications { get; set; } + public int LocalBranchCreations { get; set; } + public int LocalBranchDeletion { get; set; } + public int LocalBranchCheckouts { get; set; } + public int RemoteBranchCheckouts { get; set; } } class UsageModel diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index d3442d71e..e767ef094 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -164,7 +164,7 @@ public void IncrementNumberOfStartups() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfStartups++; + usage.Measures.Startups++; SaveUsage(usageStore); } @@ -174,7 +174,7 @@ public void IncrementNumberOfCommits() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfCommits++; + usage.Measures.Commits++; SaveUsage(usageStore); } @@ -184,7 +184,7 @@ public void IncrementNumberOfFetches() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfFetches++; + usage.Measures.Fetches++; SaveUsage(usageStore); } @@ -194,7 +194,7 @@ public void IncrementNumberOfPushes() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfPushes++; + usage.Measures.Pushes++; SaveUsage(usageStore); } @@ -204,7 +204,7 @@ public void IncrementNumberOfProjectsInitialized() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfProjectsInitialized++; + usage.Measures.ProjectsInitialized++; SaveUsage(usageStore); } @@ -214,7 +214,7 @@ public void IncrementNumberOfLocalBranchCreations() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfLocalBranchCreations++; + usage.Measures.LocalBranchCreations++; SaveUsage(usageStore); } @@ -224,7 +224,7 @@ public void IncrementNumberOfLocalBranchDeletions() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfLocalBranchDeletion++; + usage.Measures.LocalBranchDeletion++; SaveUsage(usageStore); } @@ -234,7 +234,7 @@ public void IncrementNumberOfLocalBranchCheckouts() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfLocalBranchCheckouts++; + usage.Measures.LocalBranchCheckouts++; SaveUsage(usageStore); } @@ -244,7 +244,7 @@ public void IncrementNumberOfRemoteBranchCheckouts() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfRemoteBranchCheckouts++; + usage.Measures.RemoteBranchCheckouts++; SaveUsage(usageStore); } @@ -254,7 +254,7 @@ public void IncrementNumberOfPulls() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfPulls++; + usage.Measures.Pulls++; SaveUsage(usageStore); } @@ -264,7 +264,7 @@ public void IncrementNumberOfAuthentications() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.NumberOfAuthentications++; + usage.Measures.Authentications++; SaveUsage(usageStore); } From 58fe6017fa02288ba1f8b93fcaef2f736e62f46b Mon Sep 17 00:00:00 2001 From: ThomasAunvik Date: Fri, 4 May 2018 23:30:04 +0200 Subject: [PATCH 044/567] Fixes icons in changes tab Also includes fancy new icons --- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 14 -------------- .../Editor/GitHub.Unity/UI/ChangesTreeControl.cs | 6 +----- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index c5d5edbcb..1cde46a27 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -90,7 +90,6 @@ class Styles favoriteIconOff, smallLogoIcon, bigLogoIcon, - defaultAssetIcon, folderIcon, mergeIcon, dotIcon, @@ -784,19 +783,6 @@ public static Texture2D LocalCommitIcon } } - public static Texture2D DefaultAssetIcon - { - get - { - if (defaultAssetIcon == null) - { - defaultAssetIcon = EditorGUIUtility.FindTexture("DefaultAsset Icon"); - } - - return defaultAssetIcon; - } - } - public static Texture2D FolderIcon { get diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs index 36e818c5c..d7d9a03c4 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs @@ -163,17 +163,13 @@ protected Texture GetNodeIcon(ChangesTreeNode node) { if (!string.IsNullOrEmpty(node.ProjectPath)) { - nodeIcon = AssetDatabase.GetCachedIcon(node.ProjectPath); + nodeIcon = UnityEditorInternal.InternalEditorUtility.GetIconForFile(node.ProjectPath); } if (nodeIcon != null) { nodeIcon.hideFlags = HideFlags.HideAndDontSave; } - else - { - nodeIcon = Styles.DefaultAssetIcon; - } } return nodeIcon; From e31e99e35bd5e4e5392127f3a75215ca8dbccf86 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 17:30:28 -0400 Subject: [PATCH 045/567] Restoring NumberOfStartups --- src/GitHub.Api/Metrics/UsageModel.cs | 2 +- src/GitHub.Api/Metrics/UsageTracker.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 7c56560bf..8cc0a58ed 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -24,7 +24,7 @@ public class Dimensions public class Measures { - public int Startups { get; set; } + public int NumberOfStartups { get; set; } public int Commits { get; set; } public int Fetches { get; set; } public int Pushes { get; set; } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index e767ef094..e45d7f33a 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -164,7 +164,7 @@ public void IncrementNumberOfStartups() var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.Startups++; + usage.Measures.NumberOfStartups++; SaveUsage(usageStore); } From 186fac0086ab94d40afa085c50bab9e1ac344988 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 4 May 2018 17:37:38 -0400 Subject: [PATCH 046/567] Force unlock when utilized from settings --- .../Assets/Editor/GitHub.Unity/UI/SettingsView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs index f5f509bd8..550804e6c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs @@ -292,7 +292,7 @@ private void OnGitLfsLocksGUI() GUILayout.FlexibleSpace(); if (GUILayout.Button("Unlock")) { - Repository.ReleaseLock(lck.Path, false).Start(); + Repository.ReleaseLock(lck.Path, true).Start(); } } GUILayout.EndHorizontal(); From 9abcd25143073624c7a6b89c17a08e954164bca0 Mon Sep 17 00:00:00 2001 From: ThomasAunvik Date: Sat, 5 May 2018 00:26:14 +0200 Subject: [PATCH 047/567] Added option to fetch all --- src/GitHub.Api/Git/GitClient.cs | 2 +- src/GitHub.Api/Git/Tasks/GitFetchTask.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index ebc337aaa..176dc056f 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -276,7 +276,7 @@ public ITask Fetch(string remote, { //Logger.Trace("Fetch"); - return new GitFetchTask(remote, cancellationToken, true, processor) + return new GitFetchTask(remote, cancellationToken, true, true, processor) .Configure(processManager); } diff --git a/src/GitHub.Api/Git/Tasks/GitFetchTask.cs b/src/GitHub.Api/Git/Tasks/GitFetchTask.cs index c5a006b8b..59a89a4ad 100644 --- a/src/GitHub.Api/Git/Tasks/GitFetchTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitFetchTask.cs @@ -10,13 +10,18 @@ class GitFetchTask : ProcessTask private readonly string arguments; public GitFetchTask(string remote, - CancellationToken token, bool prune = false, IOutputProcessor processor = null) + CancellationToken token, bool all = false, bool prune = false, IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) { Name = TaskName; var stringBuilder = new StringBuilder(); stringBuilder.Append("fetch"); + if (all) + { + stringBuilder.Append(" --all"); + } + if (prune) { stringBuilder.Append(" -p"); @@ -34,4 +39,4 @@ public GitFetchTask(string remote, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } } -} \ No newline at end of file +} From 61e022760d940ac6426091fd0deb9891a8ee8f04 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 7 May 2018 11:34:02 -0400 Subject: [PATCH 048/567] Renaming metrics values --- .../Application/ApplicationManagerBase.cs | 2 +- src/GitHub.Api/Metrics/IUsageTracker.cs | 40 +++++++++---------- src/GitHub.Api/Metrics/UsageModel.cs | 20 +++++----- src/GitHub.Api/Metrics/UsageTracker.cs | 40 +++++++++---------- .../GitHub.Unity/UI/AuthenticationView.cs | 2 +- .../Editor/GitHub.Unity/UI/BranchesView.cs | 8 ++-- .../Editor/GitHub.Unity/UI/ChangesView.cs | 2 +- .../Editor/GitHub.Unity/UI/HistoryView.cs | 6 +-- 8 files changed, 60 insertions(+), 60 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 1ac712387..93ed119c0 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -168,7 +168,7 @@ public ITask InitializeRepository() }) .ThenInUI(() => { - TaskManager.Run(UsageTracker.IncrementNumberOfProjectsInitialized); + TaskManager.Run(UsageTracker.Initialized); InitializeUI(); }); return task; diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index 56e1747b6..055c4706e 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -4,32 +4,32 @@ public interface IUsageTracker { bool Enabled { get; set; } void IncrementNumberOfStartups(); - void IncrementNumberOfCommits(); - void IncrementNumberOfFetches(); - void IncrementNumberOfPushes(); - void IncrementNumberOfPulls(); - void IncrementNumberOfAuthentications(); - void IncrementNumberOfProjectsInitialized(); - void IncrementNumberOfLocalBranchCreations(); - void IncrementNumberOfLocalBranchDeletions(); - void IncrementNumberOfLocalBranchCheckouts(); - void IncrementNumberOfRemoteBranchCheckouts(); + void ChangesViewButtonCommit(); + void HistoryToolbarButtonFetch(); + void HistoryToolbarButtonPush(); + void HistoryToolbarButtonPull(); + void AuthenticationViewButtonAuthentication(); + void Initialized(); + void BranchesViewButtonCreateBranch(); + void BranchesViewButtonDeleteBranch(); + void BranchesViewButtonCheckoutLocalBranch(); + void BranchesViewButtonCheckoutRemoteBranch(); } class NullUsageTracker : IUsageTracker { public bool Enabled { get; set; } public void IncrementNumberOfStartups() { } - public void IncrementNumberOfCommits() { } - public void IncrementNumberOfFetches() { } - public void IncrementNumberOfPushes() { } - public void IncrementNumberOfPulls() { } - public void IncrementNumberOfAuthentications() { } - public void IncrementNumberOfProjectsInitialized() { } - public void IncrementNumberOfLocalBranchCreations() { } - public void IncrementNumberOfLocalBranchDeletions() { } - public void IncrementNumberOfLocalBranchCheckouts() { } - public void IncrementNumberOfRemoteBranchCheckouts() { } + public void ChangesViewButtonCommit() { } + public void HistoryToolbarButtonFetch() { } + public void HistoryToolbarButtonPush() { } + public void HistoryToolbarButtonPull() { } + public void AuthenticationViewButtonAuthentication() { } + public void Initialized() { } + public void BranchesViewButtonCreateBranch() { } + public void BranchesViewButtonDeleteBranch() { } + public void BranchesViewButtonCheckoutLocalBranch() { } + public void BranchesViewButtonCheckoutRemoteBranch() { } public void SetMetricsService(IMetricsService instance) { } } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 8cc0a58ed..3184d38b2 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -25,16 +25,16 @@ public class Dimensions public class Measures { public int NumberOfStartups { get; set; } - public int Commits { get; set; } - public int Fetches { get; set; } - public int Pushes { get; set; } - public int Pulls { get; set; } - public int ProjectsInitialized { get; set; } - public int Authentications { get; set; } - public int LocalBranchCreations { get; set; } - public int LocalBranchDeletion { get; set; } - public int LocalBranchCheckouts { get; set; } - public int RemoteBranchCheckouts { get; set; } + public int ChangesViewButtonCommit { get; set; } + public int HistoryToolbarButtonFetch { get; set; } + public int HistoryToolbarButtonPush { get; set; } + public int HistoryToolbarButtonPull { get; set; } + public int Initialized { get; set; } + public int AuthenticationViewButtonAuthentication { get; set; } + public int BranchesViewButtonCreateBranch { get; set; } + public int BranchesViewButtonDeleteBranch { get; set; } + public int BranchesViewButtonCheckoutLocalBranch { get; set; } + public int BranchesViewButtonCheckoutRemoteBranch { get; set; } } class UsageModel diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index e45d7f33a..17b178b2d 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -169,102 +169,102 @@ public void IncrementNumberOfStartups() SaveUsage(usageStore); } - public void IncrementNumberOfCommits() + public void ChangesViewButtonCommit() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.Commits++; + usage.Measures.ChangesViewButtonCommit++; SaveUsage(usageStore); } - public void IncrementNumberOfFetches() + public void HistoryToolbarButtonFetch() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.Fetches++; + usage.Measures.HistoryToolbarButtonFetch++; SaveUsage(usageStore); } - public void IncrementNumberOfPushes() + public void HistoryToolbarButtonPush() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.Pushes++; + usage.Measures.HistoryToolbarButtonPush++; SaveUsage(usageStore); } - public void IncrementNumberOfProjectsInitialized() + public void Initialized() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.ProjectsInitialized++; + usage.Measures.Initialized++; SaveUsage(usageStore); } - public void IncrementNumberOfLocalBranchCreations() + public void BranchesViewButtonCreateBranch() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.LocalBranchCreations++; + usage.Measures.BranchesViewButtonCreateBranch++; SaveUsage(usageStore); } - public void IncrementNumberOfLocalBranchDeletions() + public void BranchesViewButtonDeleteBranch() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.LocalBranchDeletion++; + usage.Measures.BranchesViewButtonDeleteBranch++; SaveUsage(usageStore); } - public void IncrementNumberOfLocalBranchCheckouts() + public void BranchesViewButtonCheckoutLocalBranch() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.LocalBranchCheckouts++; + usage.Measures.BranchesViewButtonCheckoutLocalBranch++; SaveUsage(usageStore); } - public void IncrementNumberOfRemoteBranchCheckouts() + public void BranchesViewButtonCheckoutRemoteBranch() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.RemoteBranchCheckouts++; + usage.Measures.BranchesViewButtonCheckoutRemoteBranch++; SaveUsage(usageStore); } - public void IncrementNumberOfPulls() + public void HistoryToolbarButtonPull() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.Pulls++; + usage.Measures.HistoryToolbarButtonPull++; SaveUsage(usageStore); } - public void IncrementNumberOfAuthentications() + public void AuthenticationViewButtonAuthentication() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.Authentications++; + usage.Measures.AuthenticationViewButtonAuthentication++; SaveUsage(usageStore); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 265ef73e6..bcaef3c3c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -214,7 +214,7 @@ private void DoResult(bool success, string msg) if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfAuthentications); + TaskManager.Run(UsageTracker.AuthenticationViewButtonAuthentication); Clear(); Finish(true); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 1e56ed445..051e058e1 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -309,7 +309,7 @@ private void OnButtonBarGUI() { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfLocalBranchCreations); + TaskManager.Run(UsageTracker.BranchesViewButtonCreateBranch); Redraw(); } else @@ -479,7 +479,7 @@ private void CheckoutRemoteBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfRemoteBranchCheckouts); + TaskManager.Run(UsageTracker.BranchesViewButtonCheckoutRemoteBranch); Redraw(); } else @@ -502,7 +502,7 @@ private void SwitchBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfLocalBranchCheckouts); + TaskManager.Run(UsageTracker.BranchesViewButtonCheckoutLocalBranch); Redraw(); } else @@ -524,7 +524,7 @@ private void DeleteLocalBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfLocalBranchDeletions); + TaskManager.Run(UsageTracker.BranchesViewButtonDeleteBranch); } }) .Start(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index fbdc6a9cb..624e19908 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -357,7 +357,7 @@ private void Commit() { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfCommits); + TaskManager.Run(UsageTracker.ChangesViewButtonCommit); } commitMessage = ""; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index b330dce07..a44b8f593 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -705,7 +705,7 @@ private void Pull() .FinallyInUI((success, e) => { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfPulls); + TaskManager.Run(UsageTracker.HistoryToolbarButtonPull); EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), @@ -729,7 +729,7 @@ private void Push() .FinallyInUI((success, e) => { if (success) { - TaskManager.Run(UsageTracker.IncrementNumberOfPushes); + TaskManager.Run(UsageTracker.HistoryToolbarButtonPush); EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), @@ -752,7 +752,7 @@ private void Fetch() .FinallyInUI((success, e) => { if (!success) { - TaskManager.Run(UsageTracker.IncrementNumberOfFetches); + TaskManager.Run(UsageTracker.HistoryToolbarButtonFetch); EditorUtility.DisplayDialog(FetchActionTitle, FetchFailureDescription, Localization.Ok); From 3e2db8e875ce9dc4e18b58bf38432e608c179466 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 7 May 2018 12:31:14 -0400 Subject: [PATCH 049/567] Making names more uniform --- src/GitHub.Api/Metrics/IUsageTracker.cs | 12 ++++++------ src/GitHub.Api/Metrics/UsageModel.cs | 6 +++--- src/GitHub.Api/Metrics/UsageTracker.cs | 12 ++++++------ .../Assets/Editor/GitHub.Unity/UI/HistoryView.cs | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index 055c4706e..2a40cdb98 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -5,9 +5,9 @@ public interface IUsageTracker bool Enabled { get; set; } void IncrementNumberOfStartups(); void ChangesViewButtonCommit(); - void HistoryToolbarButtonFetch(); - void HistoryToolbarButtonPush(); - void HistoryToolbarButtonPull(); + void HistoryViewToolbarButtonFetch(); + void HistoryViewToolbarButtonPush(); + void HistoryViewToolbarButtonPull(); void AuthenticationViewButtonAuthentication(); void Initialized(); void BranchesViewButtonCreateBranch(); @@ -21,9 +21,9 @@ class NullUsageTracker : IUsageTracker public bool Enabled { get; set; } public void IncrementNumberOfStartups() { } public void ChangesViewButtonCommit() { } - public void HistoryToolbarButtonFetch() { } - public void HistoryToolbarButtonPush() { } - public void HistoryToolbarButtonPull() { } + public void HistoryViewToolbarButtonFetch() { } + public void HistoryViewToolbarButtonPush() { } + public void HistoryViewToolbarButtonPull() { } public void AuthenticationViewButtonAuthentication() { } public void Initialized() { } public void BranchesViewButtonCreateBranch() { } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 3184d38b2..69fc1120b 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -26,9 +26,9 @@ public class Measures { public int NumberOfStartups { get; set; } public int ChangesViewButtonCommit { get; set; } - public int HistoryToolbarButtonFetch { get; set; } - public int HistoryToolbarButtonPush { get; set; } - public int HistoryToolbarButtonPull { get; set; } + public int HistoryViewToolbarButtonFetch { get; set; } + public int HistoryViewToolbarButtonPush { get; set; } + public int HistoryViewToolbarButtonPull { get; set; } public int Initialized { get; set; } public int AuthenticationViewButtonAuthentication { get; set; } public int BranchesViewButtonCreateBranch { get; set; } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 17b178b2d..8e98d444d 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -179,22 +179,22 @@ public void ChangesViewButtonCommit() SaveUsage(usageStore); } - public void HistoryToolbarButtonFetch() + public void HistoryViewToolbarButtonFetch() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.HistoryToolbarButtonFetch++; + usage.Measures.HistoryViewToolbarButtonFetch++; SaveUsage(usageStore); } - public void HistoryToolbarButtonPush() + public void HistoryViewToolbarButtonPush() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.HistoryToolbarButtonPush++; + usage.Measures.HistoryViewToolbarButtonPush++; SaveUsage(usageStore); } @@ -249,12 +249,12 @@ public void BranchesViewButtonCheckoutRemoteBranch() SaveUsage(usageStore); } - public void HistoryToolbarButtonPull() + public void HistoryViewToolbarButtonPull() { var usageStore = LoadUsage(); var usage = GetCurrentUsage(usageStore); - usage.Measures.HistoryToolbarButtonPull++; + usage.Measures.HistoryViewToolbarButtonPull++; SaveUsage(usageStore); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index a44b8f593..08e2221b8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -705,7 +705,7 @@ private void Pull() .FinallyInUI((success, e) => { if (success) { - TaskManager.Run(UsageTracker.HistoryToolbarButtonPull); + TaskManager.Run(UsageTracker.HistoryViewToolbarButtonPull); EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), @@ -729,7 +729,7 @@ private void Push() .FinallyInUI((success, e) => { if (success) { - TaskManager.Run(UsageTracker.HistoryToolbarButtonPush); + TaskManager.Run(UsageTracker.HistoryViewToolbarButtonPush); EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), @@ -752,7 +752,7 @@ private void Fetch() .FinallyInUI((success, e) => { if (!success) { - TaskManager.Run(UsageTracker.HistoryToolbarButtonFetch); + TaskManager.Run(UsageTracker.HistoryViewToolbarButtonFetch); EditorUtility.DisplayDialog(FetchActionTitle, FetchFailureDescription, Localization.Ok); From 891d1bc790137c4901cb5218f4e9549c80d9a5bf Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 7 May 2018 19:33:01 +0200 Subject: [PATCH 050/567] Add more options to SimpleJson Add extension methods for serializing/deserializing so we can control how fields and properties are named. This allows us to serialize/deserialize public and private fields and properties with either Capitalized or lowerCase names --- script | 2 +- src/GitHub.Api/Authentication/Keychain.cs | 4 +- src/GitHub.Api/Helpers/SimpleJson.cs | 176 +++++++++++++++++++--- src/GitHub.Api/Metrics/UsageTracker.cs | 4 +- src/GitHub.Api/Platform/Settings.cs | 4 +- src/GitHub.Logging/LogHelper.cs | 2 + 6 files changed, 163 insertions(+), 29 deletions(-) diff --git a/script b/script index 48d975141..73f71c796 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 48d975141aae81c47fe64981518edc726ee520f0 +Subproject commit 73f71c796245ec82f82db57b66ac21c48f529ed3 diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index ed169b7e5..48d2296f0 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -198,7 +198,7 @@ private void LoadConnectionsFromDisk() var json = cachePath.ReadAllText(); try { - var conns = SimpleJson.DeserializeObject(json); + var conns = json.FromJson(); UpdateConnections(conns); } catch (IOException ex) @@ -219,7 +219,7 @@ private void SaveConnectionsToDisk(bool raiseChangedEvent = true) //logger.Trace("WriteCacheToDisk Count:{0} Path:{1}", connectionCache.Count, cachePath.ToString()); try { - var json = SimpleJson.SerializeObject(connections.Values.ToArray()); + var json = connections.Values.ToJson(); cachePath.WriteAllText(json); } catch (IOException ex) diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index dd2cf8483..54253097a 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -36,7 +36,7 @@ // NOTE: uncomment the following line to disable linq expressions/compiled lambda (better performance) instead of method.invoke(). // define if you are using .net framework <= 3.0 or < WP7.5 -//#define SIMPLE_JSON_NO_LINQ_EXPRESSION +#define SIMPLE_JSON_NO_LINQ_EXPRESSION // NOTE: uncomment the following line if you are compiling under Window Metro style application/library. // usually already defined in properties @@ -66,12 +66,14 @@ using System.Reflection; using System.Runtime.Serialization; using System.Text; -using GitHub.Reflection; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using GitHub.Unity.Json; // ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable RedundantExplicitArrayCreation // ReSharper disable SuggestUseVarKeywordEvident -namespace GitHub +namespace GitHub.Unity.Json { /// /// Represents the json array. @@ -482,10 +484,7 @@ public override IEnumerable GetDynamicMemberNames() } #endif } -} -namespace GitHub -{ /// /// This class encodes and decodes JSON strings. /// Spec. details, see http://www.json.org/ @@ -517,6 +516,7 @@ static class SimpleJson private static readonly char[] EscapeTable; private static readonly char[] EscapeCharacters = new char[] { '"', '\\', '\b', '\f', '\n', '\r', '\t' }; + private static readonly string EscapeCharactersString = new string(EscapeCharacters); static SimpleJson() { @@ -620,7 +620,7 @@ public static string EscapeToJavascriptString(string jsonString) StringBuilder sb = new StringBuilder(); char c; - for (int i = 0; i < jsonString.Length; ) + for (int i = 0; i < jsonString.Length;) { c = jsonString[i++]; @@ -1282,14 +1282,14 @@ internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(T if (propertyInfo.CanRead) { MethodInfo getMethod = ReflectionUtils.GetGetterMethodInfo(propertyInfo); - if (getMethod.IsStatic || !getMethod.IsPublic) + if (!CanAddProperty(propertyInfo, getMethod)) continue; result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = ReflectionUtils.GetGetMethod(propertyInfo); } } foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) { - if (fieldInfo.IsStatic || !fieldInfo.IsPublic) + if (!CanAddField(fieldInfo)) continue; result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = ReflectionUtils.GetGetMethod(fieldInfo); } @@ -1304,20 +1304,40 @@ internal virtual ReflectionUtils.ConstructorDelegate ContructorDelegateFactory(T if (propertyInfo.CanWrite) { MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo); - if (setMethod.IsStatic || !setMethod.IsPublic) + if (!CanAddProperty(propertyInfo, setMethod)) continue; result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = new KeyValuePair(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo)); } } foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type)) { - if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic) + if (fieldInfo.IsInitOnly || !CanAddField(fieldInfo)) continue; result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = new KeyValuePair(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo)); } return result; } + protected virtual bool CanAddField(FieldInfo field) + { + if (field.IsStatic) + return false; + if (ReflectionUtils.GetAttribute(field, typeof(NotSerializedAttribute)) != null) + return false; + if (ReflectionUtils.GetAttribute(field, typeof(CompilerGeneratedAttribute)) != null) + return false; + return true; + } + + protected virtual bool CanAddProperty(PropertyInfo property, MethodInfo method) + { + if (method.IsStatic) + return false; + if (ReflectionUtils.GetAttribute(property, typeof(NotSerializedAttribute)) != null) + return false; + return true; + } + public virtual bool TrySerializeNonPrimitiveObject(object input, out object output) { return TrySerializeKnownTypes(input, out output) || TrySerializeUnknownTypes(input, out output); @@ -1329,7 +1349,7 @@ public virtual object DeserializeObject(object value, Type type) if (type == null) throw new ArgumentNullException("type"); string str = value as string; - if (type == typeof (Guid) && string.IsNullOrEmpty(str)) + if (type == typeof(Guid) && string.IsNullOrEmpty(str)) return default(Guid); if (value == null) @@ -1349,19 +1369,19 @@ public virtual object DeserializeObject(object value, Type type) return new Guid(str); if (type == typeof(Uri)) { - bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); + bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); Uri result; if (isValid && Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out result)) return result; - return null; + return null; } - if (type == typeof(string)) - return str; + if (type == typeof(string)) + return str; - return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); + return Convert.ChangeType(str, type, CultureInfo.InvariantCulture); } else { @@ -1592,8 +1612,6 @@ private static bool CanAdd(MemberInfo info, out string jsonKey) #endif - namespace Reflection - { // This class is meant to be copied into other libraries. So we want to exclude it from Code Analysis rules // that might be in place in the target project. [GeneratedCode("reflection-utils", "1.0.0")] @@ -1648,7 +1666,7 @@ public static Type GetGenericListElementType(Type type) foreach (Type implementedInterface in interfaces) { if (IsTypeGeneric(implementedInterface) && - implementedInterface.GetGenericTypeDefinition() == typeof (IList<>)) + implementedInterface.GetGenericTypeDefinition() == typeof(IList<>)) { return GetGenericTypeArguments(implementedInterface)[0]; } @@ -1837,7 +1855,13 @@ public static ConstructorDelegate GetConstructorByReflection(ConstructorInfo con public static ConstructorDelegate GetConstructorByReflection(Type type, params Type[] argsType) { ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByReflection(constructorInfo); + // if it's a value type (i.e., struct), it won't have a default constructor, so use Activator instead + return constructorInfo == null ? (type.IsValueType ? GetConstructorForValueType(type) : null) : GetConstructorByReflection(constructorInfo); + } + + static ConstructorDelegate GetConstructorForValueType(Type type) + { + return delegate(object[] args) { return Activator.CreateInstance(type); }; } #if !SIMPLE_JSON_NO_LINQ_EXPRESSION @@ -1864,7 +1888,8 @@ public static ConstructorDelegate GetConstructorByExpression(ConstructorInfo con public static ConstructorDelegate GetConstructorByExpression(Type type, params Type[] argsType) { ConstructorInfo constructorInfo = GetConstructorInfo(type, argsType); - return constructorInfo == null ? null : GetConstructorByExpression(constructorInfo); + // if it's a value type (i.e., struct), it won't have a default constructor, so use Activator instead + return constructorInfo == null ? (type.IsValueType ? GetConstructorForValueType(type) : null) : GetConstructorByExpression(constructorInfo); } #endif @@ -1924,6 +1949,9 @@ public static SetDelegate GetSetMethod(PropertyInfo propertyInfo) #if SIMPLE_JSON_NO_LINQ_EXPRESSION return GetSetMethodByReflection(propertyInfo); #else + // if it's a struct, we want to use reflection, as linq expressions modify copies of the object and not the real thing + if (propertyInfo.DeclaringType.IsValueType) + return GetSetMethodByReflection(propertyInfo); return GetSetMethodByExpression(propertyInfo); #endif } @@ -1933,6 +1961,9 @@ public static SetDelegate GetSetMethod(FieldInfo fieldInfo) #if SIMPLE_JSON_NO_LINQ_EXPRESSION return GetSetMethodByReflection(fieldInfo); #else + // if it's a struct, we want to use reflection, as linq expressions modify copies of the object and not the real thing + if (fieldInfo.DeclaringType.IsValueType) + return GetSetMethodByReflection(fieldInfo); return GetSetMethodByExpression(fieldInfo); #endif } @@ -2119,6 +2150,107 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() } } + +} + +namespace GitHub.Unity +{ + [System.AttributeUsage(System.AttributeTargets.Property | + System.AttributeTargets.Field)] + public sealed class NotSerializedAttribute : Attribute + { + } + + public static class JsonSerializerExtensions + { + static JsonSerializationStrategy publicLowerCaseStrategy = new JsonSerializationStrategy(true, true); + static JsonSerializationStrategy publicUpperCaseStrategy = new JsonSerializationStrategy(false, true); + static JsonSerializationStrategy privateLowerCaseStrategy = new JsonSerializationStrategy(true, false); + static JsonSerializationStrategy privateUpperCaseStrategy = new JsonSerializationStrategy(false, false); + public static string ToJson(this T model, bool lowerCase = false, bool onlyPublic = true) + { + return SimpleJson.SerializeObject(model, GetStrategy(lowerCase, onlyPublic)); + } + + public static T FromJson(this string json, bool lowerCase = false, bool onlyPublic = true) + { + return SimpleJson.DeserializeObject(json, GetStrategy(lowerCase, onlyPublic)); + } + + public static T FromObject(this object obj, bool lowerCase = false, bool onlyPublic = true) + { + if (obj == null) + return default(T); + var ret = GetStrategy(lowerCase, onlyPublic).DeserializeObject(obj, typeof(T)); + if (ret is T) + return (T)ret; + return default(T); + } + + private static JsonSerializationStrategy GetStrategy(bool lowerCase, bool onlyPublic) + { + if (lowerCase && onlyPublic) + return publicLowerCaseStrategy; + if (lowerCase && !onlyPublic) + return privateLowerCaseStrategy; + if (!lowerCase && onlyPublic) + return publicUpperCaseStrategy; + return privateUpperCaseStrategy; + } + + /// + /// Convert from PascalCase to camelCase. + /// + private static string ToJsonPropertyName(string propertyName) + { + Guard.ArgumentNotNullOrWhiteSpace(propertyName, "propertyName"); + int i = 0; + while (i < propertyName.Length && char.IsUpper(propertyName[i])) + i++; + return propertyName.Substring(0, i).ToLowerInvariant() + propertyName.Substring(i); + } + + class JsonSerializationStrategy : PocoJsonSerializerStrategy + { + private bool toLowerCase = false; + private bool onlyPublic = true; + + public JsonSerializationStrategy(bool toLowerCase, bool onlyPublic) + { + this.toLowerCase = toLowerCase; + this.onlyPublic = onlyPublic; + } + + protected override bool CanAddField(FieldInfo field) + { + var canAdd = base.CanAddField(field); + return canAdd && ((onlyPublic && field.IsPublic) || !onlyPublic); + } + + protected override bool CanAddProperty(PropertyInfo property, MethodInfo method) + { + var canAdd = base.CanAddProperty(property, method); + if (!canAdd) + return false; + + // we always serialize public things + if (method.IsPublic) + return true; + + // if the getter is private and we're only serializing public things, skip this property + if (onlyPublic && method.Name.StartsWith("get_")) + return false; + + return true; + } + + protected override string MapClrMemberNameToJsonFieldName(string clrPropertyName) + { + if (!toLowerCase) + return base.MapClrMemberNameToJsonFieldName(clrPropertyName); + return ToJsonPropertyName(clrPropertyName); + } + } } } // ReSharper restore LoopCanBeConvertedToQuery diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 737209420..3482b4d13 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -47,7 +47,7 @@ private UsageStore LoadUsage() json = storePath.ReadAllText(Encoding.UTF8); if (json != null) { - result = SimpleJson.DeserializeObject(json); + result = json.FromJson(); } } catch (Exception ex) @@ -83,7 +83,7 @@ private void SaveUsage(UsageStore store) try { - var json = SimpleJson.SerializeObject(store); + var json = store.ToJson(); storePath.WriteAllText(json, Encoding.UTF8); } catch (Exception ex) diff --git a/src/GitHub.Api/Platform/Settings.cs b/src/GitHub.Api/Platform/Settings.cs index 8f622cf3a..e32011398 100644 --- a/src/GitHub.Api/Platform/Settings.cs +++ b/src/GitHub.Api/Platform/Settings.cs @@ -125,7 +125,7 @@ private void LoadFromCache(string path) try { - cacheData = SimpleJson.DeserializeObject(data); + cacheData = data.FromJson(); } catch(Exception ex) { @@ -149,7 +149,7 @@ private bool SaveToCache(string path) try { - var data = SimpleJson.SerializeObject(cacheData); + var data = cacheData.ToJson(); writeAllText(path, data); } catch (Exception ex) diff --git a/src/GitHub.Logging/LogHelper.cs b/src/GitHub.Logging/LogHelper.cs index c2709817f..2c0dc35d3 100644 --- a/src/GitHub.Logging/LogHelper.cs +++ b/src/GitHub.Logging/LogHelper.cs @@ -45,11 +45,13 @@ public static ILogging Instance set { instance = value; } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] public static ILogging GetLogger() { return GetLogger(typeof(T)); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public static ILogging GetLogger(Type type) { return GetLogger(type.Name); From 4ab676733484e70302f513beb18767441fb7b522 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 26 Apr 2018 11:56:32 -0400 Subject: [PATCH 051/567] Prototyping checking for updates --- GitHub.Unity.sln | 9 + common/build.targets | 6 +- common/properties.props | 8 +- lib/.gitignore | 3 +- lib/Managed/.gitignore | 0 .../Unity/TestRunner/Editor/.gitignore | 0 script | 2 +- src/UnityExtension/.gitignore | 4 + .../Assets/Editor/GitHub.Unity/EntryPoint.cs | 2 + .../Editor/GitHub.Unity/GitHub.Unity.asmdef | 8 + .../Editor/GitHub.Unity/GitHub.Unity.csproj | 5 +- .../Editor/GitHub.Unity/Misc/Utility.cs | 2 +- .../Assets/Editor/GitHub.Unity/UpdateCheck.cs | 375 ++++++++++++++++++ .../Editor/UnityTests/UnityTests.asmdef | 10 + .../Editor/UnityTests/UnityTests.csproj | 82 ++++ .../Assets/Editor/UnityTests/VersionTests.cs | 185 +++++++++ .../TestWebServer/files/unity/latest.json | 4 + src/tests/TestWebServer/nginx.conf | 10 + 18 files changed, 705 insertions(+), 10 deletions(-) create mode 100644 lib/Managed/.gitignore create mode 100644 lib/UnityExtensions/Unity/TestRunner/Editor/.gitignore create mode 100644 src/UnityExtension/.gitignore create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs create mode 100644 src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef create mode 100644 src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj create mode 100644 src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs create mode 100644 src/tests/TestWebServer/files/unity/latest.json create mode 100644 src/tests/TestWebServer/nginx.conf diff --git a/GitHub.Unity.sln b/GitHub.Unity.sln index db12af918..bfb537388 100644 --- a/GitHub.Unity.sln +++ b/GitHub.Unity.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "src\tests\TestAp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebServer", "src\tests\TestWebServer\TestWebServer.csproj", "{3DD3451C-30FA-4294-A3A9-1E080342F867}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityTests", "src\UnityExtension\Assets\Editor\UnityTests\UnityTests.csproj", "{462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +104,12 @@ Global {3DD3451C-30FA-4294-A3A9-1E080342F867}.dev|Any CPU.Build.0 = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Release|Any CPU.Build.0 = Release|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.ActiveCfg = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.Build.0 = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -115,5 +123,6 @@ Global {1A382F40-FD9E-43E1-89C1-320073F35CE9} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} {08B87D2A-8CF1-4211-B7AA-5209F00F72F8} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} {3DD3451C-30FA-4294-A3A9-1E080342F867} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} EndGlobalSection EndGlobal diff --git a/common/build.targets b/common/build.targets index 450909d08..163a071a8 100644 --- a/common/build.targets +++ b/common/build.targets @@ -16,7 +16,11 @@ - Location of Unity dlls is not set. You'll need to install Unity in a known location (the default installation directory for your system), or copy UnityEngine.dll and UnityEditor.dll to the {0}lib folder + The location of Unity dlls is not set. You'll need to install Unity in a known location (the default installation directory for your system), or: + - Copy UnityEngine.dll and UnityEditor.dll to the {0}lib\Managed folder + - Copy UnityEngine.TestRunner.dll to the {0}lib\UnityExtensions/Unity/TestRunner folder + - Copy UnityEditor.TestRunner.dll to the {0}lib\UnityExtensions/Unity/TestRunner/Editor folder + To build the OctokitDebugging solution you need to have https://github.com/github-for-unity/octokit.net checked out in {0} To build the OctokitDebugging solution you need to have https://github.com/github-for-unity/dotnet-httpclient35 checked out in {0} diff --git a/common/properties.props b/common/properties.props index f8b204df8..60074b739 100644 --- a/common/properties.props +++ b/common/properties.props @@ -5,11 +5,11 @@ Internal ENABLE_METRICS - $(SolutionDir)\script\lib\ - $(SolutionDir)\lib\ - C:\Program Files\Unity\Editor\Data\Managed\ + $(SolutionDir)script\lib\ + $(SolutionDir)lib\ + C:\Program Files\Unity\Editor\Data\ C:\Program Files (x86)\Unity\Editor\Data\Managed\ - \Applications\Unity\Unity.app\Contents\Managed\ + \Applications\Unity\Unity.app\Contents\ Debug $(Configuration) diff --git a/lib/.gitignore b/lib/.gitignore index 1ea843a1c..9432a8bee 100644 --- a/lib/.gitignore +++ b/lib/.gitignore @@ -1 +1,2 @@ -Unity* \ No newline at end of file +Managed +UnityExtensions \ No newline at end of file diff --git a/lib/Managed/.gitignore b/lib/Managed/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/lib/UnityExtensions/Unity/TestRunner/Editor/.gitignore b/lib/UnityExtensions/Unity/TestRunner/Editor/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/script b/script index 73f71c796..f678e6be0 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 73f71c796245ec82f82db57b66ac21c48f529ed3 +Subproject commit f678e6be03fcc984949f1b79b05aa2701349d8ba diff --git a/src/UnityExtension/.gitignore b/src/UnityExtension/.gitignore new file mode 100644 index 000000000..a2411d2f3 --- /dev/null +++ b/src/UnityExtension/.gitignore @@ -0,0 +1,4 @@ +*.csproj +UnityPackageManager +JetBrains +UnityExtension.sln \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 51a0edc40..6fcc9de63 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -69,6 +69,8 @@ private static void Initialize() LogHelper.Info("Initializing GitHub for Unity version " + ApplicationInfo.Version); ApplicationManager.Run(ApplicationCache.Instance.FirstRun); + + UpdateCheckWindow.CheckForUpdates(); } private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef new file mode 100644 index 000000000..7e63aedb3 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.asmdef @@ -0,0 +1,8 @@ +{ + "name": "GitHub.Unity", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [] +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index 33b722ca6..31e37e023 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -67,11 +67,11 @@ - $(UnityDir)UnityEditor.dll + $(UnityDir)Managed\UnityEditor.dll False - $(UnityDir)UnityEngine.dll + $(UnityDir)Managed\UnityEngine.dll False @@ -85,6 +85,7 @@ + diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs index e590d1d4f..6e68dcbdc 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs @@ -26,7 +26,7 @@ public static Texture2D GetIcon(string filename, string filename2x = "") } else { - var iconPath = EntryPoint.Environment.ExtensionInstallPath.Combine("IconsAndLogos", filename).ToString(SlashMode.Forward); + var iconPath = "Assets/Editor/GitHubUnity/IconsAndLogos/" + filename; texture2D = AssetDatabase.LoadAssetAtPath(iconPath); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs new file mode 100644 index 000000000..f6eb7173a --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs @@ -0,0 +1,375 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + public struct TheVersion : IComparable + { + const string versionRegex = @"(?\d+)(\.?(?[^.]+))?(\.?(?[^.]+))?(\.?(?.+))?"; + const int PART_COUNT = 4; + + [NotSerialized] private int major; + [NotSerialized] public int Major { get { Initialize(original); return major; } } + [NotSerialized] private int minor; + [NotSerialized] public int Minor { get { Initialize(original); return minor; } } + [NotSerialized] private int patch; + [NotSerialized] public int Patch { get { Initialize(original); return patch; } } + [NotSerialized] private int build; + [NotSerialized] public int Build { get { Initialize(original); return build; } } + [NotSerialized] private string special; + [NotSerialized] public string Special { get { Initialize(original); return special; } } + [NotSerialized] private bool isAlpha; + [NotSerialized] public bool IsAlpha { get { Initialize(original); return isAlpha; } } + [NotSerialized] private bool isBeta; + [NotSerialized] public bool IsBeta { get { Initialize(original); return isBeta; } } + [NotSerialized] private bool isUnstable; + [NotSerialized] public bool IsUnstable { get { Initialize(original); return isUnstable; } } + + [NotSerialized] private int[] intParts; + [NotSerialized] private string[] stringParts; + [NotSerialized] private int parts; + [NotSerialized] private bool initialized; + + private string original; + + private static readonly Regex regex = new Regex(versionRegex); + + public static TheVersion Parse(string version) + { + Guard.ArgumentNotNull(version, "version"); + TheVersion ret = default(TheVersion); + ret.Initialize(version); + return ret; + } + + private void Initialize(string version) + { + if (initialized) + return; + + original = version; + + isAlpha = false; + isBeta = false; + major = 0; + minor = 0; + patch = 0; + build = 0; + special = null; + parts = 0; + + intParts = new int[PART_COUNT]; + stringParts = new string[PART_COUNT]; + + for (var i = 0; i < PART_COUNT; i++) + stringParts[i] = intParts[i].ToString(); + + var match = regex.Match(version); + if (!match.Success) + throw new ArgumentException("Invalid version: " + version, "version"); + + major = int.Parse(match.Groups["major"].Value); + intParts[0] = major; + parts = 1; + + var minorMatch = match.Groups["minor"]; + var patchMatch = match.Groups["patch"]; + var buildMatch = match.Groups["build"]; + + if (minorMatch.Success) + { + parts++; + if (!int.TryParse(minorMatch.Value, out minor)) + { + special = minorMatch.Value; + stringParts[parts - 1] = special; + } + else + { + intParts[parts - 1] = minor; + + if (patchMatch.Success) + { + parts++; + if (!int.TryParse(patchMatch.Value, out patch)) + { + special = patchMatch.Value; + stringParts[parts - 1] = special; + } + else + { + intParts[parts - 1] = patch; + + if (buildMatch.Success) + { + parts++; + if (!int.TryParse(buildMatch.Value, out build)) + { + special = buildMatch.Value; + stringParts[parts - 1] = special; + } + else + { + intParts[parts - 1] = build; + } + } + } + } + } + } + + isUnstable = special != null; + if (isUnstable) + { + isAlpha = special.IndexOf("alpha") >= 0; + isBeta = special.IndexOf("beta") >= 0; + } + } + + public override string ToString() + { + return original; + } + + public int CompareTo(TheVersion other) + { + if (this > other) + return 1; + if (this == other) + return 0; + return -1; + } + + public override int GetHashCode() + { + int hash = 17; + hash = hash * 23 + Major.GetHashCode(); + hash = hash * 23 + Minor.GetHashCode(); + hash = hash * 23 + Patch.GetHashCode(); + hash = hash * 23 + Build.GetHashCode(); + hash = hash * 23 + (Special != null ? Special.GetHashCode() : 0); + return hash; + } + + public override bool Equals(object obj) + { + if (obj is TheVersion) + return Equals((TheVersion)obj); + return false; + } + + public bool Equals(TheVersion other) + { + return this == other; + } + + public static bool operator==(TheVersion lhs, TheVersion rhs) + { + if (lhs.original == rhs.original) + return true; + return + (lhs.Major == rhs.Major) && + (lhs.Minor == rhs.Minor) && + (lhs.Patch == rhs.Patch) && + (lhs.Build == rhs.Build) && + (lhs.Special == rhs.Special); + } + + public static bool operator!=(TheVersion lhs, TheVersion rhs) + { + return !(lhs == rhs); + } + + public static bool operator>(TheVersion lhs, TheVersion rhs) + { + if (lhs.original == rhs.original) + return false; + if (lhs.original == null) + return false; + if (rhs.original == null) + return true; + + for (var i = 0; i < PART_COUNT; i++) + { + if (lhs.intParts[i] != rhs.intParts[i]) + return lhs.intParts[i] > rhs.intParts[i]; + } + + for (var i = 1; i < PART_COUNT; i++) + { + if (lhs.stringParts[i] != rhs.stringParts[i]) + { + return GreaterThan(lhs.stringParts[i], rhs.stringParts[i]); + } + } + return false; + } + + public static bool operator<(TheVersion lhs, TheVersion rhs) + { + return !(lhs > rhs); + } + + public static bool operator>=(TheVersion lhs, TheVersion rhs) + { + return lhs > rhs || lhs == rhs; + } + + public static bool operator<=(TheVersion lhs, TheVersion rhs) + { + return lhs < rhs || lhs == rhs; + } + + private static bool GreaterThan(string lhs, string rhs) + { + var lhsNonDigitPos = IndexOfFirstNonDigit(lhs); + var rhsNonDigitPos = IndexOfFirstNonDigit(rhs); + + var lhsNumber = -1; + if (lhsNonDigitPos > -1) + { + lhsNumber = int.Parse(lhs.Substring(0, lhsNonDigitPos)); + } + else + { + int.TryParse(lhs, out lhsNumber); + } + + var rhsNumber = -1; + if (rhsNonDigitPos > -1) + { + rhsNumber = int.Parse(rhs.Substring(0, rhsNonDigitPos)); + } + else + { + int.TryParse(rhs, out rhsNumber); + } + + if (lhsNumber != rhsNumber) + return lhsNumber > rhsNumber; + + return lhs.Substring(lhsNonDigitPos > -1 ? lhsNonDigitPos : 0).CompareTo(rhs.Substring(rhsNonDigitPos > -1 ? rhsNonDigitPos : 0)) > 0; + } + + private static int IndexOfFirstNonDigit(string str) + { + for (var i = 0; i < str.Length; i++) + { + if (!char.IsDigit(str[i])) + { + return i; + } + } + return -1; + } + } + + [Serializable] + public class Package + { + public string Url { get; set; } + public string Version { get; set; } + } + + public class UpdateCheckWindow : EditorWindow + { + //public const string UpdateFeedUrl = "https://ghfvs-installer.github.com/unity/latest.json"; + public const string UpdateFeedUrl = "http://localhost:8081/unity/latest.json"; + + public static void CheckForUpdates() + { + var download = new DownloadTask(TaskManager.Instance.Token, EntryPoint.Environment.FileSystem, UpdateFeedUrl, EntryPoint.Environment.UserCachePath); + download.OnEnd += (thisTask, result, success, exception) => + { + if (success) + { + try + { + var json = result.ReadAllText(); + var package = SimpleJson.DeserializeObject(json); + + var current = TheVersion.Parse(ApplicationInfo.Version); + var latest = TheVersion.Parse(package.Version); + if (latest > current) + { + TaskManager.Instance.RunInUI(() => + { + NotifyOfNewUpdate(package); + }); + } + } + catch(Exception ex) + { + Debug.LogError(ex); + } + } + }; + download.Start(); + } + + private static void NotifyOfNewUpdate(Package package) + { + var window = GetWindow(); + window.Initialize(package); + window.Show(); + } + + + [SerializeField] private string latestVersion; + [SerializeField] private string url; + + private void Initialize(Package package) + { + latestVersion = package.Version; + url = package.Url; + } + + private void OnGUI() + { + Styles.BeginInitialStateArea("There's a new update!", String.Format("Latest version: {0} at {1}", latestVersion, url)); + Styles.EndInitialStateArea(); + } + + } + + public static class JsonSerializerExtensions + { + static JsonSerializationStrategy strategy = new JsonSerializationStrategy(); + public static string ToJson(this T model, bool toLowerCase = false) + { + if (toLowerCase) + return SimpleJson.SerializeObject(model, strategy); + return SimpleJson.SerializeObject(model); + } + + public static T FromJson(this string json, bool fromLowerCase = false) + { + if (fromLowerCase) + return SimpleJson.DeserializeObject(json, strategy); + return SimpleJson.DeserializeObject(json); + } + + /// + /// Convert from PascalCase to camelCase. + /// + static string ToJsonPropertyName(string propertyName) + { + Guard.ArgumentNotNullOrWhiteSpace(propertyName, "propertyName"); + int i = 0; + while (i < propertyName.Length && char.IsUpper(propertyName[i])) + i++; + return propertyName.Substring(0, i).ToLowerInvariant() + propertyName.Substring(i); + } + + class JsonSerializationStrategy : PocoJsonSerializerStrategy + { + protected override string MapClrMemberNameToJsonFieldName(string clrPropertyName) + { + return ToJsonPropertyName(clrPropertyName); + } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef new file mode 100644 index 000000000..eef024216 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.asmdef @@ -0,0 +1,10 @@ +{ + "name": "UnityTests", + "references": [ + "GitHub.Unity" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [] +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj new file mode 100644 index 000000000..3d46efc86 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj @@ -0,0 +1,82 @@ + + + + 4 + + + Debug + AnyCPU + 10.0.20506 + 2.0 + + + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5} + Library + Properties + UnityTests + v4.5 + 512 + ..\..\..\obj\ + + + + true + full + false + ..\..\..\bin\Debug\ + DEBUG;TRACE + prompt + 4 + 0169 + true + + + pdbonly + true + ..\..\..\bin\Release\ + prompt + 4 + 0169 + true + + + + + + + + + $(UnityDir)Managed\UnityEditor.dll + + + $(UnityDir)Managed\UnityEngine.dll + + + $(UnityDir)UnityExtensions\Unity\TestRunner\Editor\UnityEditor.TestRunner.dll + + + $(UnityDir)UnityExtensions\Unity\TestRunner\UnityEngine.TestRunner.dll + + + $(SolutionDir)packages\NUnit.2.6.4\lib\nunit.framework.dll + False + + + + + + + + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A} + GitHub.Unity + + + + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs b/src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs new file mode 100644 index 000000000..21ec8047f --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/VersionTests.cs @@ -0,0 +1,185 @@ +using UnityEngine; +using UnityEditor; +using UnityEngine.TestTools; +using NUnit.Framework; +using System.Collections; +using GitHub.Unity; + +public class VersionTests +{ + [Test] + public void OnePart_IsValid() + { + var version = TheVersion.Parse("2"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(0, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void TwoParts_IsValid() + { + var version = TheVersion.Parse("2.1"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void ThreeParts_IsValid() + { + var version = TheVersion.Parse("2.1.32"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(32, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void FourParts_IsValid() + { + var version = TheVersion.Parse("2.1.32.5"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(32, version.Patch); + Assert.AreEqual(5, version.Build); + Assert.AreEqual(null, version.Special); + } + + [Test] + public void TwoPartsWithAlpha_IsValid() + { + var version = TheVersion.Parse("2.1alpha1"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual("1alpha1", version.Special); + Assert.AreEqual(0, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + } + + [Test] + public void ThreePartsWithAlpha_IsValid() + { + var version = TheVersion.Parse("2.1.3beta"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual("3beta", version.Special); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + } + + [Test] + public void FourPartsWithAlpha_IsValid() + { + var version = TheVersion.Parse("2.1.32.delta"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(32, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual("delta", version.Special); + } + + [Test] + public void ParsingStopsAtAlpha() + { + var version = TheVersion.Parse("2.1.1beta2.3"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual(1, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + Assert.AreEqual("1beta2", version.Special); + + version = TheVersion.Parse("2.3beta2.3alpha.4"); + Assert.AreEqual(2, version.Major); + Assert.AreEqual("3beta2", version.Special); + Assert.AreEqual(0, version.Minor); + Assert.AreEqual(0, version.Patch); + Assert.AreEqual(0, version.Build); + } + + [Test] + public void EqualsWorks() + { + var version1 = TheVersion.Parse("2.1"); + var version2 = TheVersion.Parse("2.1.0.0"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2"); + version2 = TheVersion.Parse("2.0.0.0"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.1.3"); + version2 = TheVersion.Parse("2.1.3.0"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.alpha1.1.2"); + version2 = TheVersion.Parse("2.alpha1"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.1.alpha1.1"); + version2 = TheVersion.Parse("2.1.alpha1"); + Assert.AreEqual(version1, version2); + + version1 = TheVersion.Parse("2.1.3.alpha1"); + version2 = TheVersion.Parse("2.1.3.alpha1"); + Assert.AreEqual(version1, version2); + } + + [Test] + public void ComparisonWorks() + { + var version1 = TheVersion.Parse("2"); + var version2 = TheVersion.Parse("1"); + Assert.IsTrue(version1 > version2); + + version1 = TheVersion.Parse("1"); + version2 = TheVersion.Parse("1.1alpha1"); + Assert.IsTrue(version2 > version1); + + version1 = TheVersion.Parse("1.0"); + version2 = TheVersion.Parse("1.1alpha1"); + Assert.IsTrue(version1 < version2); + + version1 = TheVersion.Parse("1.1"); + version2 = TheVersion.Parse("1.1alpha1"); + Assert.IsTrue(version1 > version2); + + version1 = TheVersion.Parse("1.1"); + version2 = TheVersion.Parse("1.2.3alpha1"); + Assert.IsTrue(version1 <= version2); + + version1 = TheVersion.Parse("1.2.3"); + version2 = TheVersion.Parse("1.2.3alpha1"); + Assert.IsTrue(version1 >= version2); + } + + [Test] + public void DetectingUnstableVersionsWorks() + { + var version = TheVersion.Parse("2"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2.3"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2.3.4"); + Assert.IsFalse(version.IsUnstable); + + version = TheVersion.Parse("1.2alpha1"); + Assert.IsTrue(version.IsUnstable); + + version = TheVersion.Parse("1.2.3stuff"); + Assert.IsTrue(version.IsUnstable); + + version = TheVersion.Parse("1.2.3.4whatever"); + Assert.IsTrue(version.IsUnstable); + } +} diff --git a/src/tests/TestWebServer/files/unity/latest.json b/src/tests/TestWebServer/files/unity/latest.json new file mode 100644 index 000000000..9aa4f31eb --- /dev/null +++ b/src/tests/TestWebServer/files/unity/latest.json @@ -0,0 +1,4 @@ +{ + "Url": "http://localhost:8080/unity/update.zip", + "Version": "1.1" +} \ No newline at end of file diff --git a/src/tests/TestWebServer/nginx.conf b/src/tests/TestWebServer/nginx.conf new file mode 100644 index 000000000..1daca4370 --- /dev/null +++ b/src/tests/TestWebServer/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 8081; + listen [::]:8081; + + root ~/code/work/github/Unity/src/tests/TestWebServer/files; + server_name unity.localhost; + + location / { + } +} From 61da6fab48ea2561334f8fa75590e7d540048dee Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 3 May 2018 16:41:15 +0200 Subject: [PATCH 052/567] Update lfs to 2.4.0 --- src/GitHub.Api/IO/Utils.cs | 4 ++-- src/GitHub.Api/Installer/GitInstaller.cs | 7 ++----- src/GitHub.Api/PlatformResources/linux/git-lfs.zip | 4 ++-- src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 | 2 +- src/GitHub.Api/PlatformResources/mac/git-lfs.zip | 4 ++-- src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 | 2 +- src/GitHub.Api/PlatformResources/windows/git-lfs.zip | 4 ++-- src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 | 2 +- src/tests/IntegrationTests/Git/GitClientTests.cs | 4 ++-- .../GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 | 2 +- .../GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 | 2 +- .../Editor/PlatformResources/windows/git-lfs.zip.md5 | 2 +- .../GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 | 2 +- .../GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 | 2 +- .../Editor/PlatformResources/windows/git-lfs.zip.md5 | 2 +- 15 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/GitHub.Api/IO/Utils.cs b/src/GitHub.Api/IO/Utils.cs index fb4358dd9..33194585d 100644 --- a/src/GitHub.Api/IO/Utils.cs +++ b/src/GitHub.Api/IO/Utils.cs @@ -82,9 +82,9 @@ public static bool Copy(Stream source, Stream destination, } public static bool VerifyFileIntegrity(NPath file, NPath md5file) { - var expected = md5file.ReadAllText(); + var expected = md5file.ReadAllText().Trim(); var actual = file.CalculateMD5(); return expected.Equals(actual, StringComparison.InvariantCultureIgnoreCase); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 772878462..d494dd717 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -304,14 +304,11 @@ public class GitInstallDetails public const string DefaultGitLfsZipMd5Url = "https://ghfvs-installer.github.com/unity/git/windows/git-lfs.zip.md5"; public const string DefaultGitLfsZipUrl = "https://ghfvs-installer.github.com/unity/git/windows/git-lfs.zip"; - public const string GitExtractedMD5 = "e6cfc0c294a2312042f27f893dfc9c0a"; - public const string GitLfsExtractedMD5 = "36e3ae968b69fbf42dff72311040d24a"; - public const string WindowsGitExecutableMD5 = "50570ed932559f294d1a1361801740b9"; public const string MacGitExecutableMD5 = ""; - public const string WindowsGitLfsExecutableMD5 = "177bb14d0c08f665a24f0d5516c3b080"; - public const string MacGitLfsExecutableMD5 = "f81a1a065a26a4123193e8fd96c561ad"; + public const string WindowsGitLfsExecutableMD5 = "4294df6cbb467b8133553570450757c7"; + public const string MacGitLfsExecutableMD5 = "d0d59164a4b7b35685502d7c5f747f2f"; private const string PackageVersion = "f02737a78695063deace08e96d5042710d3e32db"; private const string PackageName = "PortableGit"; diff --git a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip index c1efa41e2..9ab5a9b0a 100644 --- a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip +++ b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c86ab0b766e2d523ba2bf2ae427fe755e553989f7160217380b0f4dead7b802b -size 2631425 +oid sha256:15269ac992592484612ddcb760363294c1978fd79807f75b8536818db7c6f9e7 +size 2681983 diff --git a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 index 68e72bb08..9ea13a125 100644 --- a/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 +++ b/src/GitHub.Api/PlatformResources/linux/git-lfs.zip.md5 @@ -1 +1 @@ -3cde251dc13fe09ef62a2a2227fcc310 \ No newline at end of file +81ea7bb262838e3779e4bdc426086e27 diff --git a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip index 3932710f3..c920a8f42 100644 --- a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip +++ b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bde4722bdbb24ec6651aa2ab559bfa6e85d31ee9e4195c81f6d6fa7548cacc6 -size 2905910 +oid sha256:59a817fa5fa9cbd38eff9422eb576f31e3a225d5be387b292dc827faddfe785b +size 2831355 diff --git a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 index e1f3c0a06..828d6cde9 100644 --- a/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 +++ b/src/GitHub.Api/PlatformResources/mac/git-lfs.zip.md5 @@ -1 +1 @@ -fb5862c66d8d53ba4eb9599419dffa1f \ No newline at end of file +108b2832cc3d1e6f1ffe792069ad3d21 diff --git a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip index 5a56712a7..ed0b4e3f6 100644 --- a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip +++ b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a4699fe6028a3727d76b218a10a7e9c6276f097b8ebd782f2e7b3418dacda07 -size 2652291 +oid sha256:451d565d2f2b54910dc50f8522b848457001ad76fdcec0f5207e53cff80b8a7a +size 2677508 diff --git a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 index f99b259ad..c20729fe7 100644 --- a/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 +++ b/src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 @@ -1 +1 @@ -105df1302560c5f6aa64d1930284c126 \ No newline at end of file +78ff68661485e1c09f7b8d82c4317ab5 diff --git a/src/tests/IntegrationTests/Git/GitClientTests.cs b/src/tests/IntegrationTests/Git/GitClientTests.cs index 6f2101613..d7b64ca7c 100644 --- a/src/tests/IntegrationTests/Git/GitClientTests.cs +++ b/src/tests/IntegrationTests/Git/GitClientTests.cs @@ -47,8 +47,8 @@ public async Task ShouldGetGitLfsVersion() Assert.AreEqual(task.Task, taskDone); var result = await task.Task; - var expected = new Version(2,3,4); + var expected = new Version(2,4,0); result.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 b/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 index 68e72bb08..9ea13a125 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 @@ -1 +1 @@ -3cde251dc13fe09ef62a2a2227fcc310 \ No newline at end of file +81ea7bb262838e3779e4bdc426086e27 diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 b/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 index e1f3c0a06..828d6cde9 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 @@ -1 +1 @@ -fb5862c66d8d53ba4eb9599419dffa1f \ No newline at end of file +108b2832cc3d1e6f1ffe792069ad3d21 diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 b/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 index f99b259ad..c20729fe7 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 @@ -1 +1 @@ -105df1302560c5f6aa64d1930284c126 \ No newline at end of file +78ff68661485e1c09f7b8d82c4317ab5 diff --git a/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 b/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 index 68e72bb08..9ea13a125 100644 --- a/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 +++ b/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/linux/git-lfs.zip.md5 @@ -1 +1 @@ -3cde251dc13fe09ef62a2a2227fcc310 \ No newline at end of file +81ea7bb262838e3779e4bdc426086e27 diff --git a/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 b/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 index e1f3c0a06..828d6cde9 100644 --- a/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 +++ b/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/mac/git-lfs.zip.md5 @@ -1 +1 @@ -fb5862c66d8d53ba4eb9599419dffa1f \ No newline at end of file +108b2832cc3d1e6f1ffe792069ad3d21 diff --git a/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 b/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 index f99b259ad..c20729fe7 100644 --- a/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 +++ b/unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 @@ -1 +1 @@ -105df1302560c5f6aa64d1930284c126 \ No newline at end of file +78ff68661485e1c09f7b8d82c4317ab5 From 9ac3634456d207f28a0e2a0886f2b9c5ae70f6f3 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 4 May 2018 12:12:21 +0200 Subject: [PATCH 053/567] Add latest version feed capabilities Add a couple of serializable types representing information about a package - url to download it from, version, release notes, any other message we want to show in the UI when we inform the user of an update. Extend the TestApp command line tool (that we're using to test shelling out to the command Add a tool to generate the package json for a given version and host - we always assume that we're serving from [host]/unity/releases/github-for-unity-[version].unitypackage. Rename TestApp to CommandLine Add an option to run a local web server in the CommandLine app, so we can test update feeds and other download tasks locally. --- GitHub.Unity.sln | 24 +++- generate-version.sh | 21 +++ run-test-webserver.sh | 16 +++ src/GitHub.Logging/ConsoleLogAdapter.cs | 8 +- src/GitHub.Logging/FileLogAdapter.cs | 2 +- src/GitHub.Logging/MultipleLogAdapter.cs | 4 +- src/GitHub.Logging/NullLogAdapter.cs | 4 +- .../Assets/Editor/GitHub.Unity/UpdateCheck.cs | 42 +++--- src/tests/{TestApp => CommandLine}/App.config | 4 +- .../CommandLine.csproj} | 27 +++- .../CommandLine.v3.ncrunchproject} | 0 .../Mono.Options-PCL.cs | 12 +- src/tests/CommandLine/Program.cs | 127 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 10 +- src/tests/TestApp/Program.cs | 57 -------- src/tests/TestWebServer/HttpServer.cs | 3 +- src/tests/TestWebServer/TestWebServer.csproj | 22 +-- .../TestWebServer/files/unity/latest.json | 5 +- src/tests/TestWebServer/nginx.conf | 10 -- 19 files changed, 271 insertions(+), 127 deletions(-) create mode 100644 generate-version.sh create mode 100644 run-test-webserver.sh rename src/tests/{TestApp => CommandLine}/App.config (84%) rename src/tests/{TestApp/TestApp.csproj => CommandLine/CommandLine.csproj} (69%) rename src/tests/{TestApp/TestApp.v3.ncrunchproject => CommandLine/CommandLine.v3.ncrunchproject} (100%) rename src/tests/{TestApp => CommandLine}/Mono.Options-PCL.cs (99%) create mode 100644 src/tests/CommandLine/Program.cs rename src/tests/{TestApp => CommandLine}/Properties/AssemblyInfo.cs (94%) delete mode 100644 src/tests/TestApp/Program.cs delete mode 100644 src/tests/TestWebServer/nginx.conf diff --git a/GitHub.Unity.sln b/GitHub.Unity.sln index bfb537388..0197b47fc 100644 --- a/GitHub.Unity.sln +++ b/GitHub.Unity.sln @@ -25,7 +25,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtils", "src\tests\Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskSystem", "src\tests\TaskSystemIntegrationTests\TaskSystem.csproj", "{1A382F40-FD9E-43E1-89C1-320073F35CE9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "src\tests\TestApp\TestApp.csproj", "{08B87D2A-8CF1-4211-B7AA-5209F00F72F8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLine", "src\tests\CommandLine\CommandLine.csproj", "{08B87D2A-8CF1-4211-B7AA-5209F00F72F8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebServer", "src\tests\TestWebServer\TestWebServer.csproj", "{3DD3451C-30FA-4294-A3A9-1E080342F867}" EndProject @@ -34,78 +34,100 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugNoUnity|Any CPU = DebugNoUnity|Any CPU dev|Any CPU = dev|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.dev|Any CPU.ActiveCfg = dev|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.dev|Any CPU.Build.0 = dev|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Release|Any CPU.Build.0 = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.dev|Any CPU.ActiveCfg = dev|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.dev|Any CPU.Build.0 = dev|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.ActiveCfg = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.Build.0 = Release|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.dev|Any CPU.ActiveCfg = dev|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.dev|Any CPU.Build.0 = dev|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Release|Any CPU.Build.0 = Release|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.dev|Any CPU.ActiveCfg = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.dev|Any CPU.Build.0 = Debug|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {44257C81-EE4A-4817-9AF4-A26C02AA6DD4}.Release|Any CPU.Build.0 = Release|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.dev|Any CPU.ActiveCfg = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.dev|Any CPU.Build.0 = Debug|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {69F13D9D-AD56-4EEC-AE10-D528EE23E1A9}.Release|Any CPU.Build.0 = Release|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.dev|Any CPU.ActiveCfg = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.dev|Any CPU.Build.0 = Debug|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AC3F82E-AEAE-4C84-825C-207BB264FCFA}.Release|Any CPU.Build.0 = Release|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DEF4226-7740-457F-9199-34174C49A978}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.dev|Any CPU.ActiveCfg = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.dev|Any CPU.Build.0 = Debug|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DEF4226-7740-457F-9199-34174C49A978}.Release|Any CPU.Build.0 = Release|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.dev|Any CPU.ActiveCfg = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.dev|Any CPU.Build.0 = Debug|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Release|Any CPU.ActiveCfg = Release|Any CPU {66A1D219-F61D-4AE4-9BD7-AAEB97276FFF}.Release|Any CPU.Build.0 = Release|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A382F40-FD9E-43E1-89C1-320073F35CE9}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {1A382F40-FD9E-43E1-89C1-320073F35CE9}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.dev|Any CPU.ActiveCfg = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.dev|Any CPU.Build.0 = Debug|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A382F40-FD9E-43E1-89C1-320073F35CE9}.Release|Any CPU.Build.0 = Release|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.dev|Any CPU.ActiveCfg = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.dev|Any CPU.Build.0 = Debug|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {08B87D2A-8CF1-4211-B7AA-5209F00F72F8}.Release|Any CPU.Build.0 = Release|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DD3451C-30FA-4294-A3A9-1E080342F867}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {3DD3451C-30FA-4294-A3A9-1E080342F867}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.dev|Any CPU.ActiveCfg = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.dev|Any CPU.Build.0 = Debug|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DD3451C-30FA-4294-A3A9-1E080342F867}.Release|Any CPU.Build.0 = Release|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.ActiveCfg = Debug|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.Build.0 = Debug|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/generate-version.sh b/generate-version.sh new file mode 100644 index 000000000..1c74a423e --- /dev/null +++ b/generate-version.sh @@ -0,0 +1,21 @@ +#!/bin/sh -eu +if [ $# -lt 1 ]; then + echo "Usage: generate-version.sh [version] [host url (default: http://ghfvs-installer.github.com)]" + exit 1 +fi + +URL="http://ghfvs-installer.github.com" +if [ $# -eq 2 ]; then + URL=$2 +fi + +EXEC="mono" +if [ -e "/c/" ]; then + EXEC="" +fi + +if [ ! -e build/CommandLine/CommandLine.exe ]; then + >&2 xbuild /target:CommandLine GitHub.Unity.sln /verbosity:minimal +fi + +"$EXEC" build/CommandLine/CommandLine.exe -g -v "$1" -u "$URL" diff --git a/run-test-webserver.sh b/run-test-webserver.sh new file mode 100644 index 000000000..498615367 --- /dev/null +++ b/run-test-webserver.sh @@ -0,0 +1,16 @@ +#!/bin/sh -eu +PORT="55555" +if [ $# -eq 1 ]; then + PORT="$1" +fi + +EXEC="mono " +if [ -e "/c/" ]; then + EXEC="" +fi + +if [ ! -e build/CommandLine/CommandLine.exe ]; then + >&2 xbuild /target:CommandLine GitHub.Unity.sln /verbosity:minimal +fi + +"$EXEC"build/CommandLine/CommandLine.exe --web --port $PORT diff --git a/src/GitHub.Logging/ConsoleLogAdapter.cs b/src/GitHub.Logging/ConsoleLogAdapter.cs index 4fc945c36..bf8a5951c 100644 --- a/src/GitHub.Logging/ConsoleLogAdapter.cs +++ b/src/GitHub.Logging/ConsoleLogAdapter.cs @@ -3,7 +3,7 @@ namespace GitHub.Logging { - class ConsoleLogAdapter : LogAdapterBase + public class ConsoleLogAdapter : LogAdapterBase { private string GetMessage(string level, string context, string message) { @@ -14,7 +14,7 @@ private string GetMessage(string level, string context, string message) public override void Info(string context, string message) { - WriteLine("INFO", context, message); + Console.WriteLine(message); } public override void Debug(string context, string message) @@ -34,7 +34,7 @@ public override void Warning(string context, string message) public override void Error(string context, string message) { - WriteLine("ERROR", context, message); + Console.Error.WriteLine(message); } private void WriteLine(string level, string context, string message) @@ -42,4 +42,4 @@ private void WriteLine(string level, string context, string message) Console.WriteLine(GetMessage(level, context, message)); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Logging/FileLogAdapter.cs b/src/GitHub.Logging/FileLogAdapter.cs index 693dbc296..9c6dde452 100644 --- a/src/GitHub.Logging/FileLogAdapter.cs +++ b/src/GitHub.Logging/FileLogAdapter.cs @@ -4,7 +4,7 @@ namespace GitHub.Logging { - class FileLogAdapter : LogAdapterBase + public class FileLogAdapter : LogAdapterBase { private static readonly object lk = new object(); private readonly string filePath; diff --git a/src/GitHub.Logging/MultipleLogAdapter.cs b/src/GitHub.Logging/MultipleLogAdapter.cs index f9daf572a..df9137b8e 100644 --- a/src/GitHub.Logging/MultipleLogAdapter.cs +++ b/src/GitHub.Logging/MultipleLogAdapter.cs @@ -1,6 +1,6 @@ namespace GitHub.Logging { - class MultipleLogAdapter : LogAdapterBase + public class MultipleLogAdapter : LogAdapterBase { private readonly LogAdapterBase[] logAdapters; @@ -49,4 +49,4 @@ public override void Error(string context, string message) } } } -} \ No newline at end of file +} diff --git a/src/GitHub.Logging/NullLogAdapter.cs b/src/GitHub.Logging/NullLogAdapter.cs index 3d0e78724..7c5b672f7 100644 --- a/src/GitHub.Logging/NullLogAdapter.cs +++ b/src/GitHub.Logging/NullLogAdapter.cs @@ -1,6 +1,6 @@ namespace GitHub.Logging { - class NullLogAdapter : LogAdapterBase + public class NullLogAdapter : LogAdapterBase { public override void Info(string context, string message) { @@ -22,4 +22,4 @@ public override void Error(string context, string message) { } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs index f6eb7173a..1a50e5a78 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs @@ -12,28 +12,28 @@ public struct TheVersion : IComparable const int PART_COUNT = 4; [NotSerialized] private int major; - [NotSerialized] public int Major { get { Initialize(original); return major; } } + [NotSerialized] public int Major { get { Initialize(version); return major; } } [NotSerialized] private int minor; - [NotSerialized] public int Minor { get { Initialize(original); return minor; } } + [NotSerialized] public int Minor { get { Initialize(version); return minor; } } [NotSerialized] private int patch; - [NotSerialized] public int Patch { get { Initialize(original); return patch; } } + [NotSerialized] public int Patch { get { Initialize(version); return patch; } } [NotSerialized] private int build; - [NotSerialized] public int Build { get { Initialize(original); return build; } } + [NotSerialized] public int Build { get { Initialize(version); return build; } } [NotSerialized] private string special; - [NotSerialized] public string Special { get { Initialize(original); return special; } } + [NotSerialized] public string Special { get { Initialize(version); return special; } } [NotSerialized] private bool isAlpha; - [NotSerialized] public bool IsAlpha { get { Initialize(original); return isAlpha; } } + [NotSerialized] public bool IsAlpha { get { Initialize(version); return isAlpha; } } [NotSerialized] private bool isBeta; - [NotSerialized] public bool IsBeta { get { Initialize(original); return isBeta; } } + [NotSerialized] public bool IsBeta { get { Initialize(version); return isBeta; } } [NotSerialized] private bool isUnstable; - [NotSerialized] public bool IsUnstable { get { Initialize(original); return isUnstable; } } + [NotSerialized] public bool IsUnstable { get { Initialize(version); return isUnstable; } } [NotSerialized] private int[] intParts; [NotSerialized] private string[] stringParts; [NotSerialized] private int parts; [NotSerialized] private bool initialized; - private string original; + private string version; private static readonly Regex regex = new Regex(versionRegex); @@ -45,12 +45,12 @@ public static TheVersion Parse(string version) return ret; } - private void Initialize(string version) + private void Initialize(string theVersion) { if (initialized) return; - original = version; + this.version = theVersion; isAlpha = false; isBeta = false; @@ -69,7 +69,7 @@ private void Initialize(string version) var match = regex.Match(version); if (!match.Success) - throw new ArgumentException("Invalid version: " + version, "version"); + throw new ArgumentException("Invalid version: " + version, "theVersion"); major = int.Parse(match.Groups["major"].Value); intParts[0] = major; @@ -127,11 +127,12 @@ private void Initialize(string version) isAlpha = special.IndexOf("alpha") >= 0; isBeta = special.IndexOf("beta") >= 0; } + initialized = true; } public override string ToString() { - return original; + return version; } public int CompareTo(TheVersion other) @@ -168,7 +169,7 @@ public bool Equals(TheVersion other) public static bool operator==(TheVersion lhs, TheVersion rhs) { - if (lhs.original == rhs.original) + if (lhs.version == rhs.version) return true; return (lhs.Major == rhs.Major) && @@ -185,11 +186,11 @@ public bool Equals(TheVersion other) public static bool operator>(TheVersion lhs, TheVersion rhs) { - if (lhs.original == rhs.original) + if (lhs.version == rhs.version) return false; - if (lhs.original == null) + if (lhs.version == null) return false; - if (rhs.original == null) + if (rhs.version == null) return true; for (var i = 0; i < PART_COUNT; i++) @@ -267,11 +268,14 @@ private static int IndexOfFirstNonDigit(string str) } } - [Serializable] public class Package { + private string version; public string Url { get; set; } - public string Version { get; set; } + public string ReleaseNotes { get; set; } + public string ReleaseNotesUrl { get; set; } + public string Message { get; set; } + [NotSerialized] public TheVersion Version { get { return TheVersion.Parse(version); } set { version = value.ToString(); } } } public class UpdateCheckWindow : EditorWindow diff --git a/src/tests/TestApp/App.config b/src/tests/CommandLine/App.config similarity index 84% rename from src/tests/TestApp/App.config rename to src/tests/CommandLine/App.config index 343984d02..258fc34a2 100644 --- a/src/tests/TestApp/App.config +++ b/src/tests/CommandLine/App.config @@ -1,6 +1,6 @@ - - + + diff --git a/src/tests/TestApp/TestApp.csproj b/src/tests/CommandLine/CommandLine.csproj similarity index 69% rename from src/tests/TestApp/TestApp.csproj rename to src/tests/CommandLine/CommandLine.csproj index ff3b3c2bb..a5e3b5a0b 100644 --- a/src/tests/TestApp/TestApp.csproj +++ b/src/tests/CommandLine/CommandLine.csproj @@ -7,19 +7,19 @@ {08B87D2A-8CF1-4211-B7AA-5209F00F72F8} Exe Properties - TestApp - TestApp + Test.CommandLine + CommandLine v3.5 512 true + $(SolutionDir)build\$(AssemblyName) AnyCPU true full false - bin\Debug\ DEBUG;TRACE prompt 4 @@ -29,7 +29,6 @@ AnyCPU pdbonly true - bin\Release\ TRACE prompt 4 @@ -47,8 +46,26 @@ + + + {b389adaf-62cc-486e-85b4-2d8b078df763} + GitHub.Api + + + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} + GitHub.Logging + + + {add7a18b-dd2a-4c22-a2c1-488964eff30a} + GitHub.Unity + + + {3dd3451c-30fa-4294-a3a9-1e080342f867} + TestWebServer + + - - files\git\mac\git-lfs.zip + files\unity\git\mac\git-lfs.zip PreserveNewest - files\git\mac\git-lfs.zip.md5 + files\unity\git\mac\git-lfs.zip.md5 + PreserveNewest + + + PreserveNewest + + PreserveNewest diff --git a/src/tests/TestWebServer/files/unity/latest.json b/src/tests/TestWebServer/files/unity/latest.json index 9aa4f31eb..0ef37d095 100644 --- a/src/tests/TestWebServer/files/unity/latest.json +++ b/src/tests/TestWebServer/files/unity/latest.json @@ -1,4 +1 @@ -{ - "Url": "http://localhost:8080/unity/update.zip", - "Version": "1.1" -} \ No newline at end of file +{"url":"http://localhost:55555/unity/releases/github-for-unity-0.33.0-beta.unitypackage","releaseNotes":null,"releaseNotesUrl":null,"message":null,"version":"0.33.0-beta"} diff --git a/src/tests/TestWebServer/nginx.conf b/src/tests/TestWebServer/nginx.conf deleted file mode 100644 index 1daca4370..000000000 --- a/src/tests/TestWebServer/nginx.conf +++ /dev/null @@ -1,10 +0,0 @@ -server { - listen 8081; - listen [::]:8081; - - root ~/code/work/github/Unity/src/tests/TestWebServer/files; - server_name unity.localhost; - - location / { - } -} From 32d33984e3a7392a7b9ed1db1fb4155411799fe8 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 7 May 2018 15:07:44 +0200 Subject: [PATCH 054/567] Feed model for downloading packages; refactor installers This adds a json format for specifying packages, with data about where to download zips from, md5 checksums, release notes and messages to show in the UI Also moving the git and octorun installers away from a task-based model, it's just too hard to maintain. Startup just runs a thread now and all tasks are manually invoked in that thread via the Run() and RunWithReturn() methods (i.e. they're not tasks at all) --- generate-package.sh | 44 ++ generate-version.sh | 4 +- .../Application/ApplicationManagerBase.cs | 116 ++--- src/GitHub.Api/Authentication/Keychain.cs | 6 +- .../Extensions/DateTimeExtensions.cs | 23 + src/GitHub.Api/Extensions/ListExtensions.cs | 14 +- src/GitHub.Api/Git/GitConfig.cs | 22 +- src/GitHub.Api/Git/RepositoryManager.cs | 20 +- src/GitHub.Api/GitHub.Api.csproj | 19 +- src/GitHub.Api/Helpers/AssemblyResources.cs | 19 +- src/GitHub.Api/Helpers/Constants.cs | 6 +- src/GitHub.Api/IO/FileSystem.cs | 2 +- src/GitHub.Api/IO/NiceIO.cs | 12 +- src/GitHub.Api/IO/Utils.cs | 7 +- src/GitHub.Api/Installer/GitInstaller.cs | 360 ++++++-------- src/GitHub.Api/Installer/OctorunInstaller.cs | 42 +- src/GitHub.Api/Installer/UnzipTask.cs | 2 +- src/GitHub.Api/Managers/Downloader.cs | 61 +-- src/GitHub.Api/Metrics/UsageTracker.cs | 1 - src/GitHub.Api/Platform/DefaultEnvironment.cs | 6 + src/GitHub.Api/Platform/IEnvironment.cs | 3 + src/GitHub.Api/Platform/ISettings.cs | 1 + src/GitHub.Api/Platform/Settings.cs | 132 +++-- .../PlatformResources/windows/git-lfs.json | 1 + .../PlatformResources/windows/git-lfs.zip.md5 | 1 - .../PlatformResources/windows/git.json | 1 + .../PlatformResources/windows/git.zip.md5 | 1 - src/GitHub.Api/Primitives/Package.cs | 64 +++ src/GitHub.Api/Primitives/TheVersion.cs | 267 ++++++++++ src/GitHub.Api/Primitives/UriString.cs | 9 +- src/GitHub.Api/Tasks/ActionTask.cs | 6 +- src/GitHub.Api/Tasks/DownloadTask.cs | 12 +- src/GitHub.Api/Tasks/ProcessTask.cs | 4 +- src/GitHub.Api/Tasks/TaskBase.cs | 35 +- .../Editor/GitHub.Unity/ApplicationCache.cs | 7 +- .../Assets/Editor/GitHub.Unity/EntryPoint.cs | 16 +- .../IconsAndLogos/big-logo-light.png | 4 +- .../IconsAndLogos/big-logo-light@2x.png | 4 +- .../GitHub.Unity/IconsAndLogos/big-logo.png | 4 +- .../IconsAndLogos/big-logo@2x.png | 4 +- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 34 +- .../Editor/GitHub.Unity/Misc/Utility.cs | 2 +- .../Editor/GitHub.Unity/UI/GitPathView.cs | 9 +- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 26 +- .../Assets/Editor/GitHub.Unity/UpdateCheck.cs | 469 ++++++------------ .../Editor/UnityTests/UnityTests.csproj | 8 + .../Assets/Editor/UnityTests/VersionTests.cs | 4 + src/tests/CommandLine/Program.cs | 67 ++- .../IntegrationTests/BaseIntegrationTest.cs | 56 +-- .../Download/BaseTestWithHttpServer.cs | 32 +- .../Download/DownloadTaskTests.cs | 126 +++-- .../Events/RepositoryManagerTests.cs | 226 +++++---- .../IntegrationTests/Git/GitClientTests.cs | 4 +- .../Installer/GitInstallerTests.cs | 29 +- .../IntegrationTestEnvironment.cs | 7 + .../TaskSystem.csproj | 4 +- src/tests/TaskSystemIntegrationTests/Tests.cs | 10 +- src/tests/TestWebServer/TestWebServer.csproj | 32 +- .../files/unity/git/linux/git-lfs.zip | 3 + .../files/unity/git/linux/gitconfig | 18 + .../files/unity/git/mac/git-lfs.zip | 3 + .../files/unity/git/mac/gitconfig | 18 + .../files/unity/git/windows/git-lfs.json | 1 + .../files/unity/git/windows/git-lfs.zip | 3 + .../files/unity/git/windows/git.json | 1 + .../files/unity/git/windows/git.zip | 3 + .../files/unity/git/windows/gitconfig | 20 + .../TestWebServer/files/unity/latest.json | 2 +- .../PlatformResources/windows/git-lfs.json | 1 + .../Editor/PlatformResources/windows/git.json | 1 + .../PlatformResources/windows/git-lfs.json | 1 + .../PlatformResources/windows/git-lfs.zip.md5 | 1 - .../Editor/PlatformResources/windows/git.json | 1 + .../PlatformResources/windows/git.zip.md5 | 1 - 74 files changed, 1497 insertions(+), 1088 deletions(-) create mode 100644 generate-package.sh create mode 100644 src/GitHub.Api/Extensions/DateTimeExtensions.cs create mode 100644 src/GitHub.Api/PlatformResources/windows/git-lfs.json delete mode 100644 src/GitHub.Api/PlatformResources/windows/git-lfs.zip.md5 create mode 100644 src/GitHub.Api/PlatformResources/windows/git.json delete mode 100644 src/GitHub.Api/PlatformResources/windows/git.zip.md5 create mode 100644 src/GitHub.Api/Primitives/Package.cs create mode 100644 src/GitHub.Api/Primitives/TheVersion.cs create mode 100644 src/tests/TestWebServer/files/unity/git/linux/git-lfs.zip create mode 100644 src/tests/TestWebServer/files/unity/git/linux/gitconfig create mode 100644 src/tests/TestWebServer/files/unity/git/mac/git-lfs.zip create mode 100644 src/tests/TestWebServer/files/unity/git/mac/gitconfig create mode 100644 src/tests/TestWebServer/files/unity/git/windows/git-lfs.json create mode 100644 src/tests/TestWebServer/files/unity/git/windows/git-lfs.zip create mode 100644 src/tests/TestWebServer/files/unity/git/windows/git.json create mode 100644 src/tests/TestWebServer/files/unity/git/windows/git.zip create mode 100644 src/tests/TestWebServer/files/unity/git/windows/gitconfig create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.json create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git.json create mode 100644 unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.json delete mode 100644 unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git-lfs.zip.md5 create mode 100644 unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git.json delete mode 100644 unity/TestProject/Assets/Plugins/GitHub/Editor/PlatformResources/windows/git.zip.md5 diff --git a/generate-package.sh b/generate-package.sh new file mode 100644 index 000000000..4176b0b0b --- /dev/null +++ b/generate-package.sh @@ -0,0 +1,44 @@ +#!/bin/sh -eux +if [ $# -lt 3 ]; then + echo "Usage: generate-package.sh [git|lfs] [version] [path to file] [host url (optional)] [release notes file (optional)] [message file (optional)]" + exit 1 +fi + +LFS_MD5="4294df6cbb467b8133553570450757c7" +GIT_MD5="50570ed932559f294d1a1361801740b9" +MD5="" + +URL="http://ghfvs-installer.github.com" +if [ $# -ge 4 ]; then + URL=$4 +fi + +if [ "$1" == "git" ]; then + MD5=$GIT_MD5 + URL="$URL/unity/git/git.zip" +fi +if [ "$1" == "lfs" ]; then + MD5=$LFS_MD5 + URL="$URL/unity/git/git-lfs.zip" +fi + +RN="" +MSG="" +if [ $# -ge 5 ]; then + RN="$5" +fi + +if [ $# -ge 6 ]; then + MSG="$6" +fi + +EXEC="mono" +if [ -e "/c/" ]; then + EXEC="" +fi + +if [ ! -e build/CommandLine/CommandLine.exe ]; then + >&2 xbuild /target:CommandLine GitHub.Unity.sln /verbosity:minimal +fi + +"$EXEC"build/CommandLine/CommandLine.exe --gen-package --version "$2" --path "$3" --url "$URL" --md5 "$MD5" --rn "$RN" --msg "$MSG" diff --git a/generate-version.sh b/generate-version.sh index 1c74a423e..3e81e483d 100644 --- a/generate-version.sh +++ b/generate-version.sh @@ -1,4 +1,4 @@ -#!/bin/sh -eu +#!/bin/sh -eux if [ $# -lt 1 ]; then echo "Usage: generate-version.sh [version] [host url (default: http://ghfvs-installer.github.com)]" exit 1 @@ -18,4 +18,4 @@ if [ ! -e build/CommandLine/CommandLine.exe ]; then >&2 xbuild /target:CommandLine GitHub.Unity.sln /verbosity:minimal fi -"$EXEC" build/CommandLine/CommandLine.exe -g -v "$1" -u "$URL" +"$EXEC"build/CommandLine/CommandLine.exe -g -v "$1" -u "$URL" diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 45ac09712..5d28f8d38 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -35,14 +35,6 @@ protected void Initialize() // accessing Environment triggers environment initialization if it hasn't happened yet Platform = new Platform(Environment); - UserSettings = new UserSettings(Environment); - LocalSettings = new LocalSettings(Environment); - SystemSettings = new SystemSettings(Environment); - - UserSettings.Initialize(); - LocalSettings.Initialize(); - SystemSettings.Initialize(); - LogHelper.TracingEnabled = UserSettings.Get(Constants.TraceLoggingKey, false); ProcessManager = new ProcessManager(Environment, Platform.GitEnvironment, CancellationToken); Platform.Initialize(ProcessManager, TaskManager); @@ -54,79 +46,34 @@ public void Run(bool firstRun) Logger.Trace("Run - CurrentDirectory {0}", NPath.CurrentDirectory); isBusy = true; - var endTask = new ActionTask(CancellationToken, - (_, state) => InitializeEnvironment(state)) - { Affinity = TaskAffinity.UI }; - - ITask setExistingEnvironmentPath; - if (Environment.IsMac) + var thread = new Thread(obj => { - setExistingEnvironmentPath = new SimpleProcessTask(CancellationToken, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") - .Configure(ProcessManager, dontSetupGit: true) - .Catch(e => true) // make sure this doesn't throw if the task fails - .Then((success, path) => success ? path?.Split(new[] { "\"" }, StringSplitOptions.None)[1] : null); - } - else - { - setExistingEnvironmentPath = new FuncTask(CancellationToken, () => null); - } + CancellationToken token = (CancellationToken)obj; + var endTask = new ActionTask(token, (_, s) => InitializeEnvironment(s)) { Affinity = TaskAffinity.UI }; + string path = null; - setExistingEnvironmentPath.OnEnd += (t, path, success, ex) => - { - if (path != null) + if (Environment.IsMac) { - Logger.Trace("Existing Environment Path Original:{0} Updated:{1}", Environment.Path, path); - Environment.Path = path; + var getEnvPath = new SimpleProcessTask(token, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") + .Configure(ProcessManager, dontSetupGit: true) + .Catch(e => true); // make sure this doesn't throw if the task fails + path = getEnvPath.RunWithReturn(true); + if (getEnvPath.Successful) + { + Logger.Trace("Existing Environment Path Original:{0} Updated:{1}", Environment.Path, path); + Environment.Path = path?.Split(new[] { "\"" }, StringSplitOptions.None)[1]; + } } - }; - - var setupOctorun = new OctorunInstaller(Environment, TaskManager).SetupOctorunIfNeeded(); - var setOctorunEnvironment = new ActionTask(CancellationToken, - (s, octorunPath) => Environment.OctorunScriptPath = octorunPath); - var getGitFromSettings = new FuncTask(CancellationToken, () => - { - var gitExecutablePath = SystemSettings.Get(Constants.GitInstallPathKey)?.ToNPath(); - if (gitExecutablePath.HasValue && gitExecutablePath.Value.FileExists()) // we have a git path - { - Logger.Trace("Using git install path from settings: {0}", gitExecutablePath); - return gitExecutablePath.Value; - } - return NPath.Default; - }); + Environment.OctorunScriptPath = new OctorunInstaller(Environment, TaskManager).SetupOctorunIfNeeded(); - getGitFromSettings.OnEnd += (t, path, _, __) => - { - if (path.IsInitialized) - { - var state = new GitInstaller.GitInstallationState { - GitExecutablePath = path, - GitIsValid = true - }; - endTask.PreviousResult = state; - endTask.Start(); - return; - } - Logger.Trace("Using portable git"); - - var setupGit = new GitInstaller(Environment, ProcessManager, TaskManager).SetupGitIfNeeded(); - t.Then(setupGit); - setupGit.Finally((s, state) => - { - endTask.PreviousResult = state; - endTask.Start(); - }); - setupGit.Progress(progressReporter.UpdateProgress); - // append installer task to top chain - }; - - var setupChain = setExistingEnvironmentPath.Then(setupOctorun); - setupChain.OnEnd += (t, path, _, __) => - { - t.GetEndOfChain().Then(setOctorunEnvironment).Then(getGitFromSettings); - }; - - setupChain.Start(); + var state = new GitInstaller(Environment, ProcessManager, TaskManager, SystemSettings) + { Progress = progressReporter } + .SetupGitIfNeeded(); + endTask.PreviousResult = state; + endTask.Start(); + }); + thread.Start(CancellationToken); } public ITask InitializeRepository() @@ -216,7 +163,6 @@ protected void SetupMetrics(string unityVersion, bool firstRun) } #endif } - protected abstract void SetupMetrics(); protected abstract void InitializeUI(); protected abstract void SetProjectToTextSerialization(); @@ -277,8 +223,16 @@ protected virtual void Dispose(bool disposing) { if (disposed) return; disposed = true; - if (TaskManager != null) TaskManager.Dispose(); - if (repositoryManager != null) repositoryManager.Dispose(); + if (TaskManager != null) + { + TaskManager.Dispose(); + TaskManager = null; + } + if (repositoryManager != null) + { + repositoryManager.Dispose(); + repositoryManager = null; + } } } @@ -295,9 +249,9 @@ public void Dispose() public CancellationToken CancellationToken { get { return TaskManager.Token; } } public ITaskManager TaskManager { get; protected set; } public IGitClient GitClient { get; protected set; } - public ISettings LocalSettings { get; protected set; } - public ISettings SystemSettings { get; protected set; } - public ISettings UserSettings { get; protected set; } + public ISettings LocalSettings { get { return Environment.LocalSettings; } } + public ISettings SystemSettings { get { return Environment.SystemSettings; } } + public ISettings UserSettings { get { return Environment.UserSettings; } } public IUsageTracker UsageTracker { get; protected set; } public bool IsBusy { get { return isBusy; } } protected TaskScheduler UIScheduler { get; private set; } diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index 48d2296f0..d27029892 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -123,7 +123,7 @@ public async Task Load(UriString host) logger.Warning("Keychain Username:\"{0}\" does not match cached Username:\"{1}\"; Hopefully it works", keychainItem.Username, connection.Username); } - //logger.Trace("Loaded from Credential Manager Host:\"{0}\" Username:\"{1}\"", keychainItem.Host, keychainItem.Username); + //logger.Trace("Loaded from Credential Manager Host:\"{0}\" Username:\"{1}\"", keychainItem.Host, keychainItem.Username); keychainAdapter.Set(keychainItem); } return keychainAdapter; @@ -151,7 +151,7 @@ public async Task Clear(UriString host, bool deleteFromCredentialManager) //logger.Trace("Clear Host:{0}", host); Guard.ArgumentNotNull(host, nameof(host)); - + RemoveConnection(host); //clear octokit credentials @@ -328,4 +328,4 @@ private void UpdateConnections(Connection[] conns) public IList Hosts => connections.Keys.ToArray(); public bool HasKeys => connections.Any(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Extensions/DateTimeExtensions.cs b/src/GitHub.Api/Extensions/DateTimeExtensions.cs new file mode 100644 index 000000000..ae71d74d8 --- /dev/null +++ b/src/GitHub.Api/Extensions/DateTimeExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; + +namespace GitHub.Unity +{ + static class DateTimeExtensions + { + public static DateTimeOffset ToDateTimeOffset(this string dateString, DateTimeOffset? @default = null) + { + DateTimeOffset result; + if (DateTimeOffset.TryParseExact(dateString, Constants.Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + { + return result; + } + return @default.HasValue ? @default.Value : DateTimeOffset.MinValue; + } + + public static string ToIsoString(this DateTimeOffset dt) + { + return dt.ToString(Constants.Iso8601Format); + } + } +} diff --git a/src/GitHub.Api/Extensions/ListExtensions.cs b/src/GitHub.Api/Extensions/ListExtensions.cs index 839f58d78..bc20ed376 100644 --- a/src/GitHub.Api/Extensions/ListExtensions.cs +++ b/src/GitHub.Api/Extensions/ListExtensions.cs @@ -12,7 +12,7 @@ public static string Join(this IEnumerable list, string separator) return null; return String.Join(separator, list.Select(x => x?.ToString()).ToArray()); } - + public static IEnumerable> Spool(this IEnumerable items, int spoolLength) { var currentSpoolLength = 0; @@ -40,5 +40,15 @@ public static IEnumerable> Spool(this IEnumerable items, int yield return currentList; } } + + public static T[] Append(this T[] array, T item) + { + if (array == null) + return new T[] { item }; + var ret = new T[array.Length]; + array.CopyTo(ret, 0); + ret[ret.Length - 1] = item; + return ret; + } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitConfig.cs b/src/GitHub.Api/Git/GitConfig.cs index b2277295c..09f614686 100644 --- a/src/GitHub.Api/Git/GitConfig.cs +++ b/src/GitHub.Api/Git/GitConfig.cs @@ -296,7 +296,7 @@ private void SetAndWrite(string section, string key, string value) manager.Save(sb.ToString()); } - class Section : Dictionary + class Section : Dictionary> { public Section(string name, string description = null) { @@ -306,19 +306,20 @@ public Section(string name, string description = null) public string TryGetString(string key) { - if (ContainsKey(key)) - return this[key]; + List val; + if (TryGetValue(key, out val)) + return val.First(); return null; } public string GetString(string key) { - return this[key]; + return this[key].First(); } public int GetInt(string key) { - var value = this[key]; + var value = TryGetString(key); int result = 0; int.TryParse(value, out result); return result; @@ -326,7 +327,7 @@ public int GetInt(string key) public float GetFloat(string key) { - var value = this[key]; + var value = TryGetString(key); float result = 0F; float.TryParse(value, out result); return result; @@ -334,7 +335,9 @@ public float GetFloat(string key) public void SetString(string key, string value) { - this[key] = value; + if (!ContainsKey(key)) + this[key] = new List(); + this[key].Add(value); } public void SetInt(string key, int value) @@ -446,8 +449,7 @@ private void AddKeyValuePairToLoadedSectionFromLine(string line) var match = PairPattern.Match(line); var key = match.Groups[1].Value.Trim(); var value = match.Groups[2].Value; - - loadedSection.Add(key, value); + loadedSection.SetString(key, value); } private void EnsureFileBeginsWithSection() @@ -510,4 +512,4 @@ public bool Save(string contents) public string[] Lines { get; private set; } } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index a2768154e..e39ec5147 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -483,13 +483,13 @@ private string GetCurrentHead() private void WatcherOnRemoteBranchesChanged() { - Logger.Trace("WatcherOnRemoteBranchesChanged"); + //Logger.Trace("WatcherOnRemoteBranchesChanged"); DataNeedsRefreshing?.Invoke(CacheType.Branches); } private void WatcherOnLocalBranchesChanged() { - Logger.Trace("WatcherOnLocalBranchesChanged"); + //Logger.Trace("WatcherOnLocalBranchesChanged"); DataNeedsRefreshing?.Invoke(CacheType.Branches); // the watcher should tell us what branch has changed so we can fire this only // when the active branch has changed @@ -499,20 +499,20 @@ private void WatcherOnLocalBranchesChanged() private void WatcherOnRepositoryCommitted() { - Logger.Trace("WatcherOnRepositoryCommitted"); + //Logger.Trace("WatcherOnRepositoryCommitted"); DataNeedsRefreshing?.Invoke(CacheType.GitLog); DataNeedsRefreshing?.Invoke(CacheType.GitStatus); } private void WatcherOnRepositoryChanged() { - Logger.Trace("WatcherOnRepositoryChanged"); + //Logger.Trace("WatcherOnRepositoryChanged"); DataNeedsRefreshing?.Invoke(CacheType.GitStatus); } private void WatcherOnConfigChanged() { - Logger.Trace("WatcherOnConfigChanged"); + //Logger.Trace("WatcherOnConfigChanged"); config.Reset(); DataNeedsRefreshing?.Invoke(CacheType.Branches); DataNeedsRefreshing?.Invoke(CacheType.RepositoryInfo); @@ -521,7 +521,7 @@ private void WatcherOnConfigChanged() private void WatcherOnHeadChanged() { - Logger.Trace("WatcherOnHeadChanged"); + //Logger.Trace("WatcherOnHeadChanged"); DataNeedsRefreshing?.Invoke(CacheType.RepositoryInfo); DataNeedsRefreshing?.Invoke(CacheType.GitLog); DataNeedsRefreshing?.Invoke(CacheType.GitAheadBehind); @@ -529,13 +529,13 @@ private void WatcherOnHeadChanged() private void WatcherOnIndexChanged() { - Logger.Trace("WatcherOnIndexChanged"); + //Logger.Trace("WatcherOnIndexChanged"); DataNeedsRefreshing?.Invoke(CacheType.GitStatus); } private void UpdateLocalBranches() { - Logger.Trace("UpdateLocalBranches"); + //Logger.Trace("UpdateLocalBranches"); var branches = new Dictionary(); UpdateLocalBranches(branches, repositoryPaths.BranchesPath, config.GetBranches().Where(x => x.IsTracking), ""); @@ -566,7 +566,7 @@ private void UpdateLocalBranches(Dictionary branches, NPat private void UpdateRemoteBranches() { - Logger.Trace("UpdateRemoteBranches"); + //Logger.Trace("UpdateRemoteBranches"); var remotes = config.GetRemotes().ToArray().ToDictionary(x => x.Name, x => x); var remoteBranches = new Dictionary>(); @@ -634,7 +634,7 @@ private set { if (isBusy != value) { - Logger.Trace("IsBusyChanged Value:{0}", value); + //Logger.Trace("IsBusyChanged Value:{0}", value); isBusy = value; IsBusyChanged?.Invoke(isBusy); } diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 5842dcc0d..82c902b0a 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -97,6 +97,7 @@ + @@ -136,6 +137,8 @@ + + @@ -269,9 +272,9 @@ PreserveNewest - + PreserveNewest - + PreserveNewest @@ -286,12 +289,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - @@ -309,6 +306,12 @@ Localization.Designer.cs Designer + + PreserveNewest + + + PreserveNewest + + PreserveNewest - - files\unity\git\windows\git-lfs.zip.md5 + PreserveNewest - - - files\unity\git\mac\git-lfs.zip + PreserveNewest - - files\unity\git\mac\git-lfs.zip.md5 + PreserveNewest @@ -88,6 +79,9 @@ GitHub.Logging + + + fK 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%0DYob77Mvc-?eSS;I*_VBNYM^ly4QjL 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

P~ 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

k5RavP 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

/// The address of the server. /// - Task Logout(UriString hostAddress); + ITask Logout(UriString hostAddress); } } \ No newline at end of file diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 1bb7a3c6d..f36fc8072 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -134,11 +134,11 @@ public async Task ContinueLogin(LoginResultData loginResultData } /// - public async Task Logout(UriString hostAddress) + public ITask Logout(UriString hostAddress) { Guard.ArgumentNotNull(hostAddress, nameof(hostAddress)); - await new ActionTask(keychain.Clear(hostAddress, true)).StartAwait(); + return new ActionTask(keychain.Clear(hostAddress, true)) { Message = "Signing out" }.Start(); } private async Task TryLogin( diff --git a/src/GitHub.Api/Cache/CacheContainer.cs b/src/GitHub.Api/Cache/CacheContainer.cs index 73204a5b6..85b561f41 100644 --- a/src/GitHub.Api/Cache/CacheContainer.cs +++ b/src/GitHub.Api/Cache/CacheContainer.cs @@ -30,9 +30,7 @@ public void InvalidateAll() { foreach (var cache in caches.Values) { - // force an invalidation if the cache is valid, otherwise it will do it on its own - if (cache.Value.ValidateData()) - cache.Value.InvalidateData(); + cache.Value.InvalidateData(); } } diff --git a/src/GitHub.Api/Git/IRepository.cs b/src/GitHub.Api/Git/IRepository.cs index f7ad3571c..8585d48d0 100644 --- a/src/GitHub.Api/Git/IRepository.cs +++ b/src/GitHub.Api/Git/IRepository.cs @@ -79,5 +79,6 @@ public interface IRepository : IEquatable, IDisposable ITask CreateBranch(string branch, string baseBranch); ITask SwitchBranch(string branch); void Refresh(CacheType cacheType); + event Action OnProgress; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 292fbea1b..f81355bf0 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Threading; namespace GitHub.Unity { @@ -20,6 +19,7 @@ sealed class Repository : IEquatable, IRepository private string name; private HashSet cacheInvalidationRequests = new HashSet(); private Dictionary> cacheUpdateEvents; + private ProgressReporter progressReporter = new ProgressReporter(); public event Action LogChanged; public event Action TrackingStatusChanged; @@ -31,6 +31,11 @@ sealed class Repository : IEquatable, IRepository public event Action LocksChanged; public event Action RemoteBranchListChanged; public event Action LocalAndRemoteBranchListChanged; + public event Action OnProgress + { + add { progressReporter.OnProgress += value; } + remove { progressReporter.OnProgress -= value; } + } /// /// Initializes a new instance of the class. @@ -85,6 +90,15 @@ public void Initialize(IRepositoryManager theRepositoryManager, ITaskManager the this.repositoryManager.LocalBranchesUpdated += RepositoryManagerOnLocalBranchesUpdated; this.repositoryManager.RemoteBranchesUpdated += RepositoryManagerOnRemoteBranchesUpdated; this.repositoryManager.DataNeedsRefreshing += RefreshCache; + try + { + this.taskManager.OnProgress += progressReporter.UpdateProgress; + } + catch (Exception ex) + { + LogHelper.Error(ex); + } + } public void Start() @@ -166,12 +180,9 @@ private void RefreshCache(CacheType cacheType) } public void Refresh(CacheType cacheType) - { - var cache = cacheContainer.GetCache(cacheType); - // if the cache has valid data, we need to force an invalidation to refresh it - // if it doesn't have valid data, it will trigger an invalidation automatically - if (cache.ValidateData()) - cache.InvalidateData(); + { + var cache = cacheContainer.GetCache(cacheType); + cache.InvalidateData(); } private void CacheHasBeenInvalidated(CacheType cacheType) @@ -186,20 +197,20 @@ private void CacheHasBeenInvalidated(CacheType cacheType) switch (cacheType) { case CacheType.Branches: - repositoryManager?.UpdateBranches(); + repositoryManager?.UpdateBranches().Start(); break; case CacheType.GitLog: - repositoryManager?.UpdateGitLog(); + repositoryManager?.UpdateGitLog().Start(); break; case CacheType.GitAheadBehind: - repositoryManager?.UpdateGitAheadBehindStatus(); + repositoryManager?.UpdateGitAheadBehindStatus().Start(); break; case CacheType.GitLocks: if (CurrentRemote != null) - repositoryManager?.UpdateLocks(); + repositoryManager?.UpdateLocks().Start(); break; case CacheType.GitUser: @@ -207,11 +218,11 @@ private void CacheHasBeenInvalidated(CacheType cacheType) break; case CacheType.RepositoryInfo: - repositoryManager?.UpdateRepositoryInfo(); + repositoryManager?.UpdateRepositoryInfo().Start(); break; case CacheType.GitStatus: - repositoryManager?.UpdateGitStatus(); + repositoryManager?.UpdateGitStatus().Start(); break; default: diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index 15d49abe4..865a66d26 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -37,17 +37,18 @@ public interface IRepositoryManager : IDisposable ITask LockFile(NPath file); ITask UnlockFile(NPath file, bool force); ITask DiscardChanges(GitStatusEntry[] gitStatusEntries); - void UpdateGitLog(); - void UpdateGitStatus(); - void UpdateGitAheadBehindStatus(); - void UpdateLocks(); + ITask UpdateGitLog(); + ITask UpdateGitStatus(); + ITask UpdateGitAheadBehindStatus(); + ITask UpdateLocks(); + ITask UpdateRepositoryInfo(); + ITask UpdateBranches(); + int WaitForEvents(); - void UpdateRepositoryInfo(); IGitConfig Config { get; } IGitClient GitClient { get; } bool IsBusy { get; } - void UpdateBranches(); } interface IRepositoryPathConfiguration @@ -181,7 +182,6 @@ public ITask CommitAllFiles(string message, string body) { var task = GitClient.AddAll() .Then(GitClient.Commit(message, body)); - return HookupHandlers(task, true); } @@ -189,7 +189,6 @@ public ITask CommitFiles(List files, string message, string body) { var task = GitClient.Add(files) .Then(GitClient.Commit(message, body)); - return HookupHandlers(task, true); } @@ -255,42 +254,16 @@ public ITask CreateBranch(string branch, string baseBranch) public ITask LockFile(NPath file) { - var task = GitClient.Lock(file); - return HookupHandlers(task, false).Then(UpdateLocks); + var task = GitClient.Lock(file) + .Then(() => DataNeedsRefreshing?.Invoke(CacheType.GitLocks)); + return HookupHandlers(task, false); } public ITask UnlockFile(NPath file, bool force) { - var task = GitClient.Unlock(file, force); - return HookupHandlers(task, false).Then(UpdateLocks); - } - - public void UpdateGitLog() - { - var task = GitClient.Log() - .Then((success, logEntries) => - { - if (success) - { - GitLogUpdated?.Invoke(logEntries); - } - }); - task = HookupHandlers(task, false); - task.Start(); - } - - public void UpdateGitStatus() - { - var task = GitClient.Status() - .Then((success, status) => - { - if (success) - { - GitStatusUpdated?.Invoke(status); - } - }); - task = HookupHandlers(task, false); - task.Start(); + var task = GitClient.Unlock(file, force) + .Then(() => DataNeedsRefreshing?.Invoke(CacheType.GitLocks)); + return HookupHandlers(task, false); } public ITask DiscardChanges(GitStatusEntry[] gitStatusEntries) @@ -328,64 +301,103 @@ public ITask DiscardChanges(GitStatusEntry[] gitStatusEntries) task.Then(GitClient.Discard(itemsToRevert)); } } - , () => gitStatusEntries); + , () => gitStatusEntries) + { Message = "Discarding changes..." }; return HookupHandlers(task, true); } - public void UpdateGitAheadBehindStatus() + public ITask UpdateGitLog() + { + var task = GitClient.Log() + .Then((success, logEntries) => + { + if (success) + { + GitLogUpdated?.Invoke(logEntries); + } + }); + return HookupHandlers(task, false); + } + + public ITask UpdateGitStatus() + { + var task = GitClient.Status() + .Then((success, status) => + { + if (success) + { + GitStatusUpdated?.Invoke(status); + } + }); + return HookupHandlers(task, false); + } + + public ITask UpdateGitAheadBehindStatus() { ConfigBranch? configBranch; ConfigRemote? configRemote; GetCurrentBranchAndRemote(out configBranch, out configRemote); + var updateTask = new ActionTask(token, (success, status) => + { + if (success) + { + GitAheadBehindStatusUpdated?.Invoke(status); + } + }); if (configBranch.HasValue && configBranch.Value.Remote.HasValue) { var name = configBranch.Value.Name; var trackingName = configBranch.Value.IsTracking ? configBranch.Value.Remote.Value.Name + "/" + configBranch.Value.TrackingBranch : "[None]"; var task = GitClient.AheadBehindStatus(name, trackingName) - .Then((success, status) => - { - if (success) - { - GitAheadBehindStatusUpdated?.Invoke(status); - } - }); - task = HookupHandlers(task, false); - task.Start(); + .Then(updateTask); + return HookupHandlers(task, false); } else { - GitAheadBehindStatusUpdated?.Invoke(GitAheadBehindStatus.Default); + updateTask.PreviousResult = GitAheadBehindStatus.Default; + return updateTask; } } - public void UpdateLocks() + public ITask UpdateLocks() { - GitClient.ListLocks(false) + var task = GitClient.ListLocks(false) .Then((success, locks) => { if (success) { GitLocksUpdated?.Invoke(locks); } - }) - .Start(); + }); + return HookupHandlers(task, false); + } - public void UpdateBranches() + public ITask UpdateBranches() { - UpdateLocalBranches(); - UpdateRemoteBranches(); + var task = new ActionTask(token, () => + { + UpdateLocalBranches(); + UpdateRemoteBranches(); + }) + { Message = "Updating branches..." }; + return HookupHandlers(task, false); } - public void UpdateRepositoryInfo() + public ITask UpdateRepositoryInfo() { - ConfigBranch? branch; - ConfigRemote? remote; - GetCurrentBranchAndRemote(out branch, out remote); - CurrentBranchUpdated?.Invoke(branch, remote); + var task = new ActionTask(token, () => + { + ConfigBranch? branch; + ConfigRemote? remote; + GetCurrentBranchAndRemote(out branch, out remote); + CurrentBranchUpdated?.Invoke(branch, remote); + }) + { Message = "Updating repository info..." };; + return HookupHandlers(task, false); } private void GetCurrentBranchAndRemote(out ConfigBranch? branch, out ConfigRemote? remote) diff --git a/src/GitHub.Api/Git/Tasks/GitAddTask.cs b/src/GitHub.Api/Git/Tasks/GitAddTask.cs index e61fa5eda..bcb1baf74 100644 --- a/src/GitHub.Api/Git/Tasks/GitAddTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitAddTask.cs @@ -32,5 +32,6 @@ public GitAddTask(CancellationToken token, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Staging files..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs b/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs index 12592b022..50635e963 100644 --- a/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitAheadBehindStatusTask.cs @@ -17,5 +17,6 @@ public GitAheadBehindStatusTask(string gitRef, string otherRef, public override string ProcessArguments => arguments; public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Querying status..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs b/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs index 673979f69..a225c53b5 100644 --- a/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitBranchCreateTask.cs @@ -21,5 +21,6 @@ public GitBranchCreateTask(string newBranch, string baseBranch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Creating branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs b/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs index 983e40944..25b61fe8e 100644 --- a/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitBranchDeleteTask.cs @@ -18,5 +18,6 @@ public GitBranchDeleteTask(string branch, bool deleteUnmerged, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Deleting branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs b/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs index f64c89130..ea2e9f4d3 100644 --- a/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs @@ -32,5 +32,6 @@ public GitCheckoutTask(CancellationToken token, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Checking out branch..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitCommitTask.cs b/src/GitHub.Api/Git/Tasks/GitCommitTask.cs index b00f09b83..75d53de33 100644 --- a/src/GitHub.Api/Git/Tasks/GitCommitTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCommitTask.cs @@ -41,5 +41,6 @@ protected override void RaiseOnEnd() public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Committing..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs index a33a418f5..402c000ac 100644 --- a/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitConfigGetTask.cs @@ -48,5 +48,6 @@ public GitConfigGetTask(string key, GitConfigSource configSource, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Reading configuration..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs index 8d0fd3d92..1ecb999f6 100644 --- a/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitConfigListTask.cs @@ -28,5 +28,6 @@ public GitConfigListTask(GitConfigSource configSource, CancellationToken token, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Reading configuration..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs b/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs index 4f4604236..d08ded87d 100644 --- a/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitConfigSetTask.cs @@ -23,5 +23,6 @@ public GitConfigSetTask(string key, string value, GitConfigSource configSource, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Writing configuration..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitFetchTask.cs b/src/GitHub.Api/Git/Tasks/GitFetchTask.cs index a1b28b9f4..39a6421bb 100644 --- a/src/GitHub.Api/Git/Tasks/GitFetchTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitFetchTask.cs @@ -36,5 +36,6 @@ public GitFetchTask(string remote, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Fetching..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitInitTask.cs b/src/GitHub.Api/Git/Tasks/GitInitTask.cs index f669db44d..679f807df 100644 --- a/src/GitHub.Api/Git/Tasks/GitInitTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitInitTask.cs @@ -14,5 +14,6 @@ public GitInitTask(CancellationToken token, IOutputProcessor processor = public override string ProcessArguments { get { return "init"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Initializing..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs b/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs index 0aaef1dfc..68f5f9d19 100644 --- a/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLfsInstallTask.cs @@ -14,5 +14,6 @@ public GitLfsInstallTask(CancellationToken token, IOutputProcessor proce public override string ProcessArguments { get { return "lfs install"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Initializing LFS..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs b/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs index 6bcc1e869..444dcf972 100644 --- a/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLfsVersionTask.cs @@ -15,5 +15,6 @@ public GitLfsVersionTask(CancellationToken token, IOutputProcessor p public override string ProcessArguments { get { return "lfs version"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Reading LFS version..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs b/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs index 97938f95a..afa0c94e0 100644 --- a/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitListBranchesTask.cs @@ -13,10 +13,8 @@ public GitListLocalBranchesTask(CancellationToken token, BaseOutputListProcessor Name = TaskName; } - public override string ProcessArguments - { - get { return Arguments ; } - } + public override string ProcessArguments => Arguments; + public override string Message { get; set; } = "Listing local branches..."; } @@ -31,9 +29,7 @@ public GitListRemoteBranchesTask(CancellationToken token) Name = TaskName; } - public override string ProcessArguments - { - get { return Arguments; } - } + public override string ProcessArguments => Arguments; + public override string Message { get; set; } = "Listing remote branches..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs b/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs index aefad6373..2cf39d68c 100644 --- a/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs @@ -19,5 +19,6 @@ public GitListLocksTask(bool local, } public override string ProcessArguments => args; + public override string Message { get; set; } = "Reading locks..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitLockTask.cs b/src/GitHub.Api/Git/Tasks/GitLockTask.cs index 4903f8341..a2353957c 100644 --- a/src/GitHub.Api/Git/Tasks/GitLockTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLockTask.cs @@ -19,5 +19,6 @@ public GitLockTask(string path, public override string ProcessArguments => arguments; public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Locking file..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitLogTask.cs b/src/GitHub.Api/Git/Tasks/GitLogTask.cs index d416ae660..955521a61 100644 --- a/src/GitHub.Api/Git/Tasks/GitLogTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLogTask.cs @@ -17,5 +17,6 @@ public override string ProcessArguments { get { return @"-c i18n.logoutputencoding=utf8 -c core.quotepath=false log --pretty=format:""%H%n%P%n%aN%n%aE%n%aI%n%cN%n%cE%n%cI%n%B---GHUBODYEND---"" --name-status"; } } + public override string Message { get; set; } = "Loading the history..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitPullTask.cs b/src/GitHub.Api/Git/Tasks/GitPullTask.cs index fe51032bf..64006d950 100644 --- a/src/GitHub.Api/Git/Tasks/GitPullTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitPullTask.cs @@ -34,5 +34,6 @@ public GitPullTask(string remote, string branch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Pulling..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitPushTask.cs b/src/GitHub.Api/Git/Tasks/GitPushTask.cs index 0e4c74cfd..7e786c940 100644 --- a/src/GitHub.Api/Git/Tasks/GitPushTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitPushTask.cs @@ -30,5 +30,6 @@ public GitPushTask(string remote, string branch, bool setUpstream, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Pushing..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs index f6e948009..5512c2b28 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteAddTask.cs @@ -21,5 +21,6 @@ public GitRemoteAddTask(string remote, string url, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Adding remote..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs index 6be6ffbef..92c91f3a7 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteBranchDeleteTask.cs @@ -21,5 +21,6 @@ public GitRemoteBranchDeleteTask(string remote, string branch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Deleting remote branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs index 02eb69a48..3428fdfa9 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteChangeTask.cs @@ -21,5 +21,6 @@ public GitRemoteChangeTask(string remote, string url, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Switching remotes..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs b/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs index c1538cb63..6a25561ec 100644 --- a/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRemoteListTask.cs @@ -13,5 +13,6 @@ public GitRemoteListTask(CancellationToken token, BaseOutputListProcessor files, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Unstaging files..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitRevertTask.cs b/src/GitHub.Api/Git/Tasks/GitRevertTask.cs index 862b78c4d..f281c60ba 100644 --- a/src/GitHub.Api/Git/Tasks/GitRevertTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitRevertTask.cs @@ -18,5 +18,6 @@ public GitRevertTask(string changeset, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Reverting commit..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitStatusTask.cs b/src/GitHub.Api/Git/Tasks/GitStatusTask.cs index f4c524641..3456fad67 100644 --- a/src/GitHub.Api/Git/Tasks/GitStatusTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitStatusTask.cs @@ -18,5 +18,6 @@ public override string ProcessArguments get { return "-c i18n.logoutputencoding=utf8 -c core.quotepath=false status -b -u --porcelain"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Listing changed files..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs b/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs index 80efe5e40..d234f6b0b 100644 --- a/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitSwitchBranchesTask.cs @@ -19,5 +19,6 @@ public GitSwitchBranchesTask(string branch, public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Switching branch..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs b/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs index 86b8e2281..e2a423bcd 100644 --- a/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs @@ -31,6 +31,7 @@ public GitUnlockTask(NPath path, bool force, public override string ProcessArguments => arguments; public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Unlocking file..."; } } diff --git a/src/GitHub.Api/Git/Tasks/GitVersionTask.cs b/src/GitHub.Api/Git/Tasks/GitVersionTask.cs index bae36cd20..4774cd84e 100644 --- a/src/GitHub.Api/Git/Tasks/GitVersionTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitVersionTask.cs @@ -15,5 +15,6 @@ public GitVersionTask(CancellationToken token, IOutputProcessor proc public override string ProcessArguments { get { return "--version"; } } public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Reading git version..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Helpers/Progress.cs b/src/GitHub.Api/Helpers/Progress.cs index 222571cf5..c7ae0a003 100644 --- a/src/GitHub.Api/Helpers/Progress.cs +++ b/src/GitHub.Api/Helpers/Progress.cs @@ -1,10 +1,13 @@ using GitHub.Logging; using System; +using System.Collections.Generic; +using System.Linq; namespace GitHub.Unity { public interface IProgress { + void UpdateProgress(long value, long total, string message = null); ITask Task { get; } /// /// From 0 to 1 @@ -16,30 +19,63 @@ public interface IProgress event Action OnProgress; } + public class ProgressReporter + { + public event Action OnProgress; + private Dictionary tasks = new Dictionary(); + private Progress progress = new Progress(TaskBase.Default); + + public void UpdateProgress(IProgress prog) + { + long total = 0; + long value = 0; + lock (tasks) + { + if (!tasks.ContainsKey(prog.Task)) + tasks.Add(prog.Task, prog); + else + tasks[prog.Task] = prog; + + total = tasks.Values.Select(x => x.Total).Sum(); + value = tasks.Values.Select(x => x.Value).Sum(); + + if (prog.Percentage == 1f) + tasks.Remove(prog.Task); + } + progress.UpdateProgress(value, total, prog.Message); + OnProgress?.Invoke(progress); + } + } + public class Progress : IProgress { private static ILogging Logger = LogHelper.GetLogger(); - public ITask Task { get; internal set; } - public float Percentage { get { return Total > 0 ? (float)(double)Value / Total : 0f; } } - public long Value { get; internal set; } - public long Total { get; internal set; } - public string Message { get; internal set; } + public ITask Task { get; } + public float Percentage { get; private set; } + public long Value { get; private set; } + public long Total { get; private set; } + public string Message { get; private set; } - private long previousValue; + private long previousValue = -1; public event Action OnProgress; - public void UpdateProgress(IProgress progress) + public Progress(ITask task) { - Task = progress.Task; - UpdateProgress(progress.Value, progress.Total, progress.Message); + this.Task = task; } public void UpdateProgress(long value, long total, string message = null) { - Total = total; - Value = value; - Message = message ?? Message; - if (Total == 0 || ((float)(double)Value / Total) - ((float)(double)previousValue / Total) > 1f / 100f) + Total = total == 0 ? 100 : total; + Value = value > Total ? Total : value; + Message = String.IsNullOrEmpty(message) ? Message : message; + float fTotal = Total; + float fValue = Value; + Percentage = fValue / fTotal; + float delta = fValue / fTotal - previousValue / fTotal; + delta = delta * 100f / fTotal; + + if (Value != previousValue && (fValue == 0f || delta > 1f || fValue == fTotal)) { // signal progress in 1% increments or if we don't know what the total is previousValue = Value; OnProgress?.Invoke(this); diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index f622fe84e..a3768bf60 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -14,7 +14,7 @@ public class GitInstaller private readonly GitInstallDetails installDetails; private readonly IZipHelper sharpZipLibHelper; - public IProgress Progress { get; set; } + public IProgress Progress { get; } = new Progress(TaskBase.Default); public GitInstaller(IEnvironment environment, IProcessManager processManager, CancellationToken token, @@ -250,7 +250,7 @@ private GitInstallationState GetZipsIfNeeded(GitInstallationState state) LogHelper.Trace(e, "Failed to download"); return true; }); - downloader.Progress(p => ((Progress)Progress)?.UpdateProgress(20 + (long)(20 * p.Percentage), 100, downloader.Name)); + downloader.Progress(p => Progress.UpdateProgress(20 + (long)(20 * p.Percentage), 100, downloader.Message)); if (!state.GitZipExists && !state.GitIsValid && state.GitPackage != null) downloader.QueueDownload(state.GitPackage.Uri, installDetails.ZipPath); if (!state.GitLfsZipExists && !state.GitLfsIsValid && state.GitLfsPackage != null) @@ -259,7 +259,7 @@ private GitInstallationState GetZipsIfNeeded(GitInstallationState state) state.GitZipExists = installDetails.GitZipPath.FileExists(); state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); - ((Progress)Progress)?.UpdateProgress(30, 100); + Progress.UpdateProgress(30, 100); return state; } @@ -294,7 +294,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) LogHelper.Trace(e, "Failed to unzip " + installDetails.GitZipPath); return true; }); - unzipTask.Progress(p => ((Progress)Progress)?.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Name)); + unzipTask.Progress(p => Progress.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Message)); var path = unzipTask.RunWithReturn(true); var target = state.GitInstallationPath; if (unzipTask.Successful) @@ -319,7 +319,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) LogHelper.Trace(e, "Failed to unzip " + installDetails.GitLfsZipPath); return true; }); - unzipTask.Progress(p => ((Progress)Progress)?.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Name)); + unzipTask.Progress(p => Progress.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Message)); var path = unzipTask.RunWithReturn(true); var target = state.GitLfsInstallationPath; if (unzipTask.Successful) diff --git a/src/GitHub.Api/Installer/UnzipTask.cs b/src/GitHub.Api/Installer/UnzipTask.cs index e5911636e..49a9d4590 100644 --- a/src/GitHub.Api/Installer/UnzipTask.cs +++ b/src/GitHub.Api/Installer/UnzipTask.cs @@ -93,6 +93,7 @@ protected virtual NPath RunUnzip(bool success) return extractedPath; } protected int RetryCount { get; } + public override string Message { get; set; } = "Extracting zip..."; } public class UnzipException : Exception { diff --git a/src/GitHub.Api/Localization.Designer.cs b/src/GitHub.Api/Localization.Designer.cs index 854df3217..4d22ecf73 100644 --- a/src/GitHub.Api/Localization.Designer.cs +++ b/src/GitHub.Api/Localization.Designer.cs @@ -60,6 +60,15 @@ internal Localization() { } } + /// + /// Looks up a localized string similar to Account. + /// + public static string AccountButton { + get { + return ResourceManager.GetString("AccountButton", resourceCulture); + } + } + /// /// Looks up a localized string similar to {0}. /// @@ -162,9 +171,9 @@ public static string FetchActionTitle { /// /// Looks up a localized string similar to Fetch. /// - public static string FetchButtonText { + public static string FetchButton { get { - return ResourceManager.GetString("FetchButtonText", resourceCulture); + return ResourceManager.GetString("FetchButton", resourceCulture); } } @@ -330,6 +339,159 @@ public static string LoginFailed { } } + /// + /// Looks up a localized string similar to Created branch {0}. + /// + public static string MessageBranchCreated { + get { + return ResourceManager.GetString("MessageBranchCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Deleted branch {0}. + /// + public static string MessageBranchDeleted { + get { + return ResourceManager.GetString("MessageBranchDeleted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switched to branch {0}. + /// + public static string MessageBranchSwitched { + get { + return ResourceManager.GetString("MessageBranchSwitched", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Commit failed. + /// + public static string MessageCommitFailed { + get { + return ResourceManager.GetString("MessageCommitFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Committed. + /// + public static string MessageCommitted { + get { + return ResourceManager.GetString("MessageCommitted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Committing. + /// + public static string MessageCommitting { + get { + return ResourceManager.GetString("MessageCommitting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetched. + /// + public static string MessageFetched { + get { + return ResourceManager.GetString("MessageFetched", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetch failed. + /// + public static string MessageFetchFailed { + get { + return ResourceManager.GetString("MessageFetchFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fetching. + /// + public static string MessageFetching { + get { + return ResourceManager.GetString("MessageFetching", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulled. + /// + public static string MessagePulled { + get { + return ResourceManager.GetString("MessagePulled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to pull. + /// + public static string MessagePullFailed { + get { + return ResourceManager.GetString("MessagePullFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pulling. + /// + public static string MessagePulling { + get { + return ResourceManager.GetString("MessagePulling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pushed. + /// + public static string MessagePushed { + get { + return ResourceManager.GetString("MessagePushed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to push. + /// + public static string MessagePushFailed { + get { + return ResourceManager.GetString("MessagePushFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pushing. + /// + public static string MessagePushing { + get { + return ResourceManager.GetString("MessagePushing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refreshed. + /// + public static string MessageRefreshed { + get { + return ResourceManager.GetString("MessageRefreshed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Refreshing. + /// + public static string MessageRefreshing { + get { + return ResourceManager.GetString("MessageRefreshing", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your current project is not currently in an active git repository:. /// diff --git a/src/GitHub.Api/Localization.resx b/src/GitHub.Api/Localization.resx index ec6e32a0a..9fb4fba94 100644 --- a/src/GitHub.Api/Localization.resx +++ b/src/GitHub.Api/Localization.resx @@ -318,7 +318,7 @@ Fetch Changes - + Fetch @@ -345,4 +345,58 @@ Url of the {0} remote + + Account + + + Created branch {0} + + + Deleted branch {0} + + + Switched to branch {0} + + + Commit failed + + + Committed + + + Committing + + + Fetched + + + Fetch failed + + + Fetching + + + Pulled + + + Failed to pull + + + Pulling + + + Pushed + + + Failed to push + + + Pushing + + + Refreshed + + + Refreshing + \ No newline at end of file diff --git a/src/GitHub.Api/Managers/Downloader.cs b/src/GitHub.Api/Managers/Downloader.cs index 25322664b..81481fc43 100644 --- a/src/GitHub.Api/Managers/Downloader.cs +++ b/src/GitHub.Api/Managers/Downloader.cs @@ -28,6 +28,8 @@ class Downloader : FuncListTask private readonly List downloaders = new List(); + public override string Message { get; set; } = "Downloading..."; + public Downloader() : base(TaskManager.Instance.Token, RunDownloaders) { Name = "Downloader"; diff --git a/src/GitHub.Api/Tasks/ITaskManager.cs b/src/GitHub.Api/Tasks/ITaskManager.cs index 27a4c044f..35ed2cea8 100644 --- a/src/GitHub.Api/Tasks/ITaskManager.cs +++ b/src/GitHub.Api/Tasks/ITaskManager.cs @@ -13,7 +13,8 @@ public interface ITaskManager : IDisposable T Schedule(T task) where T : ITask; Task Wait(); - ITask Run(Action action); + ITask Run(Action action, string message); ITask RunInUI(Action action); + event Action OnProgress; } } \ No newline at end of file diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index 04fc4ad3f..b243da3df 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -53,6 +53,7 @@ public interface ITask : IAsyncResult void UpdateProgress(long value, long total, string message = null); ITask GetEndOfChain(); void Run(bool success); + string Message { get; } } public interface ITask : ITask @@ -86,8 +87,10 @@ interface ITask : ITask event Action OnData; } - public abstract class TaskBase : ITask + public class TaskBase : ITask { + public static ITask Default = new TaskBase { Name = "Global" }; + protected const TaskContinuationOptions runAlwaysOptions = TaskContinuationOptions.None; protected const TaskContinuationOptions runOnSuccessOptions = TaskContinuationOptions.OnlyOnRanToCompletion; protected const TaskContinuationOptions runOnFaultOptions = TaskContinuationOptions.OnlyOnFaulted; @@ -157,7 +160,7 @@ protected TaskBase(Task task) protected TaskBase() { - this.progress = new Progress { Task = this }; + this.progress = new Progress(this); } public virtual T Then(T nextTask, TaskRunOptions runOptions = TaskRunOptions.OnSuccess, bool taskIsTopOfChain = false) @@ -407,6 +410,7 @@ public virtual void Run(bool success) protected virtual void RaiseOnStart() { + UpdateProgress(0, 100); OnStart?.Invoke(this); } @@ -433,6 +437,7 @@ protected virtual void RaiseOnEnd() OnEnd?.Invoke(this, !taskFailed, exception); SetupContinuations(); hasRun = true; + UpdateProgress(100, 100); } protected void SetupContinuations() @@ -483,7 +488,7 @@ protected Exception GetThrownException() public void UpdateProgress(long value, long total, string message = null) { - progress.UpdateProgress(value, total, message); + progress.UpdateProgress(value, total, message ?? this.Message); } public override string ToString() @@ -504,6 +509,7 @@ public override string ToString() protected ILogging Logger { get { return logger = logger ?? LogHelper.GetLogger(GetType()); } } public TaskBase DependsOn { get; private set; } public CancellationToken Token { get; } + public virtual string Message { get; set; } } abstract class TaskBase : TaskBase, ITask @@ -663,6 +669,7 @@ public virtual TResult RunWithReturn(bool success) protected override void RaiseOnStart() { + UpdateProgress(0, 100); OnStart?.Invoke(this); base.RaiseOnStart(); } @@ -673,6 +680,7 @@ protected virtual void RaiseOnEnd(TResult data) OnEnd?.Invoke(this, result, !taskFailed, exception); SetupContinuations(); hasRun = true; + UpdateProgress(100, 100); } protected override void CallFinallyHandler() diff --git a/src/GitHub.Api/Tasks/TaskManager.cs b/src/GitHub.Api/Tasks/TaskManager.cs index 560540faf..98a9f185e 100644 --- a/src/GitHub.Api/Tasks/TaskManager.cs +++ b/src/GitHub.Api/Tasks/TaskManager.cs @@ -18,6 +18,13 @@ class TaskManager : ITaskManager private static ITaskManager instance; public static ITaskManager Instance => instance; + private ProgressReporter progressReporter = new ProgressReporter(); + + public event Action OnProgress + { + add { progressReporter.OnProgress += value; } + remove { progressReporter.OnProgress -= value; } + } public TaskManager() { @@ -51,9 +58,9 @@ public static TaskScheduler GetScheduler(TaskAffinity affinity) } } - public ITask Run(Action action) + public ITask Run(Action action, string message) { - return new ActionTask(Token, action).Start(); + return new ActionTask(Token, action) { Message = message }.Start(); } public ITask RunInUI(Action action) @@ -112,6 +119,8 @@ private T ScheduleExclusive(T task, bool setupFaultHandler) TaskContinuationOptions.OnlyOnFaulted, ConcurrentScheduler ); } + + task.Progress(progressReporter.UpdateProgress); return (T)task.Start(manager.ExclusiveTaskScheduler); } @@ -128,6 +137,8 @@ private T ScheduleConcurrent(T task, bool setupFaultHandler) TaskContinuationOptions.OnlyOnFaulted, ConcurrentScheduler ); } + + task.Progress(progressReporter.UpdateProgress); return (T)task.Start((TaskScheduler)manager.ConcurrentTaskScheduler); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs index 5076dc0b9..a8d36bce3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs @@ -181,6 +181,7 @@ abstract class ManagedCacheBase : ScriptObjectSingleton where T : Scriptab [NonSerialized] private DateTimeOffset? lastUpdatedAtValue; [NonSerialized] private DateTimeOffset? initializedAtValue; [NonSerialized] private bool isInvalidating; + [NonSerialized] protected bool forcedInvalidation; public event Action CacheInvalidated; public event Action CacheUpdated; @@ -199,12 +200,18 @@ public bool ValidateData() if (needsInvalidation && !isInvalidating) { Logger.Trace("needsInvalidation isInitialized:{0} timedOut:{1}", isInitialized, timedOut); - InvalidateData(); + Invalidate(); } return !needsInvalidation; } public void InvalidateData() + { + forcedInvalidation = true; + Invalidate(); + } + + private void Invalidate() { if (!isInvalidating) { @@ -449,25 +456,25 @@ public void UpdateData(IRepositoryInfoCacheData data) var now = DateTimeOffset.Now; var isUpdated = false; - if (!Nullable.Equals(currentGitRemote, data.CurrentGitRemote)) + if (forcedInvalidation || !Nullable.Equals(currentGitRemote, data.CurrentGitRemote)) { currentGitRemote = data.CurrentGitRemote ?? GitRemote.Default; isUpdated = true; } - if (!Nullable.Equals(currentGitBranch, data.CurrentGitBranch)) + if (forcedInvalidation ||!Nullable.Equals(currentGitBranch, data.CurrentGitBranch)) { currentGitBranch = data.CurrentGitBranch ?? GitBranch.Default; isUpdated = true; } - if (!Nullable.Equals(currentConfigRemote, data.CurrentConfigRemote)) + if (forcedInvalidation ||!Nullable.Equals(currentConfigRemote, data.CurrentConfigRemote)) { currentConfigRemote = data.CurrentConfigRemote ?? ConfigRemote.Default; isUpdated = true; } - if (!Nullable.Equals(currentConfigBranch, data.CurrentConfigBranch)) + if (forcedInvalidation ||!Nullable.Equals(currentConfigBranch, data.CurrentConfigBranch)) { currentConfigBranch = data.CurrentConfigBranch ?? ConfigBranch.Default; isUpdated = true; @@ -576,9 +583,7 @@ public List Log var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Log: current:{1} new:{2}", now, log.Count, value.Count); - - if (!log.SequenceEqual(value)) + if (forcedInvalidation || !log.SequenceEqual(value)) { log = value; isUpdated = true; @@ -612,8 +617,7 @@ public int Ahead var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Ahead: current:{1} new:{2}", now, ahead, value); - if (ahead != value) + if (forcedInvalidation || ahead != value) { ahead = value; isUpdated = true; @@ -635,9 +639,7 @@ public int Behind var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Behind: current:{1} new:{2}", now, behind, value); - - if (behind != value) + if (forcedInvalidation || behind != value) { behind = value; isUpdated = true; @@ -670,9 +672,7 @@ public List Entries var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Entries: current:{1} new:{2}", now, entries.Count, value.Count); - - if (!entries.SequenceEqual(value)) + if (forcedInvalidation || !entries.SequenceEqual(value)) { entries = value; isUpdated = true; @@ -705,9 +705,7 @@ public List GitLocks var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating GitLocks: current:{1} new:{2}", now, gitLocks.Count, value.Count); - - if (!gitLocks.SequenceEqual(value)) + if (forcedInvalidation || !gitLocks.SequenceEqual(value)) { gitLocks = value; isUpdated = true; @@ -741,9 +739,7 @@ public string Name var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Name: current:{1} new:{2}", now, gitName, value); - - if (gitName != value) + if (forcedInvalidation || gitName != value) { gitName = value; isUpdated = true; @@ -765,9 +761,7 @@ public string Email var now = DateTimeOffset.Now; var isUpdated = false; - Logger.Trace("{0} Updating Email: current:{1} new:{2}", now, gitEmail, value); - - if (gitEmail != value) + if (forcedInvalidation || gitEmail != value) { gitEmail = value; isUpdated = true; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index 16b9d9cc7..5b7e750c9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -57,6 +57,7 @@ class Styles deletedFileLabel, longMessageStyle, headerBoxStyle, + headerStyle, headerBranchLabelStyle, headerUrlLabelStyle, headerRepoLabelStyle, @@ -82,7 +83,8 @@ class Styles historyDetailsTitleStyle, historyDetailsMetaInfoStyle, genericBoxStyle, - hyperlinkStyle; + hyperlinkStyle, + progressAreaBackStyle; private static Texture2D branchIcon, activeBranchIcon, @@ -306,6 +308,21 @@ public static GUIStyle HeaderBoxStyle } } + public static GUIStyle HeaderStyle + { + get + { + if (headerStyle == null) + { + headerStyle = new GUIStyle("IN BigTitle"); + headerStyle.name = "HeaderStyle"; + headerStyle.margin = new RectOffset(0, 0, 0, 0); + headerStyle.padding = new RectOffset(0, 0, 0, 0); + } + return headerStyle; + } + } + public static GUIStyle BoldLabel { get @@ -537,6 +554,7 @@ public static GUIStyle CommitFileAreaStyle commitFileAreaStyle = new GUIStyle(GUI.skin.box); commitFileAreaStyle.name = "CommitFileAreaStyle"; commitFileAreaStyle.margin = new RectOffset(0, 0, 0, 0); + commitFileAreaStyle.padding = new RectOffset(0, 0, 2, 2); } return commitFileAreaStyle; } @@ -572,6 +590,22 @@ public static GUIStyle TextFieldStyle } } + public static GUIStyle ProgressAreaBackStyle + { + get + { + if (progressAreaBackStyle == null) + { + progressAreaBackStyle = new GUIStyle(GUI.skin.FindStyle("ProgressBarBack")); + progressAreaBackStyle.name = "ProgressAreaBackStyle"; + //progressAreaBackStyle.normal.background = Utility.GetTextureFromColor(new Color(194f/255f, 194f/255f, 194f/255f)); + progressAreaBackStyle.margin = new RectOffset(0, 0, 0, 0); + progressAreaBackStyle.padding = new RectOffset(0, 0, 0, 0); + } + return progressAreaBackStyle; + } + } + public static GUIStyle CenteredLabel { get diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 7fb3a22f7..9436f0779 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -209,7 +209,7 @@ private void DoResult(bool success, string msg) isBusy = false; if (success) { - TaskManager.Run(UsageTracker.IncrementAuthenticationViewButtonAuthentication); + TaskManager.Run(UsageTracker.IncrementAuthenticationViewButtonAuthentication, null); Clear(); Finish(true); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index e401ec4d0..889d2f40e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -112,9 +112,15 @@ public virtual void OnDestroy() public virtual void OnSelectionChange() {} + public virtual void DoneRefreshing() + { + IsRefreshing = false; + } + public Rect Position { get { return position; } } public IApplicationManager Manager { get; private set; } public abstract bool IsBusy { get; } + public bool IsRefreshing { get; private set; } public bool HasFocus { get; private set; } public IRepository Repository { get { return inLayout ? cachedRepository : Environment.Repository; } } public bool HasRepository { get { return Repository != null; } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 6c90a6913..f1f64bb2b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -95,8 +95,8 @@ public override void OnDisable() public override void Refresh() { base.Refresh(); - Repository.Refresh(CacheType.Branches); - Repository.Refresh(CacheType.RepositoryInfo); + Refresh(CacheType.Branches); + Refresh(CacheType.RepositoryInfo); } public override void OnDataUpdate() @@ -126,6 +126,7 @@ private void RepositoryOnCurrentBranchAndRemoteChanged(CacheUpdateEvent cacheUpd { if (!lastCurrentBranchAndRemoteChange.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastCurrentBranchAndRemoteChange = cacheUpdateEvent; currentBranchAndRemoteChangeHasUpdate = true; Redraw(); @@ -137,6 +138,7 @@ private void RepositoryOnLocalAndRemoteBranchListChanged(CacheUpdateEvent cacheU { if (!lastLocalAndRemoteBranchListChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastLocalAndRemoteBranchListChangedEvent = cacheUpdateEvent; localAndRemoteBranchListHasUpdate = true; Redraw(); @@ -219,6 +221,8 @@ private void Render() Redraw(); } } + if (ProgressRenderer != null) + ProgressRenderer.DoProgressGUI(); } private void BuildTree() @@ -316,7 +320,7 @@ private void OnButtonBarGUI() { if (success) { - TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCreateBranch); + TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCreateBranch, null); Redraw(); } else @@ -486,7 +490,7 @@ private void CheckoutRemoteBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCheckoutRemoteBranch); + TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCheckoutRemoteBranch, null); Redraw(); } else @@ -509,7 +513,7 @@ private void SwitchBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCheckoutLocalBranch); + TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCheckoutLocalBranch, null); Redraw(); } else diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index d878a152e..f5e8225e8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -63,9 +63,9 @@ public override void OnDisable() public override void Refresh() { base.Refresh(); - Repository.Refresh(CacheType.GitStatus); - Repository.Refresh(CacheType.RepositoryInfo); - Repository.Refresh(CacheType.GitLocks); + Refresh(CacheType.GitStatus); + Refresh(CacheType.RepositoryInfo); + Refresh(CacheType.GitLocks); } public override void OnDataUpdate() @@ -108,11 +108,15 @@ public override void OnGUI() } GUILayout.EndScrollView(); } + + if (ProgressRenderer != null) + ProgressRenderer.DoProgressGUI(); + GUILayout.EndVertical(); GUILayout.EndHorizontal(); // Do the commit details area - OnCommitDetailsAreaGUI(); + DoCommitGUI(); } public override void OnSelectionChange() @@ -194,6 +198,7 @@ private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastStatusEntriesChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastStatusEntriesChangedEvent = cacheUpdateEvent; currentStatusEntriesHasUpdate = true; Redraw(); @@ -204,6 +209,7 @@ private void RepositoryOnCurrentBranchChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastCurrentBranchChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastCurrentBranchChangedEvent = cacheUpdateEvent; currentBranchHasUpdate = true; Redraw(); @@ -214,6 +220,7 @@ private void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastLocksChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastLocksChangedEvent = cacheUpdateEvent; currentLocksHasUpdate = true; Redraw(); @@ -291,7 +298,7 @@ private void BuildTree() Redraw(); } - private void OnCommitDetailsAreaGUI() + private void DoCommitGUI() { GUILayout.BeginHorizontal(); { @@ -369,7 +376,7 @@ private void Commit() { if (success) { - TaskManager.Run(UsageTracker.IncrementChangesViewButtonCommit); + TaskManager.Run(UsageTracker.IncrementChangesViewButtonCommit, null); commitMessage = ""; commitBody = ""; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs index 2d8ad38a8..d91a9e6d1 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs @@ -173,11 +173,12 @@ public override void OnGUI() { GUI.FocusControl(null); isBusy = true; - new FuncTask(Manager.CancellationToken, () => + new FuncTask(Manager.CancellationToken, () => { var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, Manager.CancellationToken); return gitInstaller.FindSystemGit(new GitInstaller.GitInstallationState()); }) + { Message = "Locating git..." } .FinallyInUI((success, ex, state) => { if (success) @@ -242,6 +243,7 @@ private void ValidateAndSetGitInstallPath() } return state; }) + { Message = "Setting up git... " } .FinallyInUI((success, exception, state) => { if (!success) @@ -262,13 +264,15 @@ private void ValidateAndSetGitInstallPath() } else { + var newState = new GitInstaller.GitInstallationState(); + newState.GitExecutablePath = gitPath.ToNPath(); + newState.GitLfsExecutablePath = gitLfsPath.ToNPath(); + var installer = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); + installer.Progress.OnProgress += ProgressRenderer.UpdateProgress; + new FuncTask(TaskManager.Token, () => { - var state = new GitInstaller.GitInstallationState(); - state.GitExecutablePath = gitPath.ToNPath(); - state.GitLfsExecutablePath = gitLfsPath.ToNPath(); - var installer = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); - return installer.SetupGitIfNeeded(state); + return installer.SetupGitIfNeeded(newState); }) .Then((success, state) => { @@ -281,6 +285,7 @@ private void ValidateAndSetGitInstallPath() }) .FinallyInUI((success, ex, state) => { + installer.Progress.OnProgress -= ProgressRenderer.UpdateProgress; if (!success) { Logger.Error(ex, ErrorValidatingGitPath); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index 7e006192a..e2f2cdc7e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -348,8 +348,8 @@ public override void OnDisable() public override void Refresh() { base.Refresh(); - Repository.Refresh(CacheType.GitLog); - Repository.Refresh(CacheType.GitAheadBehind); + Refresh(CacheType.GitLog); + Refresh(CacheType.GitAheadBehind); } public override void OnDataUpdate() @@ -391,6 +391,9 @@ public override void OnGUI() Redraw(); } + if (ProgressRenderer != null) + ProgressRenderer.DoProgressGUI(); + if (!selectedEntry.Equals(GitLogEntry.Default)) { // Top bar for scrolling to selection or clearing it @@ -495,6 +498,7 @@ private void RepositoryOnTrackingStatusChanged(CacheUpdateEvent cacheUpdateEvent { if (!lastTrackingStatusChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastTrackingStatusChangedEvent = cacheUpdateEvent; currentTrackingStatusHasUpdate = true; Redraw(); @@ -505,6 +509,7 @@ private void RepositoryOnLogChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastLogChangedEvent.Equals(cacheUpdateEvent)) { + ReceivedEvent(cacheUpdateEvent.cacheType); lastLogChangedEvent = cacheUpdateEvent; currentLogHasUpdate = true; Redraw(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs index dae2d62c6..80565a01a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs @@ -9,6 +9,7 @@ interface IView void OnDisable(); void Refresh(); void Redraw(); + void DoneRefreshing(); Rect Position { get; } void Finish(bool result); @@ -18,6 +19,7 @@ interface IView bool HasUser { get; } IApplicationManager Manager { get; } bool IsBusy { get; } + bool IsRefreshing { get; } bool HasFocus { get; } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 6a143635d..3eba0888d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -106,7 +106,7 @@ private static void ContextMenu_Lock() { if (success) { - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsLock); + EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); } isBusy = false; @@ -152,7 +152,7 @@ private static void ContextMenu_Unlock() { if (success) { - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } isBusy = false; Selection.activeGameObject = null; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index ae216b66d..16822aca5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -168,7 +168,7 @@ public override void OnGUI() return; } - TaskManager.Run(UsageTracker.IncrementPublishViewButtonPublish); + TaskManager.Run(UsageTracker.IncrementPublishViewButtonPublish, null); if (repository == null) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs index ad3b33156..7b8270db3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs @@ -87,8 +87,8 @@ public override void Refresh() base.Refresh(); gitPathView.Refresh(); userSettingsView.Refresh(); - if (Repository != null) - Repository.Refresh(CacheType.GitLocks); + Refresh(CacheType.RepositoryInfo); + Refresh(CacheType.GitLocks); } public override void OnGUI() @@ -117,6 +117,9 @@ public override void OnGUI() } GUILayout.EndScrollView(); + + if (ProgressRenderer != null) + ProgressRenderer.DoProgressGUI(); } private void AttachHandlers(IRepository repository) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index 7cd4d7381..79a39cca6 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -1,5 +1,6 @@ using GitHub.Logging; -using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace GitHub.Unity @@ -8,6 +9,11 @@ abstract class Subview : IView { private const string NullParentError = "Subview parent is null"; + public Subview() + { + RefreshEvents = new Dictionary(); + } + public virtual void InitializeView(IView parent) { Debug.Assert(parent != null, NullParentError); @@ -45,7 +51,39 @@ public virtual void Finish(bool result) Parent.Finish(result); } + + protected void Refresh(CacheType type) + { + if (Repository == null) + return; + + IsRefreshing = true; + if (!RefreshEvents.ContainsKey(type)) + RefreshEvents.Add(type, 0); + RefreshEvents[type]++; + Repository.Refresh(type); + } + + protected void ReceivedEvent(CacheType type) + { + if (!RefreshEvents.ContainsKey(type)) + RefreshEvents.Add(type, 0); + var val = RefreshEvents[type] - 1; + RefreshEvents[type] = val > -1 ? val : 0; + if (IsRefreshing && !RefreshEvents.Values.Any(x => x > 0)) + { + DoneRefreshing(); + } + } + + public void DoneRefreshing() + { + IsRefreshing = false; + Parent.DoneRefreshing(); + } + protected IView Parent { get; private set; } + protected IUIProgress ProgressRenderer { get { return Parent is Subview ? ((Subview)Parent).ProgressRenderer : Parent as IUIProgress; } } public IApplicationManager Manager { get { return Parent.Manager; } } public IRepository Repository { get { return Parent.Repository; } } public bool HasRepository { get { return Parent.HasRepository; } } @@ -65,6 +103,8 @@ public virtual bool IsBusy public Rect Position { get { return Parent.Position; } } public string Title { get; protected set; } public Vector2 Size { get; protected set; } + protected Dictionary RefreshEvents { get; set; } + public bool IsRefreshing { get; set; } private ILogging logger; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs index efc905d7f..96d95728f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs @@ -29,6 +29,12 @@ public override void InitializeView(IView parent) gitExecutableIsSet = Environment.GitExecutablePath.IsInitialized; } + public override void Refresh() + { + base.Refresh(); + Refresh(CacheType.GitUser); + } + public override void OnDataUpdate() { base.OnDataUpdate(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index e7f319a41..462c90670 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -5,24 +5,32 @@ namespace GitHub.Unity { + interface IUIProgress + { + void DoProgressGUI(); + void UpdateProgress(IProgress progress); + } + [Serializable] - class Window : BaseWindow + class Window : BaseWindow, IUIProgress { - private const float DefaultNotificationTimeout = 4f; + private const float DefaultNotificationTimeout = 2f; private const string Title = "GitHub"; private const string Menu_Window_GitHub = "Window/GitHub"; private const string Menu_Window_GitHub_Command_Line = "Window/GitHub Command Line"; - [NonSerialized] private double notificationClearTime = -1; - [NonSerialized] private double timeSinceLastRotation = -1f; [NonSerialized] private Spinner spinner; - [NonSerialized] private IProgress progress; - [NonSerialized] private float progressValue; - [NonSerialized] private string progressMessage; + [NonSerialized] private IProgress repositoryProgress; + [NonSerialized] private IProgress appManagerProgress; + [SerializeField] private double progressMessageClearTime = -1; + [SerializeField] private double notificationClearTime = -1; + [SerializeField] private double timeSinceLastRotation = -1f; [SerializeField] private bool currentBranchAndRemoteHasUpdate; [SerializeField] private bool currentTrackingStatusHasUpdate; [SerializeField] private bool currentStatusEntriesHasUpdate; + [SerializeField] private bool repositoryProgressHasUpdate; + [SerializeField] private bool appManagerProgressHasUpdate; [SerializeField] private SubTab changeTab = SubTab.InitProject; [SerializeField] private SubTab activeTab = SubTab.InitProject; [SerializeField] private InitProjectView initProjectView = new InitProjectView(); @@ -43,6 +51,21 @@ class Window : BaseWindow [SerializeField] private CacheUpdateEvent lastTrackingStatusChangedEvent; [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; + [SerializeField] private GUIContent pullButtonContent = new GUIContent(Localization.PullButton); + [SerializeField] private GUIContent pushButtonContent = new GUIContent(Localization.PushButton); + [SerializeField] private GUIContent refreshButtonContent = new GUIContent(Localization.RefreshButton); + [SerializeField] private GUIContent fetchButtonContent = new GUIContent(Localization.FetchButton); + [SerializeField] private float repositoryProgressValue; + [SerializeField] private string repositoryProgressMessage; + [SerializeField] private float appManagerProgressValue; + [SerializeField] private string appManagerProgressMessage; + + [MenuItem("GitHub/Select")] + public static void Select() + { + Selection.activeObject = SceneView.currentDrawingSceneView; + } + [MenuItem(Menu_Window_GitHub)] public static void Window_GitHub() { @@ -53,10 +76,11 @@ public static void Window_GitHub() public static void GitHub_CommandLine() { EntryPoint.ApplicationManager.ProcessManager.RunCommandLineWindow(NPath.CurrentDirectory); - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementApplicationMenuMenuItemCommandLine); + EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementApplicationMenuMenuItemCommandLine, null); } #if DEBUG + [MenuItem("GitHub/Select Window")] public static void GitHub_SelectWindow() { @@ -88,7 +112,7 @@ public override void Initialize(IApplicationManager applicationManager) { base.Initialize(applicationManager); - applicationManager.OnProgress += OnProgress; + applicationManager.OnProgress += ApplicationManagerOnProgress; HistoryView.InitializeView(this); ChangesView.InitializeView(this); @@ -108,9 +132,6 @@ public override void OnEnable() { base.OnEnable(); -#if DEVELOPER_BUILD - Selection.activeObject = this; -#endif if (Repository != null) ValidateCachedData(Repository); @@ -119,6 +140,8 @@ public override void OnEnable() if (spinner == null) spinner = new Spinner(); + + ClearProgressMessage(); } public override void OnDisable() @@ -177,92 +200,57 @@ public override void OnSelectionChange() public override void Refresh() { + SetProgressMessage(Localization.MessageRefreshing, 0); base.Refresh(); if (ActiveView != null) ActiveView.Refresh(); - Repaint(); + Redraw(); } - - public override void OnUI() + public override void DoneRefreshing() { - base.OnUI(); - - if (HasRepository) - { - DoActionbarGUI(); - DoHeaderGUI(); - } - - DoToolbarGUI(); + base.DoneRefreshing(); + SetProgressMessage(Localization.MessageRefreshed, 100); + } - var rect = GUILayoutUtility.GetLastRect(); - // GUI for the active tab - if (ActiveView != null) - { - ActiveView.OnGUI(); - } + private void ValidateCachedData(IRepository repository) + { + repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentBranchAndRemoteChangedEvent); + } - if (IsBusy && activeTab != SubTab.Settings && Event.current.type == EventType.Repaint) + private void MaybeUpdateData() + { + if (repositoryProgressHasUpdate) { - if (timeSinceLastRotation < 0) + if (repositoryProgress != null) { - timeSinceLastRotation = EditorApplication.timeSinceStartup; + repositoryProgressMessage = repositoryProgress.Message; + repositoryProgressValue = repositoryProgress.Percentage; + if (progressMessageClearTime == -1f || progressMessageClearTime < EditorApplication.timeSinceStartup + DefaultNotificationTimeout) + progressMessageClearTime = EditorApplication.timeSinceStartup + DefaultNotificationTimeout; } else { - var elapsedTime = (float)(EditorApplication.timeSinceStartup - timeSinceLastRotation); - if (spinner == null) - spinner = new Spinner(); - spinner.Start(elapsedTime); - spinner.Rotate(elapsedTime); - - spinner.Render(); - - rect = new Rect(0f, rect.y + rect.height, Position.width, Position.height - (rect.height + rect.y)); - rect = spinner.Layout(rect); - rect.y += rect.height + 30; - rect.height = 20; - if (!String.IsNullOrEmpty(progressMessage)) - EditorGUI.ProgressBar(rect, progressValue / 100, progressMessage); + repositoryProgressMessage = ""; + repositoryProgressValue = 0; + progressMessageClearTime = -1f; } + repositoryProgressHasUpdate = false; } - } - public override void Update() - { - base.Update(); - - // Notification auto-clear timer override - if (notificationClearTime > 0f && EditorApplication.timeSinceStartup > notificationClearTime) - { - notificationClearTime = -1f; - RemoveNotification(); - Redraw(); - } - - if (IsBusy && activeTab != SubTab.Settings) - { - Redraw(); - } - else - { - timeSinceLastRotation = -1f; - spinner.Stop(); - } - } - - private void ValidateCachedData(IRepository repository) - { - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.RepositoryInfo, lastCurrentBranchAndRemoteChangedEvent); - } - - private void MaybeUpdateData() - { - if (progress != null) + if (appManagerProgressHasUpdate) { - progressValue = progress.Value; - progressMessage = progress.Message; + if (appManagerProgress != null) + { + appManagerProgressValue = appManagerProgress.Percentage; + appManagerProgressMessage = appManagerProgress.Message; + } + else + { + appManagerProgressValue = 0; + appManagerProgressMessage = ""; + } + appManagerProgressHasUpdate = false; } string updatedRepoRemote = null; @@ -372,6 +360,18 @@ private void AttachHandlers(IRepository repository) repository.CurrentBranchAndRemoteChanged += RepositoryOnCurrentBranchAndRemoteChanged; repository.TrackingStatusChanged += RepositoryOnTrackingStatusChanged; repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; + repository.OnProgress += UpdateProgress; + } + + private void DetachHandlers(IRepository repository) + { + if (repository == null) + return; + repository.CurrentBranchAndRemoteChanged -= RepositoryOnCurrentBranchAndRemoteChanged; + repository.TrackingStatusChanged -= RepositoryOnTrackingStatusChanged; + repository.StatusEntriesChanged -= RepositoryOnStatusEntriesChanged; + repository.OnProgress -= UpdateProgress; + Manager.OnProgress -= ApplicationManagerOnProgress; } private void RepositoryOnCurrentBranchAndRemoteChanged(CacheUpdateEvent cacheUpdateEvent) @@ -404,18 +404,100 @@ private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) } } - private void OnProgress(IProgress progr) + private static object lck = new object(); + public void UpdateProgress(IProgress progress) { - progress = progr; + lock (lck) + { + repositoryProgress = progress; + if (repositoryProgress != null && progress != null) + { + repositoryProgress.UpdateProgress(progress.Value, progress.Total, progress.Message); + } + repositoryProgressHasUpdate = true; + } + + if (!ThreadingHelper.InUIThread) + TaskManager.RunInUI(Redraw); + else + Redraw(); } - private void DetachHandlers(IRepository repository) + private void ApplicationManagerOnProgress(IProgress progress) { - if (repository == null) - return; - repository.CurrentBranchAndRemoteChanged -= RepositoryOnCurrentBranchAndRemoteChanged; - repository.TrackingStatusChanged -= RepositoryOnTrackingStatusChanged; - repository.StatusEntriesChanged -= RepositoryOnStatusEntriesChanged; + Debug.LogFormat("ApplicationManagerOnProgress {0} {1}", progress.Percentage, progress.Message); + appManagerProgress = progress; + appManagerProgressHasUpdate = true; + } + + public override void OnUI() + { + base.OnUI(); + + GUILayout.BeginVertical(Styles.HeaderStyle); + + if (HasRepository) + { + DoActionbarGUI(); + DoHeaderGUI(); + } + + DoToolbarGUI(); + DoActiveViewGUI(); + + GUILayout.EndVertical(); + } + + public override void Update() + { + base.Update(); + + // Notification auto-clear timer override + if (notificationClearTime > 0f && EditorApplication.timeSinceStartup > notificationClearTime) + { + notificationClearTime = -1f; + RemoveNotification(); + Redraw(); + } + + // Notification auto-clear timer override + if (progressMessageClearTime > 0f && EditorApplication.timeSinceStartup > progressMessageClearTime) + { + repositoryProgressHasUpdate = true; + ClearProgressMessage(); + } + else if (EditorApplication.timeSinceStartup < progressMessageClearTime) + { + Redraw(); + } + + if (IsBusy && activeTab != SubTab.Settings) + { + Redraw(); + } + else + { + timeSinceLastRotation = -1f; + spinner.Stop(); + } + } + + public void DoProgressGUI() + { + Rect rect1 = GUILayoutUtility.GetRect(position.width, 20); + if (Event.current.GetTypeForControl(GUIUtility.GetControlID("ghu_ProgressBar".GetHashCode(), FocusType.Keyboard, position)) == EventType.Repaint) + { + var style = Styles.ProgressAreaBackStyle; + style.Draw(rect1, false, false, false, false); + Rect rect2 = new Rect(rect1.x, rect1.y, position.width * repositoryProgressValue, rect1.height); + style = GUI.skin.FindStyle("ProgressBarBar"); + style.Draw(rect2, false, false, false, false); + style = GUI.skin.FindStyle("ProgressBarText"); + style.Draw(rect1, repositoryProgressMessage, false, false, false, false); + } + + if (repositoryProgressValue == 1f) + Redraw(); } private void DoHeaderGUI() @@ -482,14 +564,14 @@ private void DoActionbarGUI() EditorGUI.BeginDisabledGroup(currentRemoteName == null); { // Fetch button - var fetchClicked = GUILayout.Button(Localization.FetchButtonText, Styles.ToolbarButtonStyle); + var fetchClicked = GUILayout.Button(fetchButtonContent, Styles.ToolbarButtonStyle); if (fetchClicked) { Fetch(); } // Pull button - var pullButtonText = statusBehind > 0 ? String.Format(Localization.PullButtonCount, statusBehind) : Localization.PullButton; + var pullButtonText = statusBehind > 0 ? new GUIContent(String.Format(Localization.PullButtonCount, statusBehind)) : pullButtonContent; var pullClicked = GUILayout.Button(pullButtonText, Styles.ToolbarButtonStyle); if (pullClicked && @@ -507,7 +589,7 @@ private void DoActionbarGUI() // Push button EditorGUI.BeginDisabledGroup(currentRemoteName == null || statusAhead == 0); { - var pushButtonText = statusAhead > 0 ? String.Format(Localization.PushButtonCount, statusAhead) : Localization.PushButton; + var pushButtonText = statusAhead > 0 ? new GUIContent(String.Format(Localization.PushButtonCount, statusAhead)) : pushButtonContent; var pushClicked = GUILayout.Button(pushButtonText, Styles.ToolbarButtonStyle); if (pushClicked && @@ -531,19 +613,54 @@ private void DoActionbarGUI() } } - if (GUILayout.Button(Localization.RefreshButton, Styles.ToolbarButtonStyle)) + if (GUILayout.Button(refreshButtonContent, Styles.ToolbarButtonStyle)) { Refresh(); } GUILayout.FlexibleSpace(); - if (GUILayout.Button("Account", EditorStyles.toolbarDropDown)) + if (GUILayout.Button(Localization.AccountButton, EditorStyles.toolbarDropDown)) DoAccountDropdown(); } EditorGUILayout.EndHorizontal(); } + private void DoActiveViewGUI() + { + var rect = GUILayoutUtility.GetLastRect(); + // GUI for the active tab + if (ActiveView != null) + { + ActiveView.OnGUI(); + } + + if (IsBusy && activeTab != SubTab.Settings && Event.current.type == EventType.Repaint) + { + if (timeSinceLastRotation < 0) + { + timeSinceLastRotation = EditorApplication.timeSinceStartup; + } + else + { + var elapsedTime = (float)(EditorApplication.timeSinceStartup - timeSinceLastRotation); + if (spinner == null) + spinner = new Spinner(); + spinner.Start(elapsedTime); + spinner.Rotate(elapsedTime); + + spinner.Render(); + + rect = new Rect(0f, rect.y + rect.height, Position.width, Position.height - (rect.height + rect.y)); + rect = spinner.Layout(rect); + rect.y += rect.height + 30; + rect.height = 20; + if (!String.IsNullOrEmpty(appManagerProgressMessage)) + EditorGUI.ProgressBar(rect, appManagerProgressValue, appManagerProgressMessage); + } + } + } + private void Pull() { if (hasItemsToCommit) @@ -552,12 +669,15 @@ private void Pull() } else { + SetProgressMessage(Localization.MessagePulling, 0, 60f); Repository .Pull() - .FinallyInUI((success, e) => { + .FinallyInUI((success, e) => + { if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarPull); + SetProgressMessage(Localization.MessagePulled, 100); + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarPull, null); EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), @@ -565,6 +685,7 @@ private void Pull() } else { + SetProgressMessage(Localization.MessagePullFailed, 100); EditorUtility.DisplayDialog(Localization.PullActionTitle, Localization.PullFailureDescription, Localization.Ok); @@ -576,12 +697,15 @@ private void Pull() private void Push() { + SetProgressMessage(Localization.MessagePushing, 0, 60f); Repository .Push() - .FinallyInUI((success, e) => { + .FinallyInUI((success, e) => + { if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarPush); + SetProgressMessage(Localization.MessagePushed, 100); + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarPush, null); EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), @@ -589,6 +713,7 @@ private void Push() } else { + SetProgressMessage(Localization.MessagePushFailed, 100); EditorUtility.DisplayDialog(Localization.PushActionTitle, Localization.PushFailureDescription, Localization.Ok); @@ -599,13 +724,19 @@ private void Push() private void Fetch() { + SetProgressMessage(Localization.MessageFetching, 0, 60f); Repository .Fetch() - .FinallyInUI((success, e) => { - if (!success) + .FinallyInUI((success, e) => + { + if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarFetch); - + SetProgressMessage(Localization.MessageFetched, 100); + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarFetch, null); + } + else + { + SetProgressMessage(Localization.MessageFetchFailed, 100); EditorUtility.DisplayDialog(Localization.FetchActionTitle, Localization.FetchFailureDescription, Localization.Ok); } @@ -696,6 +827,28 @@ public void ShowNotification(GUIContent content, float timeout) base.ShowNotification(content); } + private void SetProgressMessage(string message, long value) + { + SetProgressMessage(message, value, DefaultNotificationTimeout); + } + + private void SetProgressMessage(string message, long value, float timeout) + { + progressMessageClearTime = EditorApplication.timeSinceStartup + timeout; + if (repositoryProgress == null) + repositoryProgress = new Progress(TaskBase.Default); + repositoryProgress.UpdateProgress(value, repositoryProgress.Total, message); + UpdateProgress(repositoryProgress); + Redraw(); + } + + private void ClearProgressMessage() + { + progressMessageClearTime = -1f; + UpdateProgress(null); + Redraw(); + } + private static SubTab TabButton(SubTab tab, string title, SubTab currentTab) { return GUILayout.Toggle(currentTab == tab, title, EditorStyles.toolbarButton) ? tab : currentTab; From f8bf8877b072a3d2de1c97e4dbe2addeaea73701 Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 07:29:45 -0700 Subject: [PATCH 217/567] Create how-to-install-and-update.md --- docs/using/how-to-install-and-update.md | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/using/how-to-install-and-update.md diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md new file mode 100644 index 000000000..33075bbc3 --- /dev/null +++ b/docs/using/how-to-install-and-update.md @@ -0,0 +1,44 @@ +# Installation and Updating Guide +[Installing from a Downloaded Package File](#Installing-from-a-Package-File) +[Installing from the Unity Asset Store](#Installing-from-the-Asset-Store) +[Updating a Previously Installed Version](#Updating-the-GitHub-for-Unity-Package) + + +# Installing from a Package File +You can download the latest release from the repository at [https://github.com/github-for-unity/Unity/releases](https://github.com/github-for-unity/Unity/releases) or from the website at [https://unity.github.com/](https://unity.github.com/). + +Once you've downloaded the package file, you can quickly install it within Unity. +1. Open a new Unity Project +screen shot 2018-05-18 at 7 10 07 am +2. Go to `Assets -> Import Package -> Custom Package` +screen shot 2018-05-18 at 7 11 35 am +3. Find and open the GitHub for Unity Asset Package +screen shot 2018-05-18 at 7 12 33 am +4. Click `Import` when the dialog pops up +screen shot 2018-05-18 at 7 12 42 am +5. After the package finishes importing, go to `Window -> GitHub` to open the GitHub window +screen shot 2018-05-18 at 7 13 03 am +6. You should see the GitHub window in your Unity project, with a button to initialize your repository +screen shot 2018-05-18 at 7 13 34 am + +# Installing from the Asset Store +1. Open a new Unity Project +screen shot 2018-05-18 at 7 10 07 am +2. Go to `Window -> Asset Store` +screen shot 2018-05-18 at 7 20 19 am +3. Search for "GitHub" in the search bar +screen shot 2018-05-18 at 7 20 51 am +4. Click on the GitHub for Unity Package +screen shot 2018-05-18 at 7 22 15 am +5. Click `Download` then `Install` +screen shot 2018-05-18 at 7 21 37 am +screen shot 2018-05-18 at 7 21 44 am +6. Click `Import` when the dialog pops up +screen shot 2018-05-18 at 7 12 42 am +7. After the package finishes importing, go to `Window -> GitHub` to open the GitHub window +screen shot 2018-05-18 at 7 13 03 am +8. You should see the GitHub window in your Unity project, with a button to initialize your repository +screen shot 2018-05-18 at 7 13 34 am + +# Updating the GitHub for Unity Package +_COMING SOON_ From 323e9aa352d8655d0e76ca5193f23d77ce555e17 Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 07:30:29 -0700 Subject: [PATCH 218/567] Update how-to-install-and-update.md --- docs/using/how-to-install-and-update.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md index 33075bbc3..948b4ed93 100644 --- a/docs/using/how-to-install-and-update.md +++ b/docs/using/how-to-install-and-update.md @@ -1,7 +1,7 @@ # Installation and Updating Guide -[Installing from a Downloaded Package File](#Installing-from-a-Package-File) -[Installing from the Unity Asset Store](#Installing-from-the-Asset-Store) -[Updating a Previously Installed Version](#Updating-the-GitHub-for-Unity-Package) +[Installing from a Downloaded Package File](#installing-from-a-package-file) +[Installing from the Unity Asset Store](#installing-from-the-asset-store) +[Updating a Previously Installed Version](#updating-the-github-for-unity-package) # Installing from a Package File From d0ace765ccb4dbb5134b145c26713943bc78f7dd Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 07:41:55 -0700 Subject: [PATCH 219/567] Create getting-started.md --- docs/using/getting-started.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/using/getting-started.md diff --git a/docs/using/getting-started.md b/docs/using/getting-started.md new file mode 100644 index 000000000..925c2b83e --- /dev/null +++ b/docs/using/getting-started.md @@ -0,0 +1,12 @@ +# Getting Started with GitHub for Unity +There are often three states that you might be in when you are setting up your GitHub for Unity package with your Unity project: +[Setting up a brand new repository](#setting-up-a-new-repository): You do not currently have your Unity project in a repository and you want to publish it to one. +[Connecting to an existing respository](#connecting-to-an-existing-repository): Your Unity project is already in a repository, but you do not have the GitHub for Unity package installed within it yet. +[Opening a Unity project that already has the GitHub for Unity package](#connecting-to-an-existing-repository-that-already-has-the-github-for-unity-package): Your Unity project is already in a repository and it already has the GitHub for Unity package and you are trying to setup a new machine. + +# Setting up a New Repository +When you + +# Connecting to an Existing Respository + +# Connecting to an Existing Repository that already has the GitHub for Unity package From 9d58fb8ce588a79c5e20b5624ef855658bc824c9 Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 07:42:20 -0700 Subject: [PATCH 220/567] Update how-to-install-and-update.md --- docs/using/how-to-install-and-update.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md index 948b4ed93..72df9d774 100644 --- a/docs/using/how-to-install-and-update.md +++ b/docs/using/how-to-install-and-update.md @@ -3,6 +3,8 @@ [Installing from the Unity Asset Store](#installing-from-the-asset-store) [Updating a Previously Installed Version](#updating-the-github-for-unity-package) +_Note: If your project is already in a remote repository, see our [Getting Started](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md) docs before continuing_ + # Installing from a Package File You can download the latest release from the repository at [https://github.com/github-for-unity/Unity/releases](https://github.com/github-for-unity/Unity/releases) or from the website at [https://unity.github.com/](https://unity.github.com/). From 9901bf37e7ba8299cdcf2a848dd586dab9b84e8f Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 07:42:57 -0700 Subject: [PATCH 221/567] Update how-to-install-and-update.md --- docs/using/how-to-install-and-update.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md index 72df9d774..5f357cf79 100644 --- a/docs/using/how-to-install-and-update.md +++ b/docs/using/how-to-install-and-update.md @@ -3,7 +3,7 @@ [Installing from the Unity Asset Store](#installing-from-the-asset-store) [Updating a Previously Installed Version](#updating-the-github-for-unity-package) -_Note: If your project is already in a remote repository, see our [Getting Started](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md) docs before continuing_ +_Note: If your Unity project already has the GitHub for Unity plugin installed, see our [Getting Started](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md) docs before continuing_ # Installing from a Package File From f7f5dc28fad60e97cd18156b07556d9d74524754 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 17:55:20 +0200 Subject: [PATCH 222/567] Fix styling and selection of locks --- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 44 ++++++- .../Editor/GitHub.Unity/UI/LocksView.cs | 118 ++++++++---------- 2 files changed, 93 insertions(+), 69 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index efd27bcda..3f4401329 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -86,7 +86,9 @@ class Styles historyDetailsTitleStyle, historyDetailsMetaInfoStyle, genericBoxStyle, - hyperlinkStyle; + hyperlinkStyle, + selectedArea, + selectedLabel; private static Texture2D branchIcon, activeBranchIcon, @@ -202,6 +204,23 @@ public static GUIStyle HistoryFileTreeBoxStyle } } + public static GUIStyle SelectedArea + { + get + { + if (selectedArea == null) + { + selectedArea = new GUIStyle(GUI.skin.label); + selectedArea.name = "SelectedArea"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + selectedArea.normal.background = hierarchyStyle.onFocused.background; + selectedArea.focused.background = hierarchyStyle.onFocused.background; + } + return selectedArea; + } + } + public static GUIStyle Label { get @@ -216,11 +235,34 @@ public static GUIStyle Label label.onNormal.textColor = hierarchyStyle.onNormal.textColor; label.onFocused.background = hierarchyStyle.onFocused.background; label.onFocused.textColor = hierarchyStyle.onFocused.textColor; + label.wordWrap = true; } return label; } } + public static GUIStyle SelectedLabel + { + get + { + if (selectedLabel == null) + { + selectedLabel = new GUIStyle(GUI.skin.label); + selectedLabel.name = "SelectedLabel"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + selectedLabel.onNormal.background = hierarchyStyle.onFocused.background; + selectedLabel.onNormal.textColor = hierarchyStyle.onFocused.textColor; + selectedLabel.onFocused.background = hierarchyStyle.onFocused.background; + selectedLabel.onFocused.textColor = hierarchyStyle.onFocused.textColor; + selectedLabel.normal.background = hierarchyStyle.onFocused.background; + selectedLabel.normal.textColor = hierarchyStyle.onFocused.textColor; + selectedLabel.wordWrap = true; + } + return selectedLabel; + } + } + public static GUIStyle HeaderBranchLabelStyle { get diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 5ffd311d6..5cb840166 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -17,16 +17,18 @@ public class GitLockEntry { public static GitLockEntry Default = new GitLockEntry(GitLock.Default, GitFileStatus.None); - [SerializeField] private GitLock gitLock; - [SerializeField] private GitFileStatus gitFileStatus; - [NonSerialized] public Texture Icon; [NonSerialized] public Texture IconBadge; + [SerializeField] private GitLock gitLock; + [SerializeField] private GitFileStatus gitFileStatus; + [SerializeField] private string lockedAt; + public GitLockEntry(GitLock gitLock, GitFileStatus gitFileStatus) { this.gitLock = gitLock; this.gitFileStatus = gitFileStatus; + this.lockedAt = gitLock.LockedAt.ToLocalTime().CreateRelativeTime(DateTimeOffset.Now); } public GitLock GitLock @@ -39,29 +41,23 @@ public GitFileStatus GitFileStatus get { return gitFileStatus; } } - public string PrettyTimeString - { - get - { - return gitLock.LockedAt.ToLocalTime().CreateRelativeTime(DateTimeOffset.Now); - } - } + public string LockedAt { get { return lockedAt; } } } [Serializable] class LocksControl { - [SerializeField] private Vector2 scroll; - [SerializeField] private List gitLockEntries = new List(); - [SerializeField] public GitLockEntryDictionary assets = new GitLockEntryDictionary(); - [SerializeField] public GitStatusDictionary gitStatusDictionary = new GitStatusDictionary(); - [NonSerialized] private Action rightClickNextRender; [NonSerialized] private GitLockEntry rightClickNextRenderEntry; - [NonSerialized] private GitLockEntry selectedEntry; [NonSerialized] private int controlId; [NonSerialized] private UnityEngine.Object lastActivatedObject; + [SerializeField] private Vector2 scroll; + [SerializeField] private List gitLockEntries = new List(); + [SerializeField] public GitLockEntryDictionary assets = new GitLockEntryDictionary(); + [SerializeField] public GitStatusDictionary gitStatusDictionary = new GitStatusDictionary(); + [SerializeField] private GitLockEntry selectedEntry; + public GitLockEntry SelectedEntry { get @@ -72,8 +68,8 @@ public GitLockEntry SelectedEntry { selectedEntry = value; - var activeObject = selectedEntry != null - ? AssetDatabase.LoadMainAssetAtPath(selectedEntry.GitLock.Path) + var activeObject = selectedEntry != null && selectedEntry.GitLock != GitLock.Default + ? AssetDatabase.LoadMainAssetAtPath(selectedEntry.GitLock.Path.MakeAbsolute().RelativeTo(EntryPoint.Environment.UnityProjectPath)) : null; lastActivatedObject = activeObject; @@ -115,16 +111,16 @@ public bool Render(Rect containingRect, Action singleClick = null, var entryRect = new Rect(rect.x, rect.y, rect.width, Styles.LocksEntryHeight); var shouldRenderEntry = !(entryRect.y > endDisplay || entryRect.yMax < startDisplay); - if (shouldRenderEntry && Event.current.type == EventType.Repaint) + if (shouldRenderEntry) { - RenderEntry(entryRect, entry); + entryRect = RenderEntry(entryRect, entry); } var entryRequiresRepaint = HandleInput(entryRect, entry, index, singleClick, doubleClick, rightClick); requiresRepaint = requiresRepaint || entryRequiresRepaint; - rect.y += Styles.LocksEntryHeight; + rect.y += entryRect.height; } GUILayout.Space(rect.y - containingRect.y); @@ -134,30 +130,30 @@ public bool Render(Rect containingRect, Action singleClick = null, return requiresRepaint; } - private void RenderEntry(Rect entryRect, GitLockEntry entry) + private Rect RenderEntry(Rect entryRect, GitLockEntry entry) { var isSelected = entry == SelectedEntry; - var iconWidth = 32; var iconHeight = 32; - var iconRect = new Rect(entryRect.x + Styles.BaseSpacing / 2, entryRect.y + (Styles.LocksEntryHeight - iconHeight) / 2, iconWidth, iconHeight); - var iconBadgeWidth = 16; - var iconBasgeHeight = 16; - var iconBadgeRect = new Rect(iconRect.x + iconBadgeWidth, iconRect.y + iconBasgeHeight, iconBadgeWidth, iconBasgeHeight); - - var entryBodyX = iconRect.x + iconRect.width + Styles.BaseSpacing / 2; - - var pathRect = new Rect(entryBodyX, entryRect.y + Styles.BaseSpacing, entryRect.width - entryBodyX, 11f * 2); - var metaDataRect = new Rect(entryBodyX, pathRect.y + pathRect.height + 2, entryRect.width - entryBodyX, 9f * 2); - + var iconBadgeHeight = 16; var hasKeyboardFocus = GUIUtility.keyboardControl == controlId; - Styles.Label.Draw(entryRect, GUIContent.none, false, false, isSelected, hasKeyboardFocus); - Styles.Label.Draw(iconRect, entry.Icon, false, false, isSelected, hasKeyboardFocus); - Styles.Label.Draw(iconBadgeRect, entry.IconBadge, false, false, isSelected, hasKeyboardFocus); - Styles.LockPathStyle.Draw(pathRect, entry.GitLock.Path, false, false, isSelected, hasKeyboardFocus); - Styles.LockMetaDataStyle.Draw(metaDataRect, string.Format("Locked {0} by {1}", entry.PrettyTimeString, entry.GitLock.Owner.Name), false, false, isSelected, hasKeyboardFocus); + GUILayout.BeginHorizontal(isSelected ? Styles.SelectedArea : Styles.Label); + GUILayout.Label(entry.Icon, GUILayout.Height(iconWidth), GUILayout.Width(iconHeight)); + if (Event.current.type == EventType.Repaint) + { + var iconRect = GUILayoutUtility.GetLastRect(); + var iconBadgeRect = new Rect(iconRect.x + iconBadgeWidth, iconRect.y + iconBadgeHeight, iconBadgeWidth, iconBadgeHeight); + Styles.Label.Draw(iconBadgeRect, entry.IconBadge, false, false, false, hasKeyboardFocus); + } + GUILayout.BeginVertical(); + GUILayout.Label(entry.GitLock.Path, isSelected ? Styles.SelectedLabel : Styles.Label); + GUILayout.Label(string.Format("Locked {0} by {1}", entry.LockedAt, entry.GitLock.Owner.Name), isSelected ? Styles.SelectedLabel : Styles.Label); + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + var itemRect = GUILayoutUtility.GetLastRect(); + return itemRect; } private bool HandleInput(Rect rect, GitLockEntry entry, int index, Action singleClick = null, @@ -214,32 +210,32 @@ private bool HandleInput(Rect rect, GitLockEntry entry, int index, Action locks, List gitStatusEntries) { - var statusEntries = gitStatusEntries.ToDictionary(entry => entry.Path.ToNPath().ToString(SlashMode.Forward), entry => entry.status); - + var statusEntries = new Dictionary(); + for (int i = 0; i < gitStatusEntries.Count; i++) + statusEntries.Add(gitStatusEntries[i].Path.ToNPath().ToString(SlashMode.Forward), i); var selectedLockId = SelectedEntry != null && SelectedEntry.GitLock != GitLock.Default ? (int?) SelectedEntry.GitLock.ID : null; var scrollValue = scroll.y; - var previousCount = gitLockEntries.Count; - var scrollIndex = (int)(scrollValue / Styles.LocksEntryHeight); assets.Clear(); - gitLockEntries = locks.Select(gitLock => { - - GitFileStatus gitFileStatus; - if (!statusEntries.TryGetValue(gitLock.Path.ToString(SlashMode.Forward), out gitFileStatus)) + gitLockEntries = locks.Select(gitLock => + { + int index = -1; + GitFileStatus gitFileStatus = GitFileStatus.None; + if (statusEntries.TryGetValue(gitLock.Path.ToString(SlashMode.Forward), out index)) { - gitFileStatus = GitFileStatus.None; + gitFileStatus = gitStatusEntries[index].Status; } var gitLockEntry = new GitLockEntry(gitLock, gitFileStatus); LoadIcon(gitLockEntry, true); - - var assetGuid = AssetDatabase.AssetPathToGUID(gitLock.Path); + var path = gitLock.Path.MakeAbsolute().RelativeTo(EntryPoint.Environment.UnityProjectPath); + var assetGuid = AssetDatabase.AssetPathToGUID(path); if (!string.IsNullOrEmpty(assetGuid)) { assets.Add(assetGuid, gitLockEntry); @@ -359,12 +355,10 @@ public bool OnSelectionChange() if (!LocksControlHasFocus) { GitLockEntry gitLockEntry = GitLockEntry.Default; - if (Selection.activeObject != lastActivatedObject) { var activeAssetPath = AssetDatabase.GetAssetPath(Selection.activeObject); var activeAssetGuid = AssetDatabase.AssetPathToGUID(activeAssetPath); - assets.TryGetValue(activeAssetGuid, out gitLockEntry); } @@ -383,7 +377,6 @@ class LocksView : Subview [NonSerialized] private bool currentLocksHasUpdate; [SerializeField] private LocksControl locksControl; - [SerializeField] private GitLock selectedEntry = GitLock.Default; [SerializeField] private CacheUpdateEvent lastLocksChangedEvent; [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; @@ -430,11 +423,10 @@ public override void OnGUI() var rect = GUILayoutUtility.GetLastRect(); if (locksControl != null) { - var lockControlRect = new Rect(0f, 0f, Position.width, Position.height - rect.height); + var lockControlRect = new Rect(rect.x, rect.y, Position.width, Position.height - rect.height); var requiresRepaint = locksControl.Render(lockControlRect, entry => { - selectedEntry = entry; }, entry => { }, entry => { @@ -446,11 +438,8 @@ public override void OnGUI() unlockFile = "Unlock File"; menuFunction = UnlockSelectedEntry; } - else - { - unlockFile = "Force Unlock File"; - menuFunction = ForceUnlockSelectedEntry; - } + unlockFile = "Force Unlock File"; + menuFunction = ForceUnlockSelectedEntry; var menu = new GenericMenu(); menu.AddItem(new GUIContent(unlockFile), false, menuFunction); @@ -465,14 +454,14 @@ public override void OnGUI() private void UnlockSelectedEntry() { Repository - .ReleaseLock(selectedEntry.Path, false) + .ReleaseLock(locksControl.SelectedEntry.GitLock.Path, false) .Start(); } private void ForceUnlockSelectedEntry() { Repository - .ReleaseLock(selectedEntry.Path, true) + .ReleaseLock(locksControl.SelectedEntry.GitLock.Path, true) .Start(); } @@ -549,7 +538,6 @@ private void MaybeUpdateData() { currentStatusEntriesHasUpdate = false; currentLocksHasUpdate = false; - BuildLocksControl(); } } @@ -562,12 +550,6 @@ private void BuildLocksControl() } locksControl.Load(lockedFiles, gitStatusEntries); - - if (!selectedEntry.Equals(GitLock.Default) - && selectedEntry.ID != locksControl.SelectedEntry.GitLock.ID) - { - selectedEntry = GitLock.Default; - } } public override void OnSelectionChange() { From 67184678f3c136f8670ccbea798bfd09cef8d23d Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 19:27:17 +0200 Subject: [PATCH 223/567] Fix locks serialization and consolidate resource strings --- src/GitHub.Api/Git/GitClient.cs | 1 + src/GitHub.Api/Git/GitLock.cs | 24 +++- src/GitHub.Api/Git/IRepository.cs | 3 +- src/GitHub.Api/Git/Repository.cs | 11 +- src/GitHub.Api/Helpers/Constants.cs | 7 +- src/GitHub.Api/Helpers/SimpleJson.cs | 2 +- src/GitHub.Api/Localization.Designer.cs | 63 +++++++++++ src/GitHub.Api/Localization.resx | 21 ++++ src/GitHub.Api/Primitives/TheVersion.cs | 1 + .../Editor/GitHub.Unity/UI/InitProjectView.cs | 2 +- .../Editor/GitHub.Unity/UI/LocksView.cs | 105 +++++++++++++----- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 66 +++++++++-- .../GitHub.Unity/UI/UserSettingsView.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 3 +- .../Assets/Editor/GitHub.Unity/UpdateCheck.cs | 3 +- .../Primitives/SerializationTests.cs | 12 +- 16 files changed, 271 insertions(+), 55 deletions(-) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 0e7091c31..9570920a9 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -306,6 +306,7 @@ public ITask Unlock(NPath file, bool force, protected static ILogging Logger { get; } = LogHelper.GetLogger(); } + [Serializable] public struct GitUser { public static GitUser Default = new GitUser(); diff --git a/src/GitHub.Api/Git/GitLock.cs b/src/GitHub.Api/Git/GitLock.cs index 120eb8688..fd1f5d7e8 100644 --- a/src/GitHub.Api/Git/GitLock.cs +++ b/src/GitHub.Api/Git/GitLock.cs @@ -1,4 +1,6 @@ using System; +using System.Globalization; +using GitHub.Logging; namespace GitHub.Unity { @@ -10,8 +12,24 @@ public struct GitLock public int id; public string path; public GitUser owner; - public DateTimeOffset locked_at; - + [NotSerialized] public string lockedAtString; + public DateTimeOffset locked_at + { + get + { + DateTimeOffset dt; + if (!DateTimeOffset.TryParseExact(lockedAtString, Constants.Iso8601Formats, + CultureInfo.InvariantCulture, Constants.DateTimeStyle, out dt)) + { + return DateTimeOffset.MinValue; + } + return dt; + } + set + { + lockedAtString = value.ToUniversalTime().ToString(Constants.Iso8601FormatZ, CultureInfo.InvariantCulture); + } + } [NotSerialized] public int ID => id; [NotSerialized] public NPath Path => path.ToNPath(); [NotSerialized] public GitUser Owner => owner; @@ -22,7 +40,7 @@ public GitLock(int id, NPath path, GitUser owner, DateTimeOffset locked_at) this.id = id; this.path = path; this.owner = owner; - this.locked_at = locked_at; + this.lockedAtString = locked_at.ToUniversalTime().ToString(Constants.Iso8601FormatZ, CultureInfo.InvariantCulture); } public override bool Equals(object other) diff --git a/src/GitHub.Api/Git/IRepository.cs b/src/GitHub.Api/Git/IRepository.cs index f7ad3571c..a7b9c53da 100644 --- a/src/GitHub.Api/Git/IRepository.cs +++ b/src/GitHub.Api/Git/IRepository.cs @@ -6,7 +6,7 @@ namespace GitHub.Unity /// /// Represents a repository, either local or retrieved via the GitHub API. /// - public interface IRepository : IEquatable, IDisposable + public interface IRepository : IEquatable, IDisposable, IBackedByCache { void Initialize(IRepositoryManager theRepositoryManager, ITaskManager theTaskManager); void Start(); @@ -21,7 +21,6 @@ public interface IRepository : IEquatable, IDisposable ITask RequestLock(NPath file); ITask ReleaseLock(NPath file, bool force); ITask DiscardChanges(GitStatusEntry[] discardEntries); - void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent); /// /// Gets the name of the repository. diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 292fbea1b..09da7c6d2 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -4,10 +4,14 @@ using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Threading; namespace GitHub.Unity { + public interface IBackedByCache + { + void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent); + } + [DebuggerDisplay("{DebuggerDisplay,nq}")] sealed class Repository : IEquatable, IRepository { @@ -380,12 +384,11 @@ public string Name CloneUrl, LocalPath, CurrentBranch, CurrentRemote); } - public interface IUser + public interface IUser : IBackedByCache { string Name { get; } string Email { get; } event Action Changed; - void CheckUserChangedEvent(CacheUpdateEvent cacheUpdateEvent); void Initialize(IGitClient client); void SetNameAndEmail(string name, string email); } @@ -406,7 +409,7 @@ public User(ICacheContainer cacheContainer) cacheContainer.CacheUpdated += (type, dt) => { if (type == CacheType.GitUser) CacheHasBeenUpdated(dt); }; } - public void CheckUserChangedEvent(CacheUpdateEvent cacheUpdateEvent) => cacheContainer.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, cacheUpdateEvent); + public void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent) => cacheContainer.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, cacheUpdateEvent); public void Initialize(IGitClient client) { diff --git a/src/GitHub.Api/Helpers/Constants.cs b/src/GitHub.Api/Helpers/Constants.cs index 835951201..19cec7a19 100644 --- a/src/GitHub.Api/Helpers/Constants.cs +++ b/src/GitHub.Api/Helpers/Constants.cs @@ -1,3 +1,5 @@ +using System.Globalization; + namespace GitHub.Unity { static class Constants @@ -11,11 +13,12 @@ static class Constants public const string Iso8601Format = @"yyyy-MM-dd\THH\:mm\:ss.fffzzz"; public const string Iso8601FormatZ = @"yyyy-MM-dd\THH\:mm\:ss\Z"; public static readonly string[] Iso8601Formats = { - @"yyyy-MM-dd\THH\:mm\:ss\Z", + Iso8601FormatZ, @"yyyy-MM-dd\THH\:mm\:ss.fffffffzzz", - @"yyyy-MM-dd\THH\:mm\:ss.fffzzz", + Iso8601Format, @"yyyy-MM-dd\THH\:mm\:sszzz", }; + public const DateTimeStyles DateTimeStyle = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; public const string SkipVersionKey = "SkipVersion"; public const string GitInstallationState = "GitInstallationState"; diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index ea257214f..f664adfff 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -1255,7 +1255,7 @@ class PocoJsonSerializerStrategy : IJsonSerializerStrategy @"yyyy-MM-dd\THH\:mm\:sszzz", @"yyyy-MM-dd\THH\:mm\:ss.fffffffzzz", @"yyyy-MM-dd\THH\:mm\:ss.fffzzz", - @"yyyy-MM-dd\THH\:mm\:ssZ", + @"yyyy-MM-dd\THH\:mm\:ss\Z", @"yyyy-MM-dd\THH:mm:ss.fffffffzzz", @"yyyy-MM-dd\THH:mm:ss.fffzzz", @"yyyy-MM-dd\THH:mm:sszzz", diff --git a/src/GitHub.Api/Localization.Designer.cs b/src/GitHub.Api/Localization.Designer.cs index 80cd1e895..6eb8ecb1c 100644 --- a/src/GitHub.Api/Localization.Designer.cs +++ b/src/GitHub.Api/Localization.Designer.cs @@ -177,6 +177,24 @@ public static string FetchFailureDescription { } } + /// + /// Looks up a localized string similar to Assets/Release Lock (forced). + /// + public static string ForceUnlockFileAssetsMenuItem { + get { + return ResourceManager.GetString("ForceUnlockFileAssetsMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Release Lock (forced). + /// + public static string ForceUnlockFileMenuItem { + get { + return ResourceManager.GetString("ForceUnlockFileMenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to .... /// @@ -321,6 +339,33 @@ public static string LockedOut { } } + /// + /// Looks up a localized string similar to Assets/Request Lock. + /// + public static string LockFileAssetsMenuItem { + get { + return ResourceManager.GetString("LockFileAssetsMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request Lock. + /// + public static string LockFileMenuItem { + get { + return ResourceManager.GetString("LockFileMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Locks. + /// + public static string LocksTitle { + get { + return ResourceManager.GetString("LocksTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Login failed. /// @@ -699,6 +744,24 @@ public static string UnknownViewModeError { } } + /// + /// Looks up a localized string similar to Assets/Release Lock. + /// + public static string UnlockFileAssetsMenuItem { + get { + return ResourceManager.GetString("UnlockFileAssetsMenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Release Lock. + /// + public static string UnlockFileMenuItem { + get { + return ResourceManager.GetString("UnlockFileMenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Changes. /// diff --git a/src/GitHub.Api/Localization.resx b/src/GitHub.Api/Localization.resx index f2741e2d7..b4146faff 100644 --- a/src/GitHub.Api/Localization.resx +++ b/src/GitHub.Api/Localization.resx @@ -351,4 +351,25 @@ Request Lock + + Assets/Release Lock (forced) + + + Release Lock (forced) + + + Assets/Request Lock + + + Request Lock + + + Locks + + + Assets/Release Lock + + + Release Lock + \ No newline at end of file diff --git a/src/GitHub.Api/Primitives/TheVersion.cs b/src/GitHub.Api/Primitives/TheVersion.cs index b1137ff82..2afc062f0 100644 --- a/src/GitHub.Api/Primitives/TheVersion.cs +++ b/src/GitHub.Api/Primitives/TheVersion.cs @@ -4,6 +4,7 @@ namespace GitHub.Unity { + [Serializable] public struct TheVersion : IComparable { private const string versionRegex = @"^(?\d+)(\.?(?[^.]+))?(\.?(?[^.]+))?(\.?(?.+))?"; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs index 93e80467b..5d7d45861 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs @@ -30,7 +30,7 @@ public override void OnEnable() base.OnEnable(); AttachHandlers(); - User.CheckUserChangedEvent(lastCheckUserChangedEvent); + User.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, lastCheckUserChangedEvent); } public override void OnDisable() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 5cb840166..a19abf7ec 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -361,11 +361,9 @@ public bool OnSelectionChange() var activeAssetGuid = AssetDatabase.AssetPathToGUID(activeAssetPath); assets.TryGetValue(activeAssetGuid, out gitLockEntry); } - SelectedEntry = gitLockEntry; return true; } - return false; } } @@ -373,18 +371,19 @@ public bool OnSelectionChange() [Serializable] class LocksView : Subview { - [NonSerialized] private bool currentStatusEntriesHasUpdate; - [NonSerialized] private bool currentLocksHasUpdate; - + [SerializeField] private bool currentStatusEntriesHasUpdate; + [SerializeField] private bool currentLocksHasUpdate; + [SerializeField] private bool currentUserHasUpdate; [SerializeField] private LocksControl locksControl; - [SerializeField] private CacheUpdateEvent lastLocksChangedEvent; [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; - + [SerializeField] private CacheUpdateEvent lastUserChangedEvent; [SerializeField] private List lockedFiles = new List(); [SerializeField] private List gitStatusEntries = new List(); - [SerializeField] private string currentUsername; + [SerializeField] private bool isBusy; + [SerializeField] private GUIContent unlockFileMenuContent = new GUIContent(Localization.UnlockFileMenuItem); + [SerializeField] private GUIContent forceUnlockFileMenuContent = new GUIContent(Localization.ForceUnlockFileMenuItem); public override void OnEnable() { @@ -421,6 +420,9 @@ public override void OnDataUpdate() public override void OnGUI() { var rect = GUILayoutUtility.GetLastRect(); + + EditorGUI.BeginDisabledGroup(IsBusy); + if (locksControl != null) { var lockControlRect = new Rect(rect.x, rect.y, Position.width, Position.height - rect.height); @@ -430,38 +432,65 @@ public override void OnGUI() }, entry => { }, entry => { - string unlockFile; - GenericMenu.MenuFunction menuFunction; - + var menu = new GenericMenu(); if (entry.Owner.Name == currentUsername) { - unlockFile = "Unlock File"; - menuFunction = UnlockSelectedEntry; + menu.AddItem(unlockFileMenuContent, false, UnlockSelectedEntry); } - unlockFile = "Force Unlock File"; - menuFunction = ForceUnlockSelectedEntry; - - var menu = new GenericMenu(); - menu.AddItem(new GUIContent(unlockFile), false, menuFunction); + menu.AddItem(forceUnlockFileMenuContent, false, ForceUnlockSelectedEntry); menu.ShowAsContext(); }); if (requiresRepaint) Redraw(); } + + EditorGUI.EndDisabledGroup(); } private void UnlockSelectedEntry() { + isBusy = true; Repository .ReleaseLock(locksControl.SelectedEntry.GitLock.Path, false) + .FinallyInUI((success, ex) => + { + if (success) + { + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + } + else + { + EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, + ex.Message, + Localization.Ok); + } + + isBusy = false; + }) .Start(); } private void ForceUnlockSelectedEntry() { + isBusy = true; Repository .ReleaseLock(locksControl.SelectedEntry.GitLock.Path, true) + .FinallyInUI((success, ex) => + { + if (success) + { + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + } + else + { + EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, + ex.Message, + Localization.Ok); + } + + isBusy = false; + }) .Start(); } @@ -474,6 +503,19 @@ private void AttachHandlers(IRepository repository) repository.LocksChanged += RepositoryOnLocksChanged; repository.LocksChanged += RepositoryOnStatusEntriesChanged; + User.Changed += UserOnChanged; + } + + private void DetachHandlers(IRepository repository) + { + if (repository == null) + { + return; + } + + repository.LocksChanged -= RepositoryOnLocksChanged; + repository.LocksChanged -= RepositoryOnStatusEntriesChanged; + User.Changed -= UserOnChanged; } private void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) @@ -496,21 +538,21 @@ private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) } } - private void DetachHandlers(IRepository repository) + private void UserOnChanged(CacheUpdateEvent cacheUpdateEvent) { - if (repository == null) + if (!lastUserChangedEvent.Equals(cacheUpdateEvent)) { - return; + lastUserChangedEvent = cacheUpdateEvent; + currentUserHasUpdate = true; + Redraw(); } - - repository.LocksChanged -= RepositoryOnLocksChanged; - repository.LocksChanged -= RepositoryOnStatusEntriesChanged; } private void ValidateCachedData(IRepository repository) { repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastStatusEntriesChangedEvent); + User.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, lastUserChangedEvent); } private void MaybeUpdateData() @@ -520,13 +562,17 @@ private void MaybeUpdateData() return; } - if (currentLocksHasUpdate) + if (currentUserHasUpdate) { - lockedFiles = Repository.CurrentLocks; - //TODO: ONE_USER_LOGIN This assumes only ever one user can login var keychainConnection = Platform.Keychain.Connections.First(); currentUsername = keychainConnection.Username; + currentUserHasUpdate = false; + } + + if (currentLocksHasUpdate) + { + lockedFiles = Repository.CurrentLocks; } if (currentStatusEntriesHasUpdate) @@ -559,5 +605,10 @@ public override void OnSelectionChange() Redraw(); } } + + public override bool IsBusy + { + get { return isBusy || base.IsBusy; } + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 54f954295..15b6d3137 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -1,9 +1,6 @@ using GitHub.Logging; -using System; using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using UnityEditor; using UnityEngine; @@ -11,6 +8,9 @@ namespace GitHub.Unity { class ProjectWindowInterface : AssetPostprocessor { + private const string AssetsMenuRequestLock = "Assets/Request Lock"; + private const string AssetsMenuReleaseLock = "Assets/Release Lock"; + private const string AssetsMenuReleaseLockForced = "Assets/Release Lock (forced)"; private static readonly List entries = new List(); private static List locks = new List(); @@ -65,7 +65,7 @@ private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) } } - [MenuItem("Assets/Request Lock", true)] + [MenuItem(AssetsMenuRequestLock, true)] private static bool ContextMenu_CanLock() { if (isBusy) @@ -91,7 +91,7 @@ private static bool ContextMenu_CanLock() return !alreadyLocked && status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; } - [MenuItem("Assets/Request Lock")] + [MenuItem(AssetsMenuRequestLock)] private static void ContextMenu_Lock() { isBusy = true; @@ -122,7 +122,7 @@ private static void ContextMenu_Lock() .Start(); } - [MenuItem("Assets/Release lock", true, 1000)] + [MenuItem(AssetsMenuReleaseLock, true, 1000)] private static bool ContextMenu_CanUnlock() { if (isBusy) @@ -143,7 +143,7 @@ private static bool ContextMenu_CanUnlock() return isLocked; } - [MenuItem("Assets/Release lock", false, 1000)] + [MenuItem(AssetsMenuReleaseLock, false, 1000)] private static void ContextMenu_Unlock() { isBusy = true; @@ -174,6 +174,58 @@ private static void ContextMenu_Unlock() .Start(); } + [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] + private static bool ContextMenu_CanUnlockForce() + { + if (isBusy) + return false; + if (repository == null || !repository.CurrentRemote.HasValue) + return false; + + var selected = Selection.activeObject; + if (selected == null) + return false; + if (locks == null || locks.Count == 0) + return false; + + NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); + NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + + var isLocked = locks.Any(x => repositoryPath == x.Path); + return isLocked; + } + + [MenuItem(AssetsMenuReleaseLockForced, false, 1000)] + private static void ContextMenu_UnlockForce() + { + isBusy = true; + var selected = Selection.activeObject; + + NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); + NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + + repository + .ReleaseLock(repositoryPath, false) + .FinallyInUI((success, ex) => + { + if (success) + { + EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + } + else + { + EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, + ex.Message, + Localization.Ok); + } + + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); + }) + .Start(); + } + private static void OnLocksUpdate() { if (locks == null) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs index efc905d7f..57e77d2cc 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/UserSettingsView.cs @@ -73,7 +73,7 @@ public override void OnEnable() base.OnEnable(); AttachHandlers(); - User.CheckUserChangedEvent(lastCheckUserChangedEvent); + User.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, lastCheckUserChangedEvent); } public override void OnDisable() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 3f37514b3..1d4b11a68 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -12,7 +12,6 @@ class Window : BaseWindow private const string Title = "GitHub"; private const string Menu_Window_GitHub = "Window/GitHub"; private const string Menu_Window_GitHub_Command_Line = "Window/GitHub Command Line"; - private const string LocksTitle = "Locks"; [NonSerialized] private double notificationClearTime = -1; [NonSerialized] private double timeSinceLastRotation = -1f; @@ -456,9 +455,9 @@ private void DoToolbarGUI() if (HasRepository) { changeTab = TabButton(SubTab.Changes, Localization.ChangesTitle, changeTab); + changeTab = TabButton(SubTab.Locks, Localization.LocksTitle, changeTab); changeTab = TabButton(SubTab.History, Localization.HistoryTitle, changeTab); changeTab = TabButton(SubTab.Branches, Localization.BranchesTitle, changeTab); - changeTab = TabButton(SubTab.Locks, LocksTitle, changeTab); } else if (!HasRepository) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs index eccab9649..1e2fbc5ad 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs @@ -89,7 +89,6 @@ public static void CheckForUpdates() return; } - TaskManager.Instance.RunInUI(() => { NotifyOfNewUpdate(current, package); @@ -97,7 +96,7 @@ public static void CheckForUpdates() } catch(Exception ex) { - Debug.LogError(ex); + LogHelper.GetLogger().Error(ex); } } }; diff --git a/src/tests/UnitTests/Primitives/SerializationTests.cs b/src/tests/UnitTests/Primitives/SerializationTests.cs index f6e536fb1..32bcdf76b 100644 --- a/src/tests/UnitTests/Primitives/SerializationTests.cs +++ b/src/tests/UnitTests/Primitives/SerializationTests.cs @@ -14,9 +14,11 @@ class SerializationTests [Test] public void DateTimeSerializationRoundTrip() { - var dt1 = DateTimeOffset.ParseExact("2018-05-01T12:04:29.0000000-02:00", Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None); - var dt2 = DateTimeOffset.ParseExact("2018-05-01T12:04:29.000-02:00", Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None); - var dt3 = DateTimeOffset.ParseExact("2018-05-01T12:04:29-02:00", Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None); + var dt1 = DateTimeOffset.ParseExact("2018-05-01T12:04:29.0000000-02:00", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var dt2 = DateTimeOffset.ParseExact("2018-05-01T12:04:29.000-02:00", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var dt3 = DateTimeOffset.ParseExact("2018-05-01T12:04:29-02:00", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var stru = dt1.ToUniversalTime().ToString(Constants.Iso8601FormatZ); + var dt4 = DateTimeOffset.ParseExact(stru, Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); var str1 = dt1.ToJson(); var ret1 = str1.FromJson(); Assert.AreEqual(dt1, ret1); @@ -26,9 +28,13 @@ public void DateTimeSerializationRoundTrip() var str3 = dt3.ToJson(); var ret3 = str3.FromJson(); Assert.AreEqual(dt3, ret3); + var str4 = dt4.ToJson(); + var ret4 = str4.FromJson(); + Assert.AreEqual(dt4, ret4); Assert.AreEqual(dt1, dt2); Assert.AreEqual(dt2, dt3); + Assert.AreEqual(dt3, dt4); } class TestData From 565b080bff41aced23d77093265be575953b66e7 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 19:46:43 +0200 Subject: [PATCH 224/567] Add a nice background when views are empty, and add a busy state to the changes view --- .../Editor/GitHub.Unity/UI/BaseWindow.cs | 3 + .../Editor/GitHub.Unity/UI/ChangesView.cs | 89 ++++++++++++------- .../Assets/Editor/GitHub.Unity/UI/IView.cs | 7 +- .../Editor/GitHub.Unity/UI/InitProjectView.cs | 8 +- .../Editor/GitHub.Unity/UI/LocksView.cs | 15 +++- .../Assets/Editor/GitHub.Unity/UI/Subview.cs | 5 ++ .../Assets/Editor/GitHub.Unity/UI/Window.cs | 18 +++- 7 files changed, 100 insertions(+), 45 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index e401ec4d0..26408ae0f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -65,6 +65,9 @@ public virtual void OnDataUpdate() public virtual void OnRepositoryChanged(IRepository oldRepository) {} + public virtual void DoEmptyGUI() + {} + // OnGUI calls this everytime, so override it to render as you would OnGUI public virtual void OnUI() {} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index d878a152e..6d88e6a96 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -39,6 +39,7 @@ class ChangesView : Subview [SerializeField] private CacheUpdateEvent lastCurrentBranchChangedEvent; [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; [SerializeField] private CacheUpdateEvent lastLocksChangedEvent; + [SerializeField] private bool isBusy; public override void OnEnable() { @@ -76,43 +77,23 @@ public override void OnDataUpdate() public override void OnGUI() { - GUILayout.BeginHorizontal(); + DoButtonBarGUI(); + if (gitStatusEntries.Count == 0) { - EditorGUI.BeginDisabledGroup(gitStatusEntries == null || !gitStatusEntries.Any()); - { - if (GUILayout.Button(SelectAllButton, EditorStyles.miniButtonLeft)) - { - SelectAll(); - } - - if (GUILayout.Button(SelectNoneButton, EditorStyles.miniButtonRight)) - { - SelectNone(); - } - } - EditorGUI.EndDisabledGroup(); - - GUILayout.FlexibleSpace(); - - GUILayout.Label(changedFilesText, EditorStyles.miniLabel); + GUILayout.BeginVertical(Styles.CommitFileAreaStyle); + DoEmptyGUI(); + GUILayout.EndVertical(); } - GUILayout.EndHorizontal(); - - var rect = GUILayoutUtility.GetLastRect(); - GUILayout.BeginHorizontal(); - GUILayout.BeginVertical(Styles.CommitFileAreaStyle); + else { - treeScroll = GUILayout.BeginScrollView(treeScroll); - { - OnTreeGUI(new Rect(0f, 0f, Position.width, Position.height - rect.height + Styles.CommitAreaPadding)); - } - GUILayout.EndScrollView(); + EditorGUI.BeginDisabledGroup(isBusy); + DoChangesTreeGUI(); + EditorGUI.EndDisabledGroup(); } - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - + EditorGUI.BeginDisabledGroup(isBusy); // Do the commit details area OnCommitDetailsAreaGUI(); + EditorGUI.EndDisabledGroup(); } public override void OnSelectionChange() @@ -135,6 +116,47 @@ public override void OnFocusChanged() } } + private void DoChangesTreeGUI() + { + var rect = GUILayoutUtility.GetLastRect(); + GUILayout.BeginHorizontal(); + GUILayout.BeginVertical(Styles.CommitFileAreaStyle); + { + treeScroll = GUILayout.BeginScrollView(treeScroll); + { + OnTreeGUI(new Rect(0f, 0f, Position.width, Position.height - rect.height + Styles.CommitAreaPadding)); + } + GUILayout.EndScrollView(); + } + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + } + + private void DoButtonBarGUI() + { + GUILayout.BeginHorizontal(); + { + EditorGUI.BeginDisabledGroup(gitStatusEntries == null || gitStatusEntries.Count == 0); + { + if (GUILayout.Button(SelectAllButton, EditorStyles.miniButtonLeft)) + { + SelectAll(); + } + + if (GUILayout.Button(SelectNoneButton, EditorStyles.miniButtonRight)) + { + SelectNone(); + } + } + EditorGUI.EndDisabledGroup(); + + GUILayout.FlexibleSpace(); + + GUILayout.Label(changedFilesText, EditorStyles.miniLabel); + } + GUILayout.EndHorizontal(); + } + private void OnTreeGUI(Rect rect) { if (treeChanges != null) @@ -376,5 +398,10 @@ private void Commit() } }).Start(); } + + public override bool IsBusy + { + get { return isBusy || base.IsBusy; } + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs index dae2d62c6..261cfcedf 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs @@ -3,7 +3,7 @@ namespace GitHub.Unity { - interface IView + interface IView : ICanRenderEmpty { void OnEnable(); void OnDisable(); @@ -20,4 +20,9 @@ interface IView bool IsBusy { get; } bool HasFocus { get; } } + + interface ICanRenderEmpty + { + void DoEmptyGUI(); + } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs index 5d7d45861..2587b4a5d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs @@ -46,13 +46,7 @@ public override void OnGUI() GUILayout.FlexibleSpace(); GUILayout.Space(-140); - GUILayout.BeginHorizontal(); - { - GUILayout.FlexibleSpace(); - GUILayout.Label(Styles.EmptyStateInit, GUILayout.MaxWidth(265), GUILayout.MaxHeight(136)); - GUILayout.FlexibleSpace(); - } - GUILayout.EndHorizontal(); + DoEmptyGUI(); GUILayout.Label(NoRepoTitle, Styles.BoldCenteredLabel); GUILayout.Space(4); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index a19abf7ec..52f444a36 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -57,6 +57,7 @@ class LocksControl [SerializeField] public GitLockEntryDictionary assets = new GitLockEntryDictionary(); [SerializeField] public GitStatusDictionary gitStatusDictionary = new GitStatusDictionary(); [SerializeField] private GitLockEntry selectedEntry; + public bool IsEmpty { get { return gitLockEntries.Count == 0; } } public GitLockEntry SelectedEntry { @@ -423,15 +424,17 @@ public override void OnGUI() EditorGUI.BeginDisabledGroup(IsBusy); - if (locksControl != null) + if (locksControl != null && !locksControl.IsEmpty) { var lockControlRect = new Rect(rect.x, rect.y, Position.width, Position.height - rect.height); var requiresRepaint = locksControl.Render(lockControlRect, - entry => { + entry => + { }, - entry => { }, - entry => { + entry => { }, + entry => + { var menu = new GenericMenu(); if (entry.Owner.Name == currentUsername) { @@ -444,6 +447,10 @@ public override void OnGUI() if (requiresRepaint) Redraw(); } + else + { + DoEmptyGUI(); + } EditorGUI.EndDisabledGroup(); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index 7cd4d7381..db794abdb 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -45,6 +45,11 @@ public virtual void Finish(bool result) Parent.Finish(result); } + public void DoEmptyGUI() + { + Parent.DoEmptyGUI(); + } + protected IView Parent { get; private set; } public IApplicationManager Manager { get { return Parent.Manager; } } public IRepository Repository { get { return Parent.Repository; } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 1d4b11a68..b9d92f168 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -6,7 +6,7 @@ namespace GitHub.Unity { [Serializable] - class Window : BaseWindow + class Window : BaseWindow, ICanRenderEmpty { private const float DefaultNotificationTimeout = 4f; private const string Title = "GitHub"; @@ -185,7 +185,6 @@ public override void Refresh() Repaint(); } - public override void OnUI() { base.OnUI(); @@ -547,6 +546,21 @@ private void DoActionbarGUI() EditorGUILayout.EndHorizontal(); } + public override void DoEmptyGUI() + { + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + GUILayout.Label(Styles.EmptyStateInit, GUILayout.MaxWidth(265), GUILayout.MaxHeight(136)); + GUILayout.FlexibleSpace(); + } + GUILayout.EndHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + } + private void Pull() { if (hasItemsToCommit) From 50ba5c2bff10552ff0587c6d3a3c86aa320629da Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 11:09:19 -0700 Subject: [PATCH 225/567] Update readme.md --- docs/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/readme.md b/docs/readme.md index a95adb7e9..095f4e709 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -32,3 +32,5 @@ Details about how the team is organizing and shipping GitHub for Unity: These documents contain more details about the internals of GitHub for Unity and how things work: +- **[Installing and Updating the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md)** +- **[Getting Started with the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md)** From 878cdf1d045aa1c421aee7d30f1249a4e61f65a1 Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 11:10:25 -0700 Subject: [PATCH 226/567] Update readme.md --- docs/readme.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/readme.md b/docs/readme.md index 095f4e709..ae4be69a9 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -28,9 +28,8 @@ Details about how the team is organizing and shipping GitHub for Unity: - **[Roadmap](process/roadmap.md)** - how we plan for the future - **[Release](process/release-process.md)** - how we review contributions -## Technical +## Using -These documents contain more details about the internals of GitHub for Unity -and how things work: +These documents contain more details on how to use the GitHub for Unity plugin: - **[Installing and Updating the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md)** - **[Getting Started with the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md)** From eb5c32ed038858fe518218fb30197b6b504ae945 Mon Sep 17 00:00:00 2001 From: Sarah Guthals Date: Fri, 18 May 2018 11:27:31 -0700 Subject: [PATCH 227/567] Update getting-started.md --- docs/using/getting-started.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/using/getting-started.md b/docs/using/getting-started.md index 925c2b83e..11f2b008f 100644 --- a/docs/using/getting-started.md +++ b/docs/using/getting-started.md @@ -1,12 +1,26 @@ # Getting Started with GitHub for Unity There are often three states that you might be in when you are setting up your GitHub for Unity package with your Unity project: -[Setting up a brand new repository](#setting-up-a-new-repository): You do not currently have your Unity project in a repository and you want to publish it to one. -[Connecting to an existing respository](#connecting-to-an-existing-repository): Your Unity project is already in a repository, but you do not have the GitHub for Unity package installed within it yet. -[Opening a Unity project that already has the GitHub for Unity package](#connecting-to-an-existing-repository-that-already-has-the-github-for-unity-package): Your Unity project is already in a repository and it already has the GitHub for Unity package and you are trying to setup a new machine. +- [Setting up a brand new repository](#setting-up-a-new-repository): You do not currently have your Unity project in a repository and you want to publish it to one. +- [Connecting to an existing respository](#connecting-to-an-existing-repository): Your Unity project is already in a repository, but you do not have the GitHub for Unity package installed within it yet. +- [Opening a Unity project that already has the GitHub for Unity package](#connecting-to-an-existing-repository-that-already-has-the-github-for-unity-package): Your Unity project is already in a repository and it already has the GitHub for Unity package and you are trying to setup a new machine. # Setting up a New Repository -When you +If you have a Unity project (new or existing) that is not yet connected to any remote repository, you can use the GitHub for Unity package to quickly intialize the repository and publish to a repository. -# Connecting to an Existing Respository +1. If your Unity project doesn't yet have the GitHub plugin installed, follow [these](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md) instructions for installing it. +2. Click on the `Initialize a git repository for this project` button +screen shot 2018-05-18 at 9 39 13 am +And you should see the GitHub spinner: +screen shot 2018-05-18 at 9 39 23 am +3. The GitHub tab should reload with the following buttons: + - Publish: Publish this repository to GitHub (Creating a new GitHub repository) + - Account: Login to your GitHub account | Logout from your GitHub account or visit your profile on GitHub.com + - Changes: Local changes that are not yet committed, with a dialog to commit those changes + - History: A history of commits with title, time stamp, and commit author + - Branches: A list of local and remote branches with the ability to create new branches, switch branches, or checkout remote branches + - Settings: your git configuration (pulled from your local git credentials if they have been previously set), your repository configuration (you can manually put the URL to any remote repository here instead of using the Publish button to publish to GitHub), a list of locked files, your git installation details, and general settings to help us better help you if you get stuck +4. You can + +# Connecting to an Existing Repository # Connecting to an Existing Repository that already has the GitHub for Unity package From 7ec4836372e118ba496c77400f68c877e76d8b82 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 18 May 2018 14:28:25 -0400 Subject: [PATCH 228/567] Removing notice about alpha quality software --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8182b7f65..e83795b86 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ You can reach the team right here by opening a [new issue](https://github.com/gi ## Notices -This software is currently alpha quality. Please refer to the [list of known issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aissue+is%3Aopen+label%3Abug), and make sure you have backups of your work before trying it out. +Please refer to the [list of known issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aissue+is%3Aopen+label%3Abug), and make sure you have backups of your work before trying it out. From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugins/GitHub`. If you have version 0.18 or lower, you need to delete the `Assets/Editor/GitHub` folder before you install newer versions. You should exit Unity and delete the folder from Explorer/Finder, as Unity will not unload native libraries while it's running. Also, remember to update your `.gitignore` file. From c14943c4ab9cd1c37664ca2e999bdf28fa59ff38 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 20:39:28 +0200 Subject: [PATCH 229/567] Add progress reporting to the locks view and cleanup the code --- .../Assets/Editor/GitHub.Unity/UI/BaseWindow.cs | 10 +++++++--- .../Assets/Editor/GitHub.Unity/UI/BranchesView.cs | 3 +-- .../Assets/Editor/GitHub.Unity/UI/ChangesView.cs | 5 +++-- .../Assets/Editor/GitHub.Unity/UI/GitPathView.cs | 4 ++-- .../Assets/Editor/GitHub.Unity/UI/HistoryView.cs | 3 +-- .../Assets/Editor/GitHub.Unity/UI/IView.cs | 10 ++++++++-- .../Assets/Editor/GitHub.Unity/UI/LocksView.cs | 11 +++++------ .../Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/SettingsView.cs | 3 +-- .../Assets/Editor/GitHub.Unity/UI/Subview.cs | 11 ++++++++++- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 12 +++--------- 11 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index 091f2e8d0..9d2836ad9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -65,9 +65,6 @@ public virtual void OnDataUpdate() public virtual void OnRepositoryChanged(IRepository oldRepository) {} - public virtual void DoEmptyGUI() - {} - // OnGUI calls this everytime, so override it to render as you would OnGUI public virtual void OnUI() {} @@ -120,6 +117,13 @@ public virtual void DoneRefreshing() IsRefreshing = false; } + public virtual void DoEmptyGUI() + {} + public virtual void DoProgressGUI() + {} + public virtual void UpdateProgress(IProgress progress) + {} + public Rect Position { get { return position; } } public IApplicationManager Manager { get; private set; } public abstract bool IsBusy { get; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index f1f64bb2b..9ebb344b9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -221,8 +221,7 @@ private void Render() Redraw(); } } - if (ProgressRenderer != null) - ProgressRenderer.DoProgressGUI(); + DoProgressGUI(); } private void BuildTree() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 99b88705d..021e8eb2f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -92,8 +92,7 @@ public override void OnGUI() } EditorGUI.BeginDisabledGroup(isBusy); - if (ProgressRenderer != null) - ProgressRenderer.DoProgressGUI(); + DoProgressGUI(); // Do the commit details area DoCommitGUI(); @@ -381,6 +380,7 @@ private void SelectNone() private void Commit() { + isBusy = true; var files = treeChanges.GetCheckedFiles().ToList(); ITask addTask; @@ -403,6 +403,7 @@ private void Commit() commitMessage = ""; commitBody = ""; } + isBusy = false; }).Start(); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs index d91a9e6d1..7ef580d6a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs @@ -268,7 +268,7 @@ private void ValidateAndSetGitInstallPath() newState.GitExecutablePath = gitPath.ToNPath(); newState.GitLfsExecutablePath = gitLfsPath.ToNPath(); var installer = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); - installer.Progress.OnProgress += ProgressRenderer.UpdateProgress; + installer.Progress.OnProgress += UpdateProgress; new FuncTask(TaskManager.Token, () => { @@ -285,7 +285,7 @@ private void ValidateAndSetGitInstallPath() }) .FinallyInUI((success, ex, state) => { - installer.Progress.OnProgress -= ProgressRenderer.UpdateProgress; + installer.Progress.OnProgress -= UpdateProgress; if (!success) { Logger.Error(ex, ErrorValidatingGitPath); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index e2f2cdc7e..69b42e83e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -391,8 +391,7 @@ public override void OnGUI() Redraw(); } - if (ProgressRenderer != null) - ProgressRenderer.DoProgressGUI(); + DoProgressGUI(); if (!selectedEntry.Equals(GitLogEntry.Default)) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs index c5f0a5aab..f20b1e6e2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs @@ -3,7 +3,7 @@ namespace GitHub.Unity { - interface IView : ICanRenderEmpty + interface IView : IUIEmpty, IUIProgress { void OnEnable(); void OnDisable(); @@ -23,8 +23,14 @@ interface IView : ICanRenderEmpty bool HasFocus { get; } } - interface ICanRenderEmpty + interface IUIEmpty { void DoEmptyGUI(); } + + interface IUIProgress + { + void DoProgressGUI(); + void UpdateProgress(IProgress progress); + } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 52f444a36..b094886c9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -429,10 +429,8 @@ public override void OnGUI() var lockControlRect = new Rect(rect.x, rect.y, Position.width, Position.height - rect.height); var requiresRepaint = locksControl.Render(lockControlRect, - entry => - { - }, - entry => { }, + entry => {}, + entry => {}, entry => { var menu = new GenericMenu(); @@ -453,6 +451,7 @@ public override void OnGUI() } EditorGUI.EndDisabledGroup(); + DoProgressGUI(); } private void UnlockSelectedEntry() @@ -464,7 +463,7 @@ private void UnlockSelectedEntry() { if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } else { @@ -487,7 +486,7 @@ private void ForceUnlockSelectedEntry() { if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } else { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 0836f2639..605cf1f75 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -210,7 +210,7 @@ private static void ContextMenu_UnlockForce() { if (success) { - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock); + EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } else { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs index 9cde0857e..fe5146dc0 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs @@ -107,8 +107,7 @@ public override void OnGUI() GUILayout.EndScrollView(); - if (ProgressRenderer != null) - ProgressRenderer.DoProgressGUI(); + DoProgressGUI(); } private void AttachHandlers(IRepository repository) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index 80fe2efaf..dc09a6e6c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -56,6 +56,16 @@ public void DoEmptyGUI() Parent.DoEmptyGUI(); } + public void DoProgressGUI() + { + Parent.DoProgressGUI(); + } + + public void UpdateProgress(IProgress progress) + { + Parent.UpdateProgress(progress); + } + protected void Refresh(CacheType type) { if (Repository == null) @@ -87,7 +97,6 @@ public void DoneRefreshing() } protected IView Parent { get; private set; } - protected IUIProgress ProgressRenderer { get { return Parent is Subview ? ((Subview)Parent).ProgressRenderer : Parent as IUIProgress; } } public IApplicationManager Manager { get { return Parent.Manager; } } public IRepository Repository { get { return Parent.Repository; } } public bool HasRepository { get { return Parent.HasRepository; } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 5da18330f..7de0fc25e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -5,14 +5,8 @@ namespace GitHub.Unity { - interface IUIProgress - { - void DoProgressGUI(); - void UpdateProgress(IProgress progress); - } - [Serializable] - class Window : BaseWindow, IUIProgress, ICanRenderEmpty + class Window : BaseWindow { private const float DefaultNotificationTimeout = 2f; private const string Title = "GitHub"; @@ -407,7 +401,7 @@ private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) } private static object lck = new object(); - public void UpdateProgress(IProgress progress) + public override void UpdateProgress(IProgress progress) { lock (lck) { @@ -484,7 +478,7 @@ public override void Update() } } - public void DoProgressGUI() + public override void DoProgressGUI() { Rect rect1 = GUILayoutUtility.GetRect(position.width, 20); if (Event.current.GetTypeForControl(GUIUtility.GetControlID("ghu_ProgressBar".GetHashCode(), FocusType.Keyboard, position)) == EventType.Repaint) From 764df014befcea1fce773de425b9f5fc04a576c3 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 21:12:11 +0200 Subject: [PATCH 230/567] Don't wrap summaries in the history view, for now --- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index 098ca7f4c..b85f92162 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -90,7 +90,9 @@ class Styles hyperlinkStyle, selectedArea, selectedLabel, - progressAreaBackStyle; + progressAreaBackStyle, + labelNoWrap, + invisibleLabel; private static Texture2D branchIcon, activeBranchIcon, @@ -243,6 +245,46 @@ public static GUIStyle Label } } + public static GUIStyle LabelNoWrap + { + get + { + if (labelNoWrap == null) + { + labelNoWrap = new GUIStyle(GUI.skin.label); + labelNoWrap.name = "LabelNoWrap"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + labelNoWrap.onNormal.background = hierarchyStyle.onNormal.background; + labelNoWrap.onNormal.textColor = hierarchyStyle.onNormal.textColor; + labelNoWrap.onFocused.background = hierarchyStyle.onFocused.background; + labelNoWrap.onFocused.textColor = hierarchyStyle.onFocused.textColor; + labelNoWrap.wordWrap = false; + } + return labelNoWrap; + } + } + + public static GUIStyle InvisibleLabel + { + get + { + if (invisibleLabel == null) + { + invisibleLabel = new GUIStyle(GUI.skin.label); + invisibleLabel.name = "InvisibleLabel"; + + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + invisibleLabel.onNormal.background = hierarchyStyle.onNormal.background; + invisibleLabel.onNormal.textColor = new Color(255, 0, 0, 0); + invisibleLabel.onFocused.background = hierarchyStyle.onFocused.background; + invisibleLabel.onFocused.textColor = new Color(255, 0, 0, 0); + invisibleLabel.wordWrap = true; + } + return invisibleLabel; + } + } + public static GUIStyle SelectedLabel { get @@ -475,7 +517,7 @@ public static GUIStyle HistoryEntrySummaryStyle { if (historyEntrySummaryStyle == null) { - historyEntrySummaryStyle = new GUIStyle(Label); + historyEntrySummaryStyle = new GUIStyle(LabelNoWrap); historyEntrySummaryStyle.name = "HistoryEntrySummaryStyle"; historyEntrySummaryStyle.contentOffset = new Vector2(BaseSpacing * 2, 0); From 833feaa7a84502ea1449d110c30ef5bd00fec697 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 21:37:04 +0200 Subject: [PATCH 231/567] Friendlier lock error message when permissions fail --- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 43 ++++++++++++++++++- .../Editor/GitHub.Unity/UI/LocksView.cs | 12 ++++-- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 15 +++++-- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index b85f92162..8896c1068 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -92,7 +92,9 @@ class Styles selectedLabel, progressAreaBackStyle, labelNoWrap, - invisibleLabel; + invisibleLabel, + locksViewLockedByStyle, + locksViewLockedBySelectedStyle; private static Texture2D branchIcon, activeBranchIcon, @@ -633,6 +635,45 @@ public static GUIStyle HistoryDetailsMetaInfoStyle } } + public static GUIStyle LocksViewLockedByStyle + { + get + { + if (locksViewLockedByStyle == null) + { + locksViewLockedByStyle = new GUIStyle(EditorStyles.miniLabel); + locksViewLockedByStyle.name = "LocksViewLockedByStyle"; + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + locksViewLockedByStyle.onNormal.background = hierarchyStyle.onNormal.background; + locksViewLockedByStyle.onNormal.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedByStyle.onFocused.background = hierarchyStyle.onFocused.background; + locksViewLockedByStyle.onFocused.textColor = hierarchyStyle.onFocused.textColor; + } + return locksViewLockedByStyle; + } + } + + public static GUIStyle LocksViewLockedBySelectedStyle + { + get + { + if (locksViewLockedBySelectedStyle == null) + { + locksViewLockedBySelectedStyle = new GUIStyle(EditorStyles.miniLabel); + locksViewLockedBySelectedStyle.name = "LocksViewLockedBySelectedStyle"; + var hierarchyStyle = GUI.skin.FindStyle("PR Label"); + locksViewLockedBySelectedStyle.onNormal.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedBySelectedStyle.onNormal.background = hierarchyStyle.onFocused.background; + locksViewLockedBySelectedStyle.onNormal.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedBySelectedStyle.onFocused.background = hierarchyStyle.onFocused.background; + locksViewLockedBySelectedStyle.onFocused.textColor = hierarchyStyle.onNormal.textColor; + locksViewLockedBySelectedStyle.normal.background = hierarchyStyle.onFocused.background; + locksViewLockedBySelectedStyle.normal.textColor = hierarchyStyle.onNormal.textColor; + } + return locksViewLockedBySelectedStyle; + } + } + public static GUIStyle CommitFileAreaStyle { get diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index b094886c9..c03c755d5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -150,7 +150,7 @@ private Rect RenderEntry(Rect entryRect, GitLockEntry entry) } GUILayout.BeginVertical(); GUILayout.Label(entry.GitLock.Path, isSelected ? Styles.SelectedLabel : Styles.Label); - GUILayout.Label(string.Format("Locked {0} by {1}", entry.LockedAt, entry.GitLock.Owner.Name), isSelected ? Styles.SelectedLabel : Styles.Label); + GUILayout.Label(string.Format("Locked {0} by {1}", entry.LockedAt, entry.GitLock.Owner.Name), isSelected ? Styles.LocksViewLockedBySelectedStyle : Styles.LocksViewLockedByStyle); GUILayout.EndVertical(); GUILayout.EndHorizontal(); var itemRect = GUILayoutUtility.GetLastRect(); @@ -467,8 +467,11 @@ private void UnlockSelectedEntry() } else { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, - ex.Message, + error, Localization.Ok); } @@ -490,8 +493,11 @@ private void ForceUnlockSelectedEntry() } else { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, - ex.Message, + error, Localization.Ok); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 605cf1f75..436390e3b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -110,8 +110,11 @@ private static void ContextMenu_Lock() } else { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, - ex.Message, + error, Localization.Ok); } @@ -162,8 +165,11 @@ private static void ContextMenu_Unlock() } else { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, - ex.Message, + error, Localization.Ok); } @@ -214,8 +220,11 @@ private static void ContextMenu_UnlockForce() } else { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, - ex.Message, + error, Localization.Ok); } From 68eba95b01316440be290c29b325823ca5fd93bd Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 18 May 2018 15:38:22 -0400 Subject: [PATCH 232/567] Including the readme.pdf in the project --- .../Assets/Plugins/GitHub/Editor/README.pdf | Bin 0 -> 283204 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/README.pdf diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/README.pdf b/unity/PackageProject/Assets/Plugins/GitHub/Editor/README.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2c1dc877b4d2a6d0ceed4865cffb0202d87a902a GIT binary patch literal 283204 zcmeFZ2V7Izwl^9?L_k4$Cnz9YK)RHGN)s_4AWevX^d?Or1VWMC1QZKMP((yZq*nyDZJ%A=odD-Z=!g{`{HKib@)as!AG4no6ftfD?3ence)a|Lo@Uue-Z-!%bA- zoO6h?Z-580g1NJYTd=4yaKhzZU7+&23k={PMwTH!Ps|FI`l2C0p+EmI6IDE=c$!(k z)mc>Olp;XB{-56UO)N#-oqdCUImbEJM^xcbfWO;+(4+e2+dBZ=hlIEV`HL!?cf0B3 z>UP=atm9vZ*8Cj}won&uH`frL<4~87U)V751ITAqu<^Pc;wh@8sKTsZ?B?a+86v8w ztj4Tx)+;2~+%4!_fZq+Ed;bto4S>V|-+&;?8_ur4ApEsYW`&Dx{vNGyb{PemjW$F#U@ISLx|afDW+J(X-R-cZ0wn5FGgM*`o24ze?F99Pmm#A)Hocp{MNbWGNh!(wNvI=C-? zz>6!p1jRBj^YHTV3rL)ll#-TFQB_md&^&YQyn&&SvB?EXE9)yZws!We*WKJbJiWYw zLqczc!Ec4f-HuO4OiE74&bfOp_x^*2kDeAiD}G+`qV#2TO>JF$Lt|5OXIFPmZ(skL zw;xAF$HpfCt_2lzSw$lxI3 zPrm35gaM77{UF0}r9&M07L3k;oF`7l9OgQk^`xqUNlf`Np4%np12d1f3QmIXleJ$s z`(up7{!5(wm9hWe3ky0zPX`E(o*e`M?VU)GDm;)vV4I2{HZ@T1QCI@xEFw$n$HF2L zgnQI?gU*WYwl-MYz7J756F3A-*5!{HJ%jEsRbclnP$y09gAQLRdmfobJ~=@lBh{5^)`~r%~(n6|oNM9T!Lpil0Mv2^_^ zbRR!hm_;0Yop<$(iiv;HbuYVi1M}0i1rPc(%VCU&EBJr1FkvT9@Zqr@Y{Clbj#(cVT zWj|7Uih5pr#pa^QyuN$9YpQNE!Q}|w_-YJ!41zs<<*zFPcNytr!mS{T3j5G+(jU35 zvdb@CzwUBkYDi?Pq|#?xm;d8Z+bG&kg)AG`?2!5}SL&%q`FlP3RnG%6+=K9lMc&&8 z$7i#1D^Ak*r4rZYmE@LGvkUxB>zr<7-AmH%_?$I7p-I-Ijp?#di?ua{_d$uF2TiW$ zdiKOm4;O!B*E9^~Xc4}h7WdWY0%+S?_VKu`BHXlVRwC12RNE3aCG2wffbCUp^PZs3 z+ugn$84-c@drPlfap|WV)-Mo8pw)4Qexa)94anbJ^hh?NWGNS&f6DujR=*-|nrPEU zKh?<7->hrW-5YBXR@ED01UFg?S-)6j#Tv5uT#9CvditlfPGg|G(-osWp~%og<6sT= z*(^CUcZOX2=Lu)NBCP|f$NVodia##NxJ}^x5w=DTxkr|QUk#_C_d%wTg=`RjuRx!D z(Dr3U8Yf{NL|8qy4?4Iw5xoz}&W7Monfstjo6+7K;Q<8uK8PV1(7`fC+8Z8wj~L3a zE8qGGq)xJ2D_3_=)B7B;6s>b$lgNdFKInt=te<9L#J_nqRZ2Y;7(@jI$!4V7oyLgL;F(i?}L04 zu)#DYCH45{q$%NU8tcT;R@9Kn%ibf3S2y@|%s^33kX?;+`=C8(`h5@+X&*%T0mg60 zBWWpm`=HJ!2u%le4xvOUdQQ25Kb41GQD+HsebHH6RC)OG0VCX_UCG5|3YYT2{T}F) zmK_y^amTR1wJsABVtub+2$(c&`|tJ1xR^5u31i0_WW{$*mE53S)i{S%8O8M=1jj#K08t;tZ=o;`CvLz zhO|UB)3NtrSKb`qP8ODJ#W(apIac;TwH`eVU>+Y7^IDV;{6gQQ=8sMjR2POC3-kh4EnaK?b5o90a@%5-Fky_W-K) z!5B<}_U(iEUIt8h5SIy%J}^ltb6pmnYCTdb3uC45)7oi$Ep;i@uUp8Gc|+=0{%6Q8qXe!rLEeBqUAfm;4{ zA=_gJZrXkyMCFRv2i>M1DdCyFA?YOyZ!u^9;w= z7VAbU2&amD=&PA{jISTk6u)|>-9zloVd5rZ)MylA+xRw=*@^VM6O1o_ed^Hi^ee+Y zFO}84@Fl*IASt>W->dV2z7O+X5P>d@n%Y2K(Ea%bwCnSmQF`&aS-E$Ay={5B=;xn8 zw%q@jyJu1we(LmCHzI_Z+iFDChFfdv>tX^H98XAJmR*}<(X60cB7jyMbzC*My^VI?j>kSzQ<;WBJpeHkt zWp6ZLxQq`DZQ*+!r$jjuenG&Rt0w9Q~L8L(|JqxlaeUL*7?0DTv%n{u>t zyPr%O^F@9^6GOZ*7+r%4n!xcOxa3;Tyu1q-i#-29J5$mc22t8HoT4R*{TQ5U8G?;t zTzxwBL9UyhUa(56#!T{=(^f*$xZZD=2~mA#3j zr*-VH(Xxtf8LcUh72uM6;8z*w1?Vx)n+2j{=!!Z|=R6DjKC)cN`khezI;$Hd5JP7maa2+LqP z(33T1NnpHwEB&*X)d>MD`Hv4Pzoc&j-WVMEE+d1#y7*;#(38G1ijN{bj`0SQ#QPFk z)2A;%Z)dn;6Nmh;oXQ z_Q<9n^SOhbZ(W|s4VH5%dcsc<5cQJ&Y?=pciaasZOAm3d$z@Yga1lJU2dkQp_EKKe zRQ7YCWm@_3iw}7e6UFZszUQ7#LE}+Gi;*@Vit+tk`;sJz#UhMb^*+&8AfX{pFi+NH zD(yxe$B9!=@%fw~F-cJo<#|kO8y~3)@Kh3{QG~^_DWCdggMvgA9-FeI$%pp+3J>t8 zY(wj5-WytB^_rS5F)S6CYh?0e+6Mt}Oodp@@b>1Bsc*zR$7W=ao%hL4)#ZU1oJdJe zzIJ*(^Fs-FWF2oLJ0t*|_UPqmn=;Oo)1{={*8LMlh z=$j-q@)=JQs65owHT31TGkr;wQIlog1j-j8)-RYVwtQlsv*O^k0u`bz8#8vJnA#Xg z?-tQ(j&3~Z_?AwAgo*|Ez5FQkyl&6t7ANYH{OO#}w^CMDEd%m~*$e7c=fJh5=u}9( zM>`5c(c7x56?*pun~IP{$BV zPrY;M-tV1CM>0BFNhD*1`(B*O)B|ykQhca)DO!Yy&NHm2GAEyUY)Sk>iI1w^ zv_?cKTxDLyop`VRPHjy7j=rc!;W4_8D`0vGFODpK3ES1F%U4}L{=^~jET2lFSO!gX zrcT1V(6}i0nxfc}Zr#=$NN&n6b>{Mfbk5j3&ZVqyJ^PGH54gRfH81HQbGHcrb6 zyZ83RRQ@%(ls!P29W0pJ6TCaTvl@hww90^w8gC{P#Ij|t#b<65rL$W-o>Tgq&k8R` z#?DpW%)|5c+ShHwEGTfEj46Zae85kC?>YI$Scmsm1uY45&S53;Ah?=uScs zphU5cb!;4-;f776@VA2tWV`WzYbNO80u%AMBs6tt~b^$o}5NS|Jpe4ZSK8KSbu<+yUdE6+DCQE`QjD2vYR%eQ}|;uIGO1 z3U@E!u#Gu>(s)6h(}XJ^r-}Pjn2@XLG~ygJmm*7GB!=YSJX1E$f7+HKsCz3|kK?DQ08u3I0iE@hp7yGb+e#ul6UbKZu^&fGJ~4(B1gZDi zZB6GNfP=2t6po$=&GveP;Q5d%9U#zK+7!d~6$yGNELSs^Fp zXt_|>sfmwsRpK<8wF(>rS&v+~K1#O^_Jz!%+fgJbo?^hfcC?RYE_@e*I|(LH9)Di5 z{E~r6kHS-5?M<{J@mJ8KlQ?Rj_PJt;0iO6kRmbBT>%^7AvktJ&*MzC{8IfP>*7B2uOnw-oW1^+FV2O0uy?qea`@Y zXV&y_R^{6K@o)Jq(GV--{B3MPsRD&l8MiwwaxSvkan?P|6$viB1*LFs+9r0u@Ru{E z@;U|Hr}{0jV1yheN6m5*hdMG7umK&=gl3zB+6TET z@~0Wnt;4U=<&Yi`0CJH2=zs*?*bv>G%;yCrXbsy9eJHsTG zEfV&F*;oT@L-HV>AbH?y)*T7zi>!~4l>-X_DdSkT$_l?=6*E`=`eV0_Jz#gWx%}>3 zK-im=+CV)pkfL@pKn^0w+Fy6`&PA@w#uL9tbTTIx^w={q4s__Y{pB zx(6}0g+hnyLA!!9CieM?FdUM??L(X#rkE@WL;J8ZDR2Y-Q0m!n7Kg`r^(d`U_`G%c z%`%yzFHaxie4}bgJ4~pU^2Ff%0vlVJW5>tmx^xx1+#cyAA0s;zy;GWJtB6d;hY>aL zm<|PO;zD0fg{=9+hPDwQm8?wm#%{o!-cGF@Q#nR`d_^^My4>_nV+@0=K=5Z#31{3Y zn{bPZsMKJD{MhEHP`)S;^&~EMQ0G#RL-^SfGbj3va~KJge-+ku3kU6D77um9O zK}x=rb5dm6h{i%e->>Et0o_F4@27;yb6zAsx`)zq6-jQmmam~lwQ#fsv{BfTUVMHR zSGfz>tf%$-(JV7%y4nNAe1&W;+YZ6e)&!3Z1xD}Yg?5kT#S4OO%;*79EJMnIjulD}Y zdGC?jx4`RDflIWvQ8GY=3Z=@$z6P%+jA^lmMTeikutFK<*8+t@e9SIh~OWEY}SOG%vw}DjiY5EbPDro z<%*W}E~VtY`;Hat9-V8hn-DAQ-qIX?4#I@|>bnnm>$bcP;xD>P3VucdOl8gB^GC2G z433QqxwrHUbuIGTo0%XbTE=^F&C=b)=hT@-!Y0W}VXp~B>BCUtK@mgINFPihic8zU z>xrk+aq`WwNKAj+8n05or?mC&+RldDwp$V56VE0FFd&*5kWRx0J|-g=930u{Ow81x zNr3SQ`ydZ0+V`~j9<|7QI^f&j`!Mvg7xaxJI`Z+=eUOmuF;ALo&VnRSbF6i98@k5T zxYgS9IdyY4HGgerhHp^Iy)Ja~Q!e=|Avdl_y+`M}&z$Gh6pg)DQ2Q<`{QIRKwjhYW zHu6&fNuPWYt`n&{HAh38tQg`NRM-c3?t>C6*cNDGK%QalNk4$W&FrD6+%QVLB>-If z%z-z@kY8W!gT7RwDB*3Dv>x4r#?0tBA&M~BjO1{6yp_G|vCqYis;L8qHUg@leqWrh z#x9MizWr%8HNyAJodQ)ZO{d3d6zK^$>}e0{ zdiU6HW3QWXxqt=A);-QSIKesKKOrrYz#WpftbB z%|1x}?$0#yM;K9hTLtNY0_K#|Z%8eNChdc&RDn-Xhn#%W!CbAMe2M-rlHtAJW&{2H zyE_3F$dY8PJ%R7wDKM1GyEeC=c_Ru|gIuvX5v`p_6P_sr>ZV;xFMX$0oSpVi~nhY>r(2m7FtWvd0UHN$HIJ?mqyA2+&L%55bL^N7-A z>%9@DTQvj0p%kqaKxv;=LhuivG##VAJ#<=!q^zyi@yUH|ZpheJv3n9ze841jG0=L@ zn4iDz;)KpTEYnGll(Vo8;{JL@b#3QNNo;`Y$d9CgK{yZA>$qK(^=AVU{q)g|wCc3^ zJNjI7UtoWP>dbW|8bBP|-%|5i;;7W;kT*r-i^v>35qdz*8>0XwR+dRQ`EwT9*pM|5 zYK?fFdq_M1;YZv>uz%UTkNmWt09iLQjoI?ox*;f}?7_cS9FF>Bq*_OC@gyi#b|Dps zBuiq7%gh;{AUl5sC{JpU^HoUFOG8rF&DC@8SI5HRiqWJo^iLweSXas;&`=%8P9n*f zE&1AQ^;xE8Czij(88mb@$;~_hb98V*W~HB<$laSCPyD5$KQVV@9~2*PYaetDFeV)? z00e6Fx1H7e2XovQoYO3F7z@GMVkp*NJURgK)elUPgVAIFSL2nr<@r;Qh8Y4#HVvq4 zwM+^RV57K=D6&&SdxAzZ7T$eON*UxQp^)0XT@^Y98u$7#^(f#N8B+H_J%GJpCVr0K$hZ%p2~YiE1_3wb{3kc|>kB`%Nl~s1N3kH(ClHPc z>4x%RU2OtYd6!4g*RQzMHzc#Ptee{xlrifOY*B!vMJBSAaVPJb*u7jtv0V zTK@J=53tQhe6S6ek#KXyq2I9e0zgD>t|GSq7rgZeFpn_3pG*O>s2GIU2mN>6*@=dI z`83xTbjU<9#@s7Ps&+!|L`!vJLu=gw{|rxY4kOW(J6)nQF1q=Dg2avm@TZqMU9*~a zVxnY^{0Ka8T^EpCr%I7W4qtHJBmI7zeu@wBH-4&edBF`IfMvwxA%n@l@R{Vk=KYhA zl_U#$gFOIL_C!;!?&LnGIM~L*Ihgu{Y>SY~)Epu<(6W|VzZ0!6TM+k5CbV{B%|7Tt zapk*qUYIYnn#TELlUlV8dM2+2wgu42%yY22;2Oz{y1BSH{J8+^Vow|2=*BjT9UcZu z%wr0*-^lJ$s`-T33zyI9JYdg)q4z<~BB%<)3=UXrkqGDl#y_feAH=20*ilC6p-27nDjb@ z^jV1-{9LLwB&41I_jH(9QRkJCm%jozEg6ryc;RDhws6lNUq!@3T-0$=5s-M;W~S_e zjxG~eE5Ntqbh$mTk8@F6PcHXvED(v(r(F9r?mQJ8f`kM*%@LG|NAUN1Orf#(Re{8$ zC$O0Cndh$y^CFV0W}$u_^3Qxin@`RrXxe6hb4@||^rEYz2%U1 z;U!&HoVZ(Rv*8&i;fnI_P{H0>2eD7(NvRk~=2W zJ|IO`9AS%JPSK9^@;QYnrf4S)5hT+*Oc`?F6TTe%cY47Cxzw zYikkSbrNTFN%!6G-xn8FYrZ*gCaiXptCUeqvMM*rolFf2q-~3wMU3JvR)ggzE#VNX zB#kK(H=IKhTq4#wSHpGC1$T$L zy6zBt zE3@1HZz^!fPRx<0GjIt+1mP076IFY(CX*ctTg{!ge{9awH+SMp8t>_;+NTSl=`2kA zxXZf6V`pFP-S0+zQs{(mPR*BMMzxLDC{pQ8b9TeJLxIfHYER$KH3rQy?0*({7$}JG zApI98Nj6^{%o3n7*(ym;SPTX{QJ&0Krvs!&9E{yz(r(qJl#ea=glbFel~?G^%99?< zFO}}e$qwv;yf=jq&eT-OQL-d$sCgnoR~B)W0P7~F`!Dyl$>1Yjx3)=-=0NKm-pFl# z*F2vtV0ss-e8Re+&PCV7AkvzUNn_%FMzN+=|H#b91@pJ5!^L{{>az~T#+`lrVRqzV zRl1EDvFujKRc&eU6t_FFlr1gJUeMPzS!Bm)6}Ak>!B%NpCNDydgsOy>wLfa{0oKc6 z_u?O;Q)+!W9hNhEB5W((?)tkZ)ioLE(U%?L1KmPVIPz>r=of4$PGC|@5yC32Q-HO> zPa#GXnsmpZZ$WTIH^R-UVncw(?f%-1tW0^DX|-`)CZ9+hIs8*9WKydq$W<_^4=_D< znAi3}?n9GZS4o^Dt3};l?x^DkE6kX^7!l1StizDq%XctVR@MPNthQF==I+9%?b5VK zlA1m+MN}r_EN)O!F;s}dFry%~f;Nnf)je6#11vmbX6Q;F%<%On4}$tUTbj0h*kSCYg!*xs_3-dC2>>l3%?A(R;ETCmWHi$P@1|oIng;(17I2Oi~FrhPXj` z^8%qmF#9}l1{E(t?CXCdef_lRqZt-hU2%z)cJbY)%`Y=4pn0Tkz?9`xQYjfmV!`#r zFOymnqE%suVhciB6qSs_KL%jyyk}>}?Cl;32zm}HH{_mEe#g-utc@miAd@lNnE7mG z6jy+*C<%ELkLi95F&kMLjT&+*_1Uu<_sf2}x~I`)=wa(-c~|t-D}Sw~Az;y12;+ie zp*W6Ggv-BT83@ERPo$P0U@1$S#9c6IR0J?ruri40@+hU1(&L5A(zZ|{kJ(GDf^ zuVDBfZq#5LG6UhWh~anmwj&*JswFbikG0H8JE%uR!G9cYbmXk`aYJi%_WX@$rhdjD zNG;o1Ss<9gwL`px-=WA4rCs_?GU_IcOp1Vzrrrr*ygK#MO{j^qfF^Ka$QaZS+;>!1h z%&rssjGeO_r27PP7Z`7rc^g?9 zRYe%;6KF{JGP=z-b@p6`c82N<}wch)5c^gS3b zX8u`}0O@E6X|8*YjdJ`s=|BMZ8*h|Qqp2Cx;qK~zUKWE!nE@_~J0I2F^Gws}9G-2a zMpL-M$b2N9c~|c!X%P;5&B~U&W9kD)?R7Pqz!`(r6qMYO@u->@|7E?7rN%juRALvB z6<*XA#YB*wh-=fFf!{Z2XlbY=KszVLkD6YLPm5TWJYjt^!T+}EtWtt4k zjfa?OVj2Ej7mQ!^wdq*`xN8cTg8lfecxB=d3ak1}P|7!Z^3;aEdcVQi(9&RcOLfcV zY!mbak~Nvr9xl0fDRps^3vredPtfYt<*I5PL1;-PdsJ_28&}?B!m8HQ$04fayeq_8 zlFb=~J?U3A*8_<0w>G;T8+r7hk~Uj_kkmaYcijPnEtzc$pqsl(`mi`o+XoSmEA-}! zKw#m6td*jT1JI6*GWBKTSD*lAn{EX8e?R?qjzbpNY*iQ8PW4mw&^4mnHF5;x@#F_u zj9R$)9w<*!4gIl&+(v|A0Z%u~*bl_LcF@c=@EwF4Nf|x8s`M4Yj?fn=1F(lR>Zwi3 zhuVxGXHL4IeseC~y(as+~t;S|NLRB5T007;nr0xV7q0qpmp?`8sIw zEM4_zNZx?-Mo2O)cga*Gc1h~FZDFfnYM_4Odt=Fp$R|%I2Zn25SPv&s<&HfNZKS9U zuugPk6~C{ltGwrWu==7)y>;5@-RibomYOeFO0`F>z`D_s&`u;5(sPal0mbB4Cc^N8 znMmBO-#omnr*dT{g&sTGOQj42c0EM##2Tp4fr-+ig2t|DmfvA)4 zoE{Oyj};WNThBUPbot@x9Nr<7CPU>T9+){O?II>M~o#5rP4G2 zq~m<65*e~W(ggD?E*`!bfyJr=BecVHc+Kl$B}VF_F$>~hxRN-Un2#<9zmtkkMmzdcuMGFdcV|u2Jvj=xXRp%ORV|lRF2HHzZ0AG(OCG6t{_RPc!tLBXZRY-md!C2EyFjr+|-E{(E>~;(FMDMJ$D^E}l+~ss_itDMXw`6O}Uhr!& zrh(x1*NVzWHBL3u(l%C~xnmT+g`*H05iOjNxF9&ypXXFx-|}&xSt&1ukJVx_w&u8o z%NY;@iX^(zHL1-4sK+uXw7ek&VN+g`tfC?AsAbt|QXYalW!F1(p1m~P0r{RkPs`@H zCJ`JzS2K}F<3yN~?k&7yNxuxh_6x<-`X^-_Vd4P+EFIptFJHavEq~#vXM8irL_;*t z7E`skR_aXVCCO}dHZ!Io94kl$;^0rsx;65(cv6tUX*&VITO0=;-l@fGO@0#-heP)>Tb1&^r&-HL(PsnYcHaTG zU|RxA5jscXLU!H)9Ks$)CXMwtK;AdcLRM26=MVsa4&O!M1KCIx0Is$Sz@A?(Ku8*i z6xoWW(V7Y*s5Srxodv$@=!W2oD3S#V0PHIT#4QV8d}VzK=rabVO+Zqai~xIj3>Yz> z3J;E<-7W%_zmt%i&Dei|kSnK>BNo;Ld-j&f0B3jqA+_dn)Hm`K_RqVOhp9XA}ZutC5ZZ=H9HvWoi{Z;Ws$SWj~lHAZX<#OcXlCy^F~l7JA-QbhbrtqIe{tZKT_Zntj{NUwtO`WWeNavi z9!-6{4_dLhhqd7)t03Pr!EI=>fNg(yVO%c9}&jkN?nEjjEIXQ+?^*FM#@c-@B0>ZABQnC|!6} zq{#Y-ArZII%O>1+B3f0d9COtBYQ2Y6V-$f#=?fj{)Hyy+1XNV;>T`8W}b)!|0`qukFGK9 z7jljko4))(?{HSRPrP~2@R7?O3_x5K|G53mn>PN|as8*d$!Nd)V}K!KEO+l@fuWJz zg@=k<32$8B;z7Yf7m4%RT+<>^T&?)vP&cE+lm^()@8 zpBTpSt3=MHSfyp%F=P{sg`5E`sgwzRYuq>=b~=&#KpAtttnr>(B1~wRroVpIm=$(*0AP(+SUDZKt%WIoJ%C_Qhl+`p;=*&B0dLNV&g`0yU#QHeo}VHr z1)_?1N;z94Qi|@1r(K^H(P=8<&ciug5H>PBaWvXNGp6&-&FV!_^7jF+f8M(ON@Wfh z$6CpBiT)_R1ymmg24yQUI?K^H8R@)+Fi$>>mR-SKKVv0vT<=LTkg|3V(dlSD+2v8O*SF#t=OfFT*AZY)VwxRxSNct-`;{ zi~Xbf{&5dy0kb>##1r4IYb4y9dG3d>x4jg-vMIizEhp-f>n*Il;CXt*saUH5(6_D3 z@!GF{-d_8g!TfjT41e$5e_Vk7qp_3@>zQE&;Q)Jqvqtq0H4`?L{J`mb(2vp(`RhJv z<0dp+Hh3r1xo<^Gom=A!3;)Kv`o2 z6}1oAjM83;KJ!wVSIaQ4fb{qM!N2U>|IN9}frO@~z!KIoH`v=;UAt7S~#T6Hv%D=}9$n$?8J z&DPB(;DCVOA1{>#+4wbcB-zr-5-m1-7BiC|$AG6B=!)JqA9a^|550eEL4%k467oic zZi9B5S{D+*KdCZva>v}7rYU|*Hd4bi^98p^GTC><#xj0X#bHVX(s$&Yu5MvG`mtZR z`)ilu33f@V>?}x`>%%&&@#2Pt!sm{#>wyD-O>oUdnYEjBUh2cMhDxo{UA<6u%YKOc zCv-gJ_~OxX?bvCXJ-@E0A^v5?FLyjPUTw&(LE^#e6*NiG*`tO+;+Pz09_u|u|MG|3(j?;S{ zka(x^&11HXtL(kGLmZM?UD<5D?3tI~J3?vZjJje8nvaF_R19@D_$Sl{b!ollYmPK&Ot@qRCxl_aPxr~YW#+qTZ(aDyLg{_YpF$+5JIH<(d3wrsaYesDIrE?L&by z@)&FqcoxWosjd&t-)St@E1vCk4ZKKQD+s!^q9@|!G(hGjJzmrHy7gRnLGWvsOnCDa z+dO?#=%VT>rkXWvFR@3p$+5*(^p>DxKDTuH<~z2*Xx)r)DO zsdZveVQsMlVd|59_EVmj+b~1qr<5yv^ze=~rM`wXJza~seC%|e6mzfI(FA@*#wWx| zV1b1VC{3(*>O~B)-3_=#M?MD`5aXuHn2i$;CzzA+h`t1mMDfK-xj&%ieAA_SBjOpk zMPkUBqz0!OhwcUXGA*wkb{6$OzXwr0=@ZYN*05%Hm;n+ti*zQRrpO=@+gR0Yl6Wn~ zl*H14Ib6TMT&HQ=mO%aJ8m5HA8iE-U&@r_*cbD(Id47SO&LyRZ5*?qh|9txYlQRFK z@|6{{A3wDgYU~n!!gJc+uPc}Sg;1IZ3TOXgh?Gl!!D`};Y#Aiq<8E_b7R$VdR-xnG zOi`6aQCYEv{4%bb2&=inhZeB~K!GF1AJ9vAx? ziK_O>g?}2aJwB?+r;;aq%Kv%B;p2h@Mt(1gzBz@rDHGIt1f#XpvcB#~<-I$T|F!y^ zlu)km8?f}jYxy49B8?v1Hv^I(>}v=q00gY50JV-Do`2PWZHqTqEV0PAJz5|;5z59% z%7>rp;&`maA`Zsd5YPj@&jN)!$bAJ7k^23IE>+DR`cQndjAAyrO&hU4)8`z5iw=3QaA#^{; zC649}LG)y8x*)8zutRfKuX^2N|MdD}{5=ZA%%U>Dh8qqe9s%eQnaQ4zIBXga=G!M+BLpm~l zY-}@DSG6<*Oa#59-e^c}b4!Rxg^F&AF|O$Gg{!CYkS}NR59`IdDz>o-Ke%JQckJe# zM_8TzxcfbvXGq?Phn@TV_viJh)vVQWwGx|#B&k^|_94H@PAx}&*sjYLPs%w3Ngr1~ zvb>_lEA{fx%~Ij9TCFch&=UvD6$g!nE;Rfuh4`0(o4Y+9) zV7Pt!_}}j&%${`3)nj$kiM|FoGftA9&jZ$Pyli#^Y6G8_md@yOllc_i=;({o*${6w zjU;xp*sfPrxV?T**YnxyjPW;FA&&N-(;yAixnFWfMmn;I3@R}YBZ9CYZU%f?@?R7o zoop#PV3sD)wv*dLDI1E?ycmk)B%M2FkrnnLvSLuV<}JUu(Gxi%oKxCMZapEo9?-7? z%72tv{mpm5*uHok_TZZe=)mtLpR)^`OeD!IEqfE^%(7JM#37`(%iM0L7Y+y1MSHJ6TlS5W8S7ZA~|~RS?*M8Gr5H zG{;Giz|WpC>7M1=tjyBTF7RNq<^YW7k572Lq*|`vs+_-BlV1pZ8rQI0 zj^sdS5g0qE0^crI5vV`v?p42#ebMfpb?9tNyKj&lPHW;3Jbc0!zh3-Vy&02)(0bVs z)T{58!}^|5R{LsM`U(Kivry7-b0trky68Q74ifB^V;OAH_GQv2clja%O~=uX8E z7JEF;rUo1#Jy*GyQvyr!2nfpj?(sd5^n#aC=u9{3lJs#uF+v;DZ&ao z!#wXl2<+T{1=QrLY$k)jV2kRehQ?PPYdQ}iwEr``Joiao7)sFk+| z!>TgFi(9ISh&UNnI)gng(CZ*Umc!-0II3%_ zjz$y=eE@CFm_o6`pDG1vGkFCv9xkLi@;JO`Pc9bCy(wDr=JCLU_Trps6Apa)zKA&C z$R0XHa^%M|#ald^%<(p#Hv;Z1YP1hEEC(#!j}NFTTjwFlD}HMZyFJB+(H3A@)c zzpnFob*LT6wCco-9~&MORXe*n|L*9+8^8wMt0Dc11sdxqAQOlOn1x!;@7&N-pTWy2 zni2d(i~f$SP)`4bPxE>9LUTo~Ys~ilt?Pe(G)DQ)X7c}=pZ%fi2VJ&GED-tVkKWv< zSo8IDj!4bROXiXCTQraxT;9i?3EH924Y_6)Elobztf2s z3({O=Mf78KCO>WKcL*r}8iU%l*9D5)agHTGO%ZkiudwceFIz}Vk*|dup_q{(7v#Oa z-)b1y9%8qfnSoZZ)b}smsqwRR*AQZmEJVLYlQeRON+b)iW>hbYbG(%iztdU3F;+?O z2yj!Ky0&DE!4XdKfl2LF6E*;^Qf*3v(00`b3l^{TA&WJ`xE@mEtr7L&a>)m-H z#G$sH-D^C}{yr$Z;=8QjqBy9V{wp#mO828K7m5BV!4MNhJj&7)j@Wx%6#7LQ<6a-D zmTRXb>f!gI{6=9;8VJgrNQOiWx3K~BblfX6HN40x)y*T`nKJgXsdg2u<0*zI`qkHs z_(GiDrY(wzJ^GBCe+t>U1{T7sp}x)S`4?c6AlCx0JH#v!P_ljS<%c?SJcOB|PzLA4 zZLNl3m#P*76Hl+Q5Z^36nCuYxP?f>=DIhEP9V2szb3fzSI`=Ml5!fzuD&w&$`7lMD zl#W9&9kp5D?a?`F#%O|n-(Z_COXELi`A{6hlWeyjbBO;oA=YL|_BGyz#>`1b#i7>U z;GnTUC4XX+pu;TU{D=s!MR9K^cDY%t>cUe)wULT0WghCtDI1W|MgJogq9fnfwJ8yz z9^%QJXP&>VaO!ye6)W+qVP_{;?bG_TWR^E#B5z+lLpj6fDZr!JV)H0)YGa!&X>V#C z9MdM?2#^?fQ~s1ft93+JKp47yna=N7qBN(mn7bpP`sLkI?XT=1?g}JnpcEJ4oB64Q z_ypLbgkZr5I^*A67+p*~>z2DTPE)CB&ZYX2IGwCg4D%jlEl&t zxBUPraofkJ{v(HjdP}H?(^kiI&p2H^4!c(d@T0)4+X&(VQXHAHp*MhqB7)a;62Nz9 z5Kfkrnw^=N1=sP#+B&4h-^*3;Y<*+*Ai%>M)1pGRhc>u1w3bTN?;u}^dOyodN)B+r zx{zv2Q+C%TDMQt7gQ^V$>2KIvp?d`cWc)qwFba<>jhS&FlOzdQ+r!YvJlLtEB8 zx|P0eb(P6ga2SqHaOA?|hd18KXlit5n=_7Y+w`OGRth93n-ruonjIlngI`K7^!`9G zn#j9=@iTEhiB%k53~_Qm50ID0lR~Vr_W{AgAUn)j1Mup^w|Gc9DiL^U$pECgKz`)@ z<(zI^?bd=h!+IO9u@fSuvb?VT?Lzr2PZ`7?fyhR^x(Xw@V7QUhL|xe@l^MbFwYfCf zhWnIoIs5Tj;;xg{Ryk|33V%oxiSW!m6pywf8HBh#p{qs;#@#Ggkf+xh_b+@koIWrZ zS83bxeD`II`0~J%*zSjBj?>Yul|DmAXB4)hkHVg3MQSP}Np=(*Ys7EFEy%kMb?vDZ zO?ffN=gl-{D%`qOt)h=Q!X)?UM6~YYQ;dupa=vD+wyzuf>2aGf95%lbvw)vS;O|RT zb-ds;K8vTm=;r90zA9H2!FdCc%8+Aa12lfFdl9b;T>=y$H&RPijKV>-|k36MLyuNSM{g<-w=O+<8Tdw|8b z#&Lod8$Wupewg*)EunaeAF5xXga8SqA?OMIO(J!^OrU z&}&Tt2u8Bk%4MMF(Eu{v93@C%S$Owuj&udWCMPx;5c0sovuIR8O*VXO%2*3En848< zTUE9$W1F)BO~TCQ&(+)xH;8|{+zCxMVVa7e5cG3gZ^ zP2r`V@9SF86(+G*w@tL33J@c>eTma;DXvt7O;5e&W60c*j4h#t6tu$cQ@A)6H`n=b zP?9`v7nG5taP`Bwud2M}DQ(TdfpVC`*XGs;|F}0V4fyGbImFxj4*F$?>=v?(2zx9l z!~qW+V}ar`o-AOFeuAT~Btc!DPJ7=lcrn{+8coo9FCO|nDCnh8|7^lzf&$=qZ?_%s z9A4E`A{ZqiBul2>%Rgxz50&ygo_o2xQ}vdeRsEQ2tq{&+9hNhPw?L+M5rHb**y*Uj zHg!^}K$mXfJd0p@xDG3m|DtI^&J`1bW@I_MyYlAp<3`EXK@hy-97!741(`A>I^i1< zC;))K1SRI?Ej*}`#?)?`l)G%@eq2>f42+$6=5%IJvJMnA(IYd&(R{jD<4AeQ@tCjr zB{qFvy!8-C3Y-F&&))}CXT~BNab|-0qU@0+W=1L@GpcFInESol&-&i?bNrs?et!S=eSYupzdIbwe82Nu zuIoI{>pVZ#b)5^0%Xe;HHKZ>#y0MZnb;~2Llru=jWTlO^)QRZG?IOk>!vUAuX8B$W zBYh%HoO+@XndOW+Nd`gkIXBB<6INbML2%z`D_K1_@&=3xPb)ln;fGh;8Fcq)T38KD z2fS;C#yxGIM##j9AamxsdXDLsCU_++T`>PKWm{>!q#TkGJn}xtByz*8d)p8QsBN4Y z)r`*0Tux&KIcHh$oBKbyK^wOnu0BMD`nmDl%q``*bjw=pLa%a6N>9B4b8sjF%xF#L zU(!XV+Gi*)$rs$1b})3MfV@>b?+&hP{EqVvD-FA}VXuc(4rh-hT-qy`-m`I$b*N6H zgzJBzAu%D(P%=rd_2&eoz?eb0{6xEA>7r|Ol1|iCbjWjk>1TC`iJ$qiN5`!q9lo(e z1zjKLUcPyvlq-w%nb!AiAOXNvJmKo9^CpcKOMnh=U{VZO$kn*)ufyVV+?guQ^wBdv ztz!$!Fad{DF&y`CB4$DRkok131Jw6qM?7x*Dn8bw>);v^W{i8~-tr3ZCHvI7UI&YU zMbiY<0y(Dj^-8tiGf`%O`eQ6_AmV6W925OT5pVNMJDM1))KhS5W$yKfD71XOSYEwB zrm!wd&3s{pf5rXOTh=g_vas{EK8x2`& zp{q62H%sq)vyG%?|Bs98K00rbFK#;Nfqm#$(1Ll zfvRRc;J4}L+bvdf~CJF)B+KJpntmCC1;DN2+qy~BT!)!paL;a_G%i9(*UNXY?%?y(t_i7e9g~0pV z;>HCbLb$-Cxzb(=j*G@jV55l>)0N-93fJG)(n;pv*fECbeI4eJN$P`3au+xrTZ2#t zJFRGc>(_~0&YRioX611sk#wG>&VazfN16doEsIpvXf{puC3jd|)F>!usH{NPU;Z;yBxJF@C{ zG4gzR#;~J*AJDG4^GW%<+uW&!Ik%Gyy;3`~Jn>%c>&N%C%TpeJC>-g)9XVi+ts+eF?gO(Tw(<`V08)NnAG=H>Vt*7n4q5s0u!5upl%y5DiOm zacU!mn>tS@s@IG?@5g6Q5IgQERoUP1Q$F|p$H%?*GE354OKfc0#*MYn|6gfo#celNodG9-$eHt#5uYltT zR#OOm>@1=vWabP~)-2)S&AEpU`>A8Gqo3an#p3xoGmoz86m!Im7h|i zMTaJ7i_xBP>mlDl7UPe3WPCqC=iuB%Q9)GRt8L8>Zd#-o%)J2TluDwnriJ9ITMo%m z*QY;uEDwhV&R0)`h9n?Yv#j^gmvDk8Zc+=Fw}9Xk9B23_15Ii?T=SK|(aZV9u5mxi za_tm8Ilffld)ay9q3tOR!fiYNqlYtSeXn46Y;hyB2jtpaIqmel zCcX^Mw~)1q0~FD!gg5igE)FHt90(fPBmE|KwYWK-Cg3=T18F`Gm{c5FJ0spon#h@o zX;%?!^5V6S(fJmop|xdPsTjf^?7b)-6|R3^=uHZP;pTlD4Cb>$Jq#Uyu>z_ZHR9rN zjj(u{IHAz8pHl2U2BO!jlzKwzHQirafBsJCwihP&wSx&m4VMX;5!lGMWAr!00)~<< zaWCVzs23MmsZk*mvuOjrkZ2tI4zwxo)|(rLg0s#U8rdcPWEsc+xfkvT$WjM_2n9Y4 zQc`zv;uKIu(=_>rQe-c_zx>6b9BHGy{F{Nzu$1smDfX9+47CQI(nJ(??K4PS7ugP@ zPoi7o@(Cb0m+ues`X@sPcz|L+<%coxZJxxLnBCcFtmY2S%XWT>ORC7t`3p(bx;HwSn^0xEW= z)Bw!(nD0~lz}^+}TEVgEl=2GM!BkC=rp&ciAxBK$ShSXTsifxplealbvg>-6z`Ux` z1ibS&y8Be8;_6ZpOCrt(oxI3LGHoFjeKSk6A=}fdLqZLXm7JRH@v}YYEp){G08_K7v$JN}*Ng!bkyUFtAejqtEmw|=ZOs7kbnsdE~Fm97a zADnZF8f02@tS&cbUUqXdzDjIQ6ZlA0WgC@*T(*Vlp$_is<}1U%T5x^EscrjyDueZ6 zldr^f)KpiiKbwitTNsR*^*Ge$E^S2EDo-3R4eo`QjO{Ks{)^6ljNiY35GZk)Fc~ul zd5!CFc1&oOC&GpOBNw%rpB9XsoRpuE&*1SyJuo%5zb)x=yfw?~MwGy}sH@cy^IdHQ zd;??)@g%ipIqda32NPI7vR3;mFl`|y`*7q@8joW?kb80DAVH`fB1T0xz1sJP5E$0| zL)5?a<8k{^HfhB@+TlFnWuF|lSO?fBqtrwB>*(dtt8uaRB`mSJ}Mw@P~z~!*FX)^ z*38~|D^HoI&li*|3}G<{We-OwMd@MY?9S!4alN)ikjy1<96oc2PZLI!)9JQ#C%h%= zU<@Vv3v!-fkq$?7I$j)1x~AWC$9hvHTjip|xug1TU!PANt&JPF0`Cgg#27N)=_#)B z8Nf0jCzfo|A53cf_FngVfAf<}tNzWfgq^`&Bp7>OngU$vaR3pIr z!5TInIz2IB72rG**c71e8{R4r_RzKSPUPOm4~$XDTMG6Jbql4}12&-9IFd!-31USK z%Ic9HywPkB!{$qeb`*WH9lIQ{zWTx2W5R5YJL=&`NaLsF#9X)eZBhC_5z9_;9LGEr z(iNF+v-(JX(e=WcA=#%>L1Kp+Cw1951WsN!{5ZaZLCpvvP46paHt+&-JDvIdIF45a zvcy=O0Ubjga$uvgz;wQW-rf2ai1yplZ*G`7Tdg)3ue7LY&V%P^klE+jM>U<15rvkTHNi!O zcX9Kaj~m7Dv_lDy{nQ*Cbd#T>(80_X6V8pFE%y7K7EF7%$3o6v^mY#a)i1j$-uXSj zu@5cOPk>~?8)ksV`mD3xT1Xjnu)B?8@s9kpX6jyYTm6y46~nO~=C3OC$IgRSI-_KuhHOirh8clc^#FFP?e#g4aIALMW>bnM`O5UsaT=v2E zmV5se58(HvY*7o?QaGqh%W$eJcD;cJ<)G@jJRy&EI~0A~?iJFuJMnP6 zfFSdR62&2{-qGlk#o2#JhACqw9jm@m1@lINQ~}tOU}SW==Q3i?1t<96qq&cCfvL`J z^#1X(jM5i#vsvj}=R9?H9hBa-&;(p4cd3QjPX6$Qo%fk>2R`ChzvL48QqR=R$a_}$ zw|G_ejnvkgj(1CV+PH>p$eA87iJNyEC}uqK3V#b!xf3xeN4)X%z;WKI!SBAI%bhjDK3zo6euv_{A0FtJt?osvyfIZiz zpZfrSKbtVqaKC5z#j^|!y^bd)q5^X|=`I0GSBb@>v*UyJmbdKyw9ZdMS>r{aX!8+kJG>2n3L&Yf+nO0p!z=%0vEkn#7&i z9)ZWP`Tfi=+g24PS{txE?7;g*P)=+0MjNxGfP;c#u#DrXJ$U6$8<=+;Ni5RBh|Mv!k&wF(nJ81v1ys{?91^gZ94o!oB0E*~N!1a4jEmq>06nTn6A` zIM1bht3_;}t?llQoT)>ikE%7TRAY^i?t+dPmo6ncCG7JOO&d7RMZ_8VmT-^asL+}9 zm&@=u2%YUaAqKq4lVqg%4WUr%VXt$1*@ILo)aEvt zy0#oeh-ZyVA~Nw_4G22uA7{Z;ve3;s|I3;)TNGHUmZI7^kV$=v>I=%H)J zvhD>snGbr$N`m*n+0orWod;0%q?sO^IL(MOn%FK3Ym^=P@(wv5^D3#X>cw8Uh#aqo zq*gyR6S1~wpG4s2S4>rOqzP(t!-Qc37wUV$W-lbE&BLX9G~>&IN!70+%R0P=%PyZe zKN_sgUwfW7Ea1(KwgnN@H*xq|=oW0|&kR90R_e#)fzG|~)E_emU(3fUCS69Hu3IbU zS3NphS^7v)Kw7H9nzBjybdLzqyd*A5tdv|7A) z1@o>1+B^vy&U))8O7g_BU^H=59VS%v1HsOe4tk|)S3QV6t z)g*4x*#S5w8ealwmS`Zt08e{=z|v{1vLl$-M}A>*F8<@&4Pud!9PM=nuF@>wZyD;w^;6S&u1twaT9LX9w#mJuI{^On9T0iJS=fXP0sb$Ny{|hB1_5dXJV@3 z+~`XXE&~%HH&sbsWpe$LF3IknE~17Cn3;W^loVGOD}`>P0#C^4xde?H$I7S zi;CTkhdDkh;eNqQJ;c(xDG>J(-8edk+lwqEIA$9#3#DQ2Piy8KOSCL=nm?h^C3yc! z+$U4EFu|VW#I(0ZMT6r2$V|9Je|ZJF5xu=1b&?u$uidhGt(oU}MO6-uTx&~f#z);u z34Z;aM|oXu^7>=SF9EC9J%&0#l4~Js-?_7<6d2hPYv_`xh^oBJdwx!xY{)gknC{Ts z%|MKY9Yh6^zb&^Uu|js?KE2oj+FaMjmX8)^qDcdFLFe zo4tZNaTUAmK;tGC4*(qYloB!yX0lK|WQ#+hOC#>qu*t-GTx-t4yno_LU!udspRTI` z%s=Im3qOY}rn7o^k@b{Zg$5yw+=Dn#s^G|_pHRmt1BkxmuR}AXZyt(Yy%wJnc@x{* z&WG<%A`B)rV!3@IkN6d+ruqpaCzna{%pd37o1;ZxFNx!4BwN#j94Y?f;tjBOxR*Hj z1-sAJIR#gZjB>1<_{b!$K|L5tu%~lKL5&*c2DqNf{3&3?p()2*;M_b#&x9+konIP=P8iI zkt)BL*(vH*Kj>4vcEzgU{7%UY>n0aV&KDt1=m$nwjYGOTLi%HN_!+5~ZB5!CsuQV_ zV2?@cRHOG6A4DA|TbgkDg~ZRt7a)Y}8X1L9m0U7xQm(dXUt+kdgs zojV0zH~6})(5qE3r0EHBno>o5bA_0GMDlgkzAUDf`%Th}yPjZtkGjLLT6h>A8ec>g zp_Xn(BO%K>@0x1nsd-%mwi7CvA5O#FTQNHt@A4GjQ9nGtTr{-*_Kanqla~~g1H+f& zNawN4C^r~qdj#nlA+;w~MdjA}{wrr!^RHx)@1zUZY0E47v@#^iD2I4uG%)KH*J^#s zLE|$Pm8p+NYhXR3Q-S)vw_xO<7K|gHhZ zQb*>BH_Ny59l~8}>fx5F?SZ5`I(sZhdim2&c!&^ktvil)6099WoTuqrocVUEO14e; zbikxyIQy-`RW>zvbL_-pF!7xyjwZK9Y-^#rZ5ttTlc%VjWR89`m!SeOXjZEOYCUlf zaRBMJ*dvU3vfiznQ*?U&tp0KJ2Y92A&GjYHok3XhH=3XXbk6u^hAuZ{Y7}X$>xaHy zfjYIQZ}mq`gOc-0M6qOHcUYLlr4koM6zFJ0wruJO>*OQs7+S$aiDj{0EsFG$m!ij$zm5Og+} z2^Q`Rl|u2*dqZ2_N%}O}k$OK@5B#4+IGCCdYNr68dY9p;2s&t~B#? zxfG={m4Cj8^mgB&3V%oc?pv1Eo;i{D6+{>mLui55_G5R!KQ4A#HSSq~R&mDEGb*`TFn4aN1Eg#RcR5@_>;_U*WzVeuAb-2em2#D0K(NCH*EFHy8Ue!$-x)fLp)> z?a(5~X5Gb98Fc?BFBRHXu#{|I_29wv%T2CQ6ifYi*B|{^h?QXxuBW9@XA_6&#Mw%? zK`xe5CHgS>%V{$@|CN@X8Dzm3F2^14ybrfAG=;AS`l%`iSTs0SoPw)^rgrkoqU>h0 zI91_kbM}4HZFh`)TC!%H`VBVp-^ab)VBBQP;PCer8JY~0s0iW?lWWnktxDBvs`tmGo=*FY?i}4)TTWu4g{}L4 z+Wv@V1oO>ibni`2JqGtfP8zu59s``X{_r;hbNnf;3)hTDFgOI%-$)Y`^MHz5hV@%> zoQE$C6kyVoSEf;p4~OP5UDEa*VTlgWNLyytH7djJF|*%>p}Q{_h>%Aqsm)Z!M@S7~ z3&<$tUvRmtIoEx;mGaAE!_l{&BNWJGKgliYCg zZpQBeJ`}MH8iYJ>gl}`i%`FO%d}w@*7SyYRYD>yh>e@ZI%9ud$kGh9~fA_QpWdt35)92p`3059Pa)YG)aO@vtqRRI{_M?5FECD#4i1UrHYz8Or zPK{F?KfN`7epWicc7KNJ4e`gJ0Z(^Ifh1RTvhEZcsxDI5dwZ7nfVCMksl%L|(Zn)R zBbos62$Xzb=i26pBbK?`z^03J9qBl z{5M(SrXkGX+-Zi)(|k@OT)L#j>_U?7{ED|(_IUiITyZB6^JiXCPe$i36e3uG@+u!I zX(grh6^r}Rr8Ra^ElG$}xmTY~b+3G{H+Q^jQ}WqUj)Qv8th+b#^afFIFLSJ z5`3e-j9S>P<3+@B1$44brQ9S!xTwXa^@6bX7X%hEHT7>Zd}MQQklYBu&%}`DDFvkB z^-)r$8+j>5YZ%T=b?kK*ShMLYmi0KD%y2kiWWC?s_PDng{z!vPGsgQK8uD6>JHyKe8B-705QN+H~Xe?6I(%j(q`_^*-eyCo^ z8+W-`5wQnO_VKzP9oy2fb*h2NHX`ojLG8+AY`UBHbi2vJfb=roE6iP1+SL~GJ5Je)!3cj7Fg6(ZOulo-9`xr~|Z<+oN2IyV-NHOUGE*aKToZhZj zj?y3NR6y3Cd0me+txrpcTn~V=GiqL|YAgHrPU&1^`cT4)mB>09VZJH)I7ngXfY=*1 zS}cS>F}o8G=}Y`5&-!7ni4$UD2TbKlc1?8FZM#h6Rj{e?g5o6mNi>mdqR^`E{pzz8c8Kt^wz!+g*|2z#dSej3$@P zPPd;3W>67tY75;y&V2i($wEo3a>-|0gAh#s+6Cghas6PSBXz>J^B6UR%-hrFk0WJL zmrn*t+dZvUY1uf!IVv`ug1-@ZoJI2D7~uA=#S`)yn!StDMR&Uzh>|eNIUS_66C_yw zX2#-SQkLLnD)2!m5)O1SFjVDxaaC2U-CyQ9tk9ODj)8p5#0qpoai@f)Ujpc*O>q@Hi4oAP9@yqCdI*oDV8wZ|?Kb{odgA zhFnQ!&%;A!xAET+P`1QajX?+J!9Jh!21oIYV{&=@SIWB?3*KC28L5|)?A#KFQc&hl zkQDl@?EC|5_c(PO$P=?A3AOF;`yZTibBQfy zzH8gOx;KP#LSy~pGv@jsRu*vQufRg-uT7IAGkAGFkEVxj&NSnyb)(2zLLarlxP*0UD1Mk-ZkAg*b(&ODPP~ zda;dp-_^{NpLRMeBn(TiDd>ckaKV55QX410cu|;_xT)WA3pFCp;MQop7pCt=rSP5c z_%{6ZL9n=iZ!K$T4dL(|IAKvmc$A^G3pT3WWB3iawAS$ouf3 z99;-We!j0giq2+VLw^Zu4{RMFKVWVsjLgK}=oBKQ6a%f!PpvR+o0K8*w%ZKL_LpQD@hwJxBlxKrrTXa!l%Oic%e#01F{?9YVzJ^@H<#Rk5@LzHd~2`7><^V++S2 zuiY)mc?0#o9my5cGKq}3SaK;VUjNrr`wiB5$~~J%scYGJ*eT4-KS#A3Tx7fYX{E^u zHgHhs!;h?t+kHJOvh5Z7+3N;OkpiEgH2$-|RR9~;4`~KKG*!>Bw^)!`X-00JK~yii zM*6SWv_4Hg^hQk2Hp9fJ#`db;waUxc*OuSCWQcM6=k_85$dTSo4N9vdZUJ2j?MDcR z!@G)v8jAbpnrLy-^)aFw>Kf(q^iMHyuHGa3ri>mh`1hC#d=*-60C%FOG27=*k!{pK zmefu;B*%bKd12TYP|kqxDBMD8!@#gN!dFJ7{bKI7ggTK!<_>d=01NyJdec###J3!k z5`my9vTU&2bE1AGo~guA(PzeFU$q~tTu3AobCos65c8}q(6ZHOpLzGWHN>$2p}T|< z-8MsRTGWwXy`$NkGNha*kDBv6m!HB}!8$%``=@&00^86LF(Vm=vx;ewhfR?sT$BHl zWgRnrvW+uCO9yfIMgUtoS!qaeMbCCNjL4EkKSb47XQ0OP7D4jPO=&%7Z{9w|p?lwB zVsGZV+`Ce8=LClpB*iQKDKjj#`M^$nJL7h=ubWhUk0tS#kSi(T!z2h#!14N3KL~K< zUl+c=;8Ld_#eDfdA8(e%P048N`Wj%wF!k)V6Y@?^fnzVI6`q>$o-8{#@AlH2@RM8< zsf5phefaU28jmJ3-@6Z$M;rqL7_Ul7WdCzLbH2FIj3$r?3=w0emKAB~G-;`?I3o^L zTW@7{ZmgZ9L>n7+-^H9U$MOMG%oed%z;655;Yh-bqBV;7PJ27sb^y(=V}{0uMn5&{ z%sVw5UTrsG$?0g^f!XnDdesw2xAxcADcvP&zwl22o^r8)^la z1y0gQHn6S)+g)pPSn(*X{`%2Z64NSoe!A!Al_NYQ=+SK?bJbOT{W~C0ED1V@$vy~; z$D(A(js$EP%Ebrx2&C{0N*s)pg7TkWPrWhwBx?V25iVPb?%Bdd#;Jb^2CI5pBGYDe;Np;x&zsZ}@!}SEGy+Ri z=E81k@9N4Lx(?EsP?3Y5sj1i&zl9-=?!Pc!c+jM6%gxG8Cf99|mxp3V)3@Ma<5FOH z{ae#zPb_Xp-JJ@*=EDPUAr!~m{TOMw)}u3R^?22?($($!+O50`gF)nHdF@GtUUtGf zSrQrvhKW1b=s>k?7nB?4QTPb`J#Ckl+IOu&Bg8i8DI?Y`}*$rN2yc(owO-jC+KWi*w{=u+YX+!#S=ln(C^k{N4L<# zOhBzwI_rCKj>_yp0gPT$rQ||q%dXCZz0T{Np6|N;YIFIf#%I$dM615}dhqeVR;&l8 zqlx9FIg^s#r?%VQ`_L&jrp1}CcKTISX-C78*K1OU43kTNHII8G2{(A2@pBD6hcwe7 z4V8B~VEDo@e~0E4%JetAm97k;R_@+hzsS&&RB6%cC(X{zg3D`KDA#6~(A_a;@9(_~ zyL6X@~NN06p>{D*qr#eZhCpV{hL2$dI%6v&Ypd zxz`vB*z;^j6{%Vnalb`{r(@yzSUz8pMSy$dOHaO~*ZuN8o)Pb2Zv(8`_t@yz2M&mS z+@Rh!Fmq-e+^Ls+LvJx9`HoIn0q{!IK@j8eD(Kxx1i@FA`TE*eGQPf!5LZ}`Y;Qof zm{XArt*Dv`-A(xE4|IMKphsX8vY_|)*gDFB$e(h|(Y~cFLutdx-St}4=fkNtq-c-D z3QMnEJ?{qG&Pi)M5FssYh@T+~>DEunfzCQP-&~s}2&pzt9VKx;sXvTVwhy&<-Zc_p zbe_sY5#MQx7zGugScUMQeo+vo@S5KH9IU}n#KQpnk^FnHScf*w*zg0D!lm{{8B<>j zkLVubPK%R2RzVZ&rJP0_ppWUv4{#a}_4bk9So!%qc@ zvyrx>vjCP-nByR`G=)}@m;@^cCBgcXOO8ZyKGuHa!qcS}vD4jq&BADdD=SadUWQEr zr?B_kmDc-K4J9%OKE1EM9OJM#SX1oL##x&%u^vK6a=p!qXMjKIdalWgQQgUj*i@o( zV74z=KHUk*#*Y_2xZ?&~S@)*zG|;5C_oKVnHAmA7wK@UGGQ=Y)j%jP6d^SxL)s}_4LRd3!Q6T@K~bx>MGvOI_=Vf&mv<);ba{B_G~Y089OGIw`G6((3#6xvM`j zT*2=fuEz^r)5$^ckF~oHpwz>V4GTi%(wve5m6He)KxT0B`2}m|l`ZrWJ5~Ak zPp)LQ6)TqoCVy`o>NFf0$GKzUi^Qf?v^Zh-{2mTY__$+&&TL?VTUYew^LB zHwM}ZWun6I*d*c@sF3`IrKe41znea%)PRzbmuFT>{=vtebVYQZZ#tnx=&UH%PO0X$ zs5OKN?djBPo|M&aHmml#wxm*d*DLSE*%nELkg5G*V}gcu0Pbtjtm<&!Gb?V^Jx+QK zWzjl?HGnD~{E=y%(_B0IW>C4VyuM7iu2WitcX{Q)^)uGM*#;zK^Dj~Xcg4tlEl-}V zyN|T)pEKqtF~piFivm=SK-;9_VY8rq%My zGp+hFS5A#9>Fzs*KfwL6#lSnhjx$GrMLv%a+Ejhbpz_@NB%7R!GM>51;~19CeiRF4x+HtdYX_^@t)^l@$b<0VD_dZz z@Htut7NBC*)5%Y^?k!G&?4=8@8p<}1bZiqXo_EEEW*BaLX~Dqu;zGKZWbMUP5XYXOIn#)kaXP4A#;&;oxtNH zsdT5#jfD)wLwm7Vi_me@apDpa*aT?XOqFrc>@c&NAUtWS^@?3vdG^uC2z{F&qfsra*%+fq_*KR5A* z?`h}87bO#y*wEcJIDZ(&B7NMsr3=kNt?aWw-Z_1>u~6C4cU1NP(}qNL9~MPgJFkOKn7_CXJl{KYaVT59sk9pS2K4&2m`#pfsA7H~Gd9Z1r7S?ej`W2EK=R0;EiokwxP zC>tQQs{Tcy!06G`h?zK1zXH=_LM+6*ErUq>@ohu9^W2fHq*`PKucNQx$~7Yc$AIk^ zdVehnEczydA(!!^Jyt9|5(o0!Tuyj;A%Y6L?_$KiHg0LKdUnYkGT4y0>oeUUPXLZV zcStSWh8lI`8(=4zC1CkkiM75fs4eR9?#@NWYo`wwQr<%g`y{LwJ>NsJgtp>d{IUi# z%iV|L{mKyo<8-1OeM-7~@a1}!lT!e3Azf!^Ng1JcAj3lRJCm;N2b@1Vz0G=Xld96P zUKs$K!p_xBf88xLpRR=va$U_U@A115@v6Fn8WsQ_;10_#HbXD%!2pYn7p3oFoy5Lz50Z@ns7QfUepWF$dnM|%r z-x=&YFs)jRXhic`cm-~!)kvz!Ha+oeeq5jZvORF_lD8+F9kXq*M4ti#2ue+(#XR<7 zF`7d85IQ){wN7rGnr|;C%oI<4q7b*0_uRqxhtS|sy-+tUqsUO=Fd5o2ntBCf*@&vj zezZi5ny48eYqaJ;Mt-$b)$BMkr1Vtr zm53`-uhfl;Rgb%Px^2uPK~Hv-VhVL@xEtLJS?4Am^QO$79{chVVb|1(YrAHGKq&vX z#K!xyS-`SZ@)AW0x)Dp3x1@sg59ZpPJ8`neinF!N)aXZ4OGgp~Q*EsW+<=|@x7bAY zc|@8{8m}I_Zb~;A;W8|smXZd$aiZ6W1 zi9vbfZ8}Hb_Tf1i?3J=shbN)=4$;b`f9A@i)!+iR!Z1IvBWA%L%thaF-4lK*a$|Wv zr$2qLVNQo8grIc4lHiE!%rqw5Y}erwR__;Geda3Amh35Z>1wyalMhO7=o~0+DtcSj zPe&V+KY=HsL;|jKfASn}e0G^W(W~pX-=8BlFJZPn(D$#1UOardh4ns@3`MCmkdyd+YxoJF5g6z6>zTT}wf(|L=2{FLj=9%}zy%aU= zw~%OoBAuYFL-O%fvDM4Rpr#sxTwTl6JxUbU=rX*9TTU7pz$DR#ib z+FNr6)=T!&rbOEK*|=nyua_n}Eb1Ds^RRZ!9e@t;@Au3Cv;;VdCKN@$KB+Cf)16RRrXC3OuB*B2b@MBq+E)fE>l49^J*>B0v zBtv14r*AY(26)KwiE9`)H$5?$-On@5Xf*p)m2uOxA`KKC92KHgoS4xHWC2JNx8I=N z_eb)Ztg~Je!v4plM(4 z%m-?mm^5&zcDVq5V=mRwaw|sox@^?t!}%ADp05Xd1XB!i>u;&=FO2pVryBB5zv90e zz?Ko59}HE-R&u=q#N7t;K}n{trXF+F{p!KJxp|33=f6Ltvs*p%gYe++73kWeJk`Q# zzvB5K*{5l>mTBV4bxpSvg&tq8m(t4}2UDtQ;FWfP6EtjN=dS$TAc?)?gM16w6?-~n ztf(-&)gc2pm3C2rIhwF1n?RaWy}56byR;;GdTiqbW3F*aKEPpN&Q~zW21l!qn`g*Y zi;4H%An%wZSh{e!1qc_>2NYq`X8bFtK2y4^9pmP1>nJv4fod@CcuJXQ?Yu5fAIQ=p zM<^fY7M>W{nBB;F*KRjVR4oVm&IO@v$4MH?j;Wo>z_KPTiw1Iy68c}g>MVWe#}=!h zF9_BL zG#2CQzEsd;AsZRAPdj($N0B;&7Mjp7r8>1QPI651n5WwqO-1nLPHC8Udwzh#9Z@T> z({F>M)zpkajS~}uq}Mn9#7Vf!2Ux9EMHbiz#bSzdsJVnuW@>L2j8DbDu)r#%JScxu zY+5@-M8dobbNRAxVO7-`wpF9^7z#hxD!+e}s{DS#f-K+t&EW^;>hd^9eX;QIR*=N# zxmWI2_a__`TFJe4Qmsy8zBSVQ4vrM~<<9^K`fGr+HSqTUsWS&u`di{eiI8HksqNAw zA3BA{sv6$<{jk3qd25_E)I8^ikj&N@b!)le-+_nX>$b2oz6xSdHi?POer($V88+B9 za0h@2Fmn1Mi)1Gu=bl(bzWU;mV%^ZF`j(CNz&zJPq+oY9ix3WfJMKs`^&XH*-JN=1 zkc^2+*pwp4sYXtzVVkCl)6UJMUKc&vuM0wy)6yi)@|ZF148(@C76Vi%Tz;;gca-=e3#Qf=s@=`k&Z8kEcg!EXuAng-^UO+!<~#; z_-YC{zinRPIie()U3Ap^q9YIQ0u%faRUhx24OAEr*NsS{%kDHnh_SdNut0!ipSAS4 zcb;QAo&V5Pt9mmQqJ2`@uD0H4_2Wn=RUxJN&-?krVaGgy?OV zwN9OL%%X3CeK{aUfl7pRs!*dS7l2l*qtA4TQ}ZkRibTk0%-5$-9^{p8j;)rLrW>;x zZci*av2w4l@tjf|u}^_T&c|BPIlRdF7Swq#FoUV1(S&30(`3V9`>U0_MkY*CYcZ}b@S(x{oLpj)5t1(8^50vmVGOjZSyu*>s7G8FX z>+4i&Ufkbe?Jj0fbP3Ti-#n&&cMqC<&tM(MNY47}L_EKBo%v0EuiqrAGthV0$^E*- zg~Xnl>J+7!PPH=yRxfC+d@2ZUjnJ3YkNqx{V`or5n3uJE@gggZP)yfCkH^8NC(naK zI1!(wY=jKt=^`MRqvMXyGMHGIV8947qk)~E0qhUxAV6lB5V~r|Tn0;65Bh($--N*PUUgZ=OA_&vSY z^BxpU|5dXAiu!d^W}ey_T_6?zs^O4dwd0$=0!Id(&LLM=SgijO>c%Y;E?C4SPhQA+*? zsptNWrdF0d_71gC;$WJ6vHp+66Ugk*3ssHy;~RsEUaeLmd)w`}&uTmGQfA2Rm; z`FP>}dDbiFQdoGXmwO;P_=fQCXIDESc|>yei>8*2_L0AyRsZu@0|R#Gx#(aoN$ByQ zAVfGj^lXGj_%Fw&{elA6p>|%L;gT*|I+Bt)Y8t9KT1UV?Nl7(rU2sBOL-m*ABa*J{ zP-8^sC9hCPm)+ML0msKB|L)PA;eLo9@N7w_y`2U7?thXB-hP+8^q^4a2|Zb@6ZTiQo*EN5C*LAg2)&IhDt-p00kmNthbU(ma zKljUiSG-h0y@C;8e&LAF->_Qi_xD#v^RMhx)6o6vcWqtG{}HTqj{r;#3itDL-yKoE z=CAhe@2|Gz-}tMe{+B*VYU^nJXZaf(;c?k7%vZ%-CCDr4zhJS>@9(nK-}IQen%dvG zto=Vjop~Yx1A%OV@f?ka2vvzfga)XnsCfFi2l;q~sf4?G{F>{!zn=t}8h_=wy84m7 zkw8o1Kh5|H5+rV!zbY|H^JP^`pR`X{i5a`5fjIzANGXNSxKxeit`)k%{(^ zzf)prf90U0w%UJKi3K7;cg<567{{JoVZR!;>c1a?X#w!@7i+AmDJiLSRQo^6$IE`6 zUO{1Azv|g<&5(_|j~6@C#yu3+F3DX0X2%Xafd~o*hhdWHe?DRN7eYy@?;37!$uE!; zT(J8;l^rTlLXuX}8F}U<>u+Lvkn1#AW?>-W0f($o18}1(dr`yfWx=ZfDC%a#MvvCg( z_X-W#eemw3rl2doyu?&Ta@Qb%wwmfn?&{^;Z)$3i;O{TDp11bcMYg|OZV`A?a#wl% z^3cCQ)L;6$+YJ{wW48vum=>FAd?N~-mavoj zWiWw{KRw0uFBte2ivNX9r@exF!hI#x|I|CEso!PLHA$%HWzhALUY>|cUb}t#^P~cK zcf<8KVnv&)69*@RU6s3a$%(gC;fRHt2}fg>B;yqYicQ&TSAMIgL)H|jFINT)E;aib)c>*bekXf9Z+JuQU1TGZyIIJt9>rsHlvple=^ zK1k7gJ4`%|kl>#ARx{>B25BK0CszlT!^Bnt&I({~fx%w-%-FvOk zt+_W!?p$E;$J2ExCdG zj6bV+u79+rYE}!@9&r5Z4X?FF39g;7Jk8W zdlBT?_j$sXx}z}irD&-;Ivjs=l*#KH>K2z(b>wO;jxq79V0U=4>QCnOPwtYo2?U~? z-$joH$~W{zWJap|934%!Pnq4}YePRWZ%U?AC|10iJamvWr?}RL!0J!Tu2fCFtgC*? z#k~4hF`)3%Nb0nS2KKCv-de>6?FCem#!cJu6urkk&Xg9}POi4`)DHEqMJecuk}_Ow zvz_u3+b+;Q9l>{r7>l|bGvxiZYsYt;(RyRX@^d4@b0_Xi1L*69{v1eWj^H1B^ zyl&&>pXPa>D1JP%C@Tq^Vq`_dzFod_^EpwZ!-BSv}uzgJ;LAv8~E;rIvU2w5OGd`tP9-VndU< zH{ORREDhWccjR|`Nmny0b;+PBB1m;Z%~hTXb+{?PBG#O3z?R7uBs$ z8$ad~PwJjkDjV@%UOdD5^W$iAV9p@{*UwD1E>F*-4fHxF2iuk~xL>^SJvuP3-Zx$& zdi(3Q$A*Q)_nOUXo?pX6*7CWp-hcJocJSTCjLoW$VpRQ!JG_S*7OEI#VaWPADc?H7sBt4(88@O|1n&O4cb9LQ}ra&hOu|6=bwz?$r~ zM#0degLJ6@K~aj>0Fe?9DT;`S6lqaW>0qHsAQb6MKu}Q#2uLUN4iQ385Rnc+NwVtw#p2WA}j3?IEW0Z`0 zWAMUUk3-|_-#xriE;xGI@kHgbD!~iiuY{LP87^;eFEN<263#(&Z7-enehnv`)5Cu5 z>3eqC#lbCp#XR0J7x8%@a8;A8ba?m(%e3M#mqrPmvZ!M`&M7~PO5j4=A#0M&jb8>6 zl7%K;q1pV1X9Zg78SR@CPJBJ!xW1@8JoJ^t&e!{>Or8=<`6TTFi_fi-i$^p+TGdvT zU)|PH4xcE`G_4$}I@$hwXZAs_p}l39E_14-CXKdbXNTO8h@D@@1o{*Q`J`C;BiU!T z8>3}8R*ZR?Ki$9d_%)to_O9qJ1wCzSwXOk&JC-qlr{u)a$@^H22Y1^mIKNoC`KNi6 z4G})yjH?5d%^yna=3bd8-=nvL$Jz-TmePN?bgxzwo+95cT@^XGDlS(t z=ezXOo$I>WZDjEDpu%7n%a}LI%?!)o{_{NCFC_t7Y84YaDyxcV!`MUDHKi*2$Dc%c zT$5P4dR#O5W=E56&6+e{MnGffT1bUwFAsU?LBS9c6q-HNtxipRgJ8X=h0%h$e{OwQ zVtj&Mem5}FzVnXA+bi+&zYfZbktWWG<&T7n4Yqw5y~=+06mgMr;!WiV$PUAMx5;DZ zqJ+)&1BL;cQZ}wP#M8?EV92)Oix#Ma&Dbi-&xQsU&ZwkK6~5ih@_11X4@t=I3YAWc z5C|WMSe^?C;5ZW0B%PXUA;>Y&6smGNDC-8J)XB=wGN7eZbkH&9{Zpj|9c|%{oB>6b zwypohBDbw8f}Sc__u@`#3$ypPxR>`l9t#|El#xho&HgO%jobJ#k1=V=picewyF2|j z#yqU#2mNw%h{_wnyIv3D&7pC*O^X)`;`1@cPcdt>50!mPJt-sG6*RVDIkN1rQAZdf z4PxyKgZQd3_WKx}Z+`3J2)iKqbj>J_f9!rXS(fk0Yrpr^Gqp6f-j=DPM)E?7bIAm+ zHA4dv+wBv#1sym;p$AyrKd;&hoQTmAzo}Uzu%Wg*eIVgvTIxXoHCMgo*1~3ES3wgw;XQXeC$)zvcj3-%PANe-OH|8{#fflHbnW!Akcj2 zP({WbAAh5retDQ=EIj_Fv_}02H;?jsE-paxaH^Y+s5g*{;!$#t;JR*+MHvd^-Oc)p8{^+#(R|w z$)abqcLFvpI{!#pYGq&5M&%bfGG3;Wg>5t@n?AA^{#Hps&u6Z8dA)^SP~FlYPgJ|7 zuC{wse`>m=hYWCuDbG zX5)9h!TAfeO|hJFnsenF7M?X$C4K#Yb^$SKuW^aJGh#aCSu&b*-arxh>~^eK6b2GB zQ#kM+e9?dCnEQI?X|I`Tg_qc3^2Pv z;N${dtn56z+*~~SzwbX}zx#eK@P~_ym2JPpf91DKIVmJ(8EFv;0Iwm$H^+j6x%U7>6-sR@yzb_~( zDz2)osjaJTXl&}hc6N35eEifqG(0joHa;2>p1TQ4#RJOCr zp1DC3y8C#LLs;%C{s?J*X@9KjKew>3|Dl!rePRFSx^NIKCI)cxnD`(t$nN3TW7&)? z0`mh@K}UQ~M`o~xLXbJ(d_>!<8noK}Xn$|K&K*vsGE({i?9{M2K#LZ7%dNe7W`KrHstba7CmB2qIb?{@HaPrc|VFT{ejX>cv| z7%2ycm=ou|liH`A<<$3f!970nA*}hQuB4FAuUUf0LYW&DSqom1r{ufr*5x|BcYBFz za#ee7r}c2Q)R*{zKl|LnycBh&FjH?G9a>|H0P)wnVe1^_6;%&8pTLGI9NHpil&{yy zH^hB0s7qjXQV3Tvws1HdAPOO$R}2MMDMsLRtytn%PN8#2(S&KPeQRVv!Ako3p&ipq z!^;4Fa(s8@IU1of@Y=OXzJ;*^-dx7iCYQA*YD1EQBfI}J==#Or9 zocs{BcUYZosHcGyf8}dnvmZj>)9o{T=K7!*= z^+Cudp%`K=d6HP)1&etETridI29q1GOgG!Rkh5fmpjSO^9AU0*Z^--rxyr-CaBgmn zX~G$O94MkV07(4Hgmo=dk6i!eZn1TgXZ_OzROD85@UGp7(+L{iiX$S8?b2m48)P*z zya@7hWoC1CsmAucI2G2QSJSH0;$GQnQyfCw8z*6(s>S0TGMvliS&7|*KAWvOkx7c{ zw6TAmIhPdU`_#8av~;_K#*jUHR5u<^o$f5IfWN2;=`56Vize0+2*_gksE!Izbs&^# zua^2wHNQ14-G*z^zH?JTYaFICQZN6*2zUDJFJ0*6K?e(xKDn8=gH3{d!kd`NcLkp! zsn#}4JGiuo4|8wf5T-j0j*ZvVeIRx7Z0Ei^D3hy_&A03OO=ry!5 z4)^P71H6@MQ2lcvOm-pzvQ~&b2;8Cs`JY*ED9gPCz)eZPwMR=%#o`Gyktvdg*riVH zq%htPx*KmKx4;VFbC}irdlXop8C9SJD;8W&GVihJTFeu%i40|VCWfdgUvRBf>X*^= z=kh%^JuWKRu0e9$F$0b+tdYFBpcL@7)Ut>m!#0ZiEF7o=IEu=Frdh1b&7JY_>r*yU zIRSEyg>Eury??z#8UGH{>Pf{6S^{|EJsif;rXJj@!!@cS4Oq|kr(0(QHum>LzClXAg+6^rZUyfraw3F) z;}j*J&m6=dbEkRhoLdBW5tSzX*N$OkVCC67=in3dq@!sp+X*9nYV~YV8V4gSnVo8F z=|jj3dKg9-90#W|1+%4P>o5Z0?N1jwUs^dZfNQyT_g1(DBp6~<3}ysDy39t?wVgn!&lByDaG)Ue+Sdm8;%C=_O1{F};4w22 z!~#MiMb$Y2oj{iZ{A@@^=nWY^l3YWkj4Odr1MKy9sX3)s-g9>D3%$sBHMnADdn*oN=NdoQ@`;q4oAf; zeGczp0Z6nHoj9)GlV4=khLe=T@u2EyrhCh0e)95%q^-9nOr*5eH(xdw@#xnV}kj z#aN%kruT@y&M?2)Ctsag>ftrtpr?gXlTHh^*u;;!A=oNqpWZqUD_G z^&;xAvF_0Fpz?19qK(HfJ68^ET;a*%%@1}&wjV;y6$c-nf#pc%UV(X{X|oZ*3JBd% zZ`*078{?_%IwEgwC1KUjMRrxDLC5YVLV8Q_CL$B+LL4`r^G>4v3Fa-}R1je4-y;-r z@SZf78;qB6_26oo5J|NjSE?dwX}WT#L{Sm)ae3uq?+2m#w+#(^9gwXMhd&V{>Ca~= zB%wBpP#d};n16<6?h<8iiSm_Yk*QVjlp`WKQ{#MW$#ltjy*YV$c& zN%de&;-ez(8&rTn0E z=q`h;Y6Mu9<>DI^3YZ}wSFGL{@(v7r`p>@6OE!HMzRrwtx@a6RG{faBZKIdpV>08^ zx=NAju`!`EfkEqCbW%;GFx|wq1f$?z*@!4`zsIP-B*|6`W=5&0f&K>R=HqE3N%i() zXzln1J&FNBc%%g;@LRq5H{9u^cuR|!U_!eO>tU_OZeUhDKXS$}xP2MW8YTKs61-yr zxT19@mq7o71TKl2(R+S*yifF$z2x|&KXx$~&{TUAb_tq~`lo@C=Ywq9vrg$Yy+FAz z74)5diu98T3xR29IlVUmFPKZy3eY}qW`A+|L0fHoC!7aZS^8=bIVe5U{KuPK<%X%o zIWDiI?12^DXDz2Z=4N@Pe%8g+%}n{Wr#xzmlgi7=+bPxN?bZ<@j=rj|YTUddDbn}4 zIdvrLX{r?x@8S&mMcacs2Xmj#J2SN--S!~AwPz=D=^bLT^i4((>8vl9ogTRT z1@c6oN;tj(BAIuOA;TWlDaA2_xK!w-R^d{IYb&kH-`=tbFnQAz?-74V+VqRy!{(R| zKXs+lDbiX8g9m48Vj51BRRNKc`LjEWwAXP0`w18bs(q-5Cl)>=V)`h80|!ky6M%&s`A5u9cHTmr`)#Jst(ZfI@2ww{0Gj9$T^ z@0f>bk*~v;WFQO5S&CUiREW+&f8Nd|+d(a5ytb^>l1Tc~!}_P4%EzqR?RL7)=t{vU zkNs1PiBSYEiiWEVVLC1HrCZSoLAh&6L7!9HpYhC@hYgQgvZ7dJMRUoo#?NlVV(O3| z>=m(Abx+IDIdF$n_03zo)j?sM28WIs-taAb<{`SGE7=VO06HTN+@it(3h^a8aml0+ z%t5B}-X}2dW8P8!b&sJV7W$%A%thZ0g(lf-v`IQ+xPlM9$;Eq!=n_{Qo_mU49cueL z?3jHwnw_a)S@Rm}XB00&vuqhv=hW@N4_>|Bz-q&-NAHd)y>k;kZf;xU<8^!?W${q` z3!Mwa{!5ACLKs)*pDTW^Rim3QX&QpMN( z^^!CL+#F#zL>EKA>%&xpT*O48Z;Yh!k|SS|!`Zcc(=S+Qlg7}1(G8+}h}QYD2^__3H#bSst=dnkc>2GfBSbG%cu65#`#__w zwFzd%_X8E1`&GVVjg)wbOFmDIT!JiUZD;dJ(bBYx3aF=OWx?9ZGxBZKL$w4)n{cZt zx5;#Ypn@8&#Y2xf=&R@EbnmM^+iHY?E@U7(n^y#s>z606E)?PAO#`m$pL~j3`LFb5 zi(1e<_rCMvh-4b0;7PtQa@1lQnAy*zl21n-=#Z=9v2C4Kw!g@1vT0omPyd}&AbApwng^3Bn< zbBII4f_U$Vychn5g9wY|KUluoJyhI6(S?OUi2_6Z2JifLR3q$*0k<9yk5jIJ3{#~|m_ClIi9{{TicXj33ZtZx-Q!OmlVeEK-4o^ziXFY6a$TPTL02Hy@8;@P9mhX(-;(SM5>seC#<%3*AF& zweqn(Nu2&m*!m2^dX~t_yp>ajPL!fc+XlU!h^{1w7g?Gw>j^S!no)(}bdGCCrzAlB zKr@MR4%3NdtB=jt0Lu+*_OA3;*uKhdO1OWjn|{|=>s^n`RaPxWRTDagl^Sxc3|?_* zz|^U0cGtZQeS^3`6+n^Ai3MP)(%MoTcEqtr0QRaJp~zHfx)ME@c0tpFNodXC$S{Is z!Ay8rj9!`Re7>#~ZZs(g`LBCRE@B zi#GD&g8S@8y=h__T~Y$X+U{f_LWeFc#lZdW^NvFa6uStS$4?yY6Dedh(5eBGw6_Ps z+$d%-boN0l7mu8uIszFT+JK=@kVPPEwEJ4VjaP>lODY7 z*MZO$8(hlI!79#b;TOyXRc6z_!C=L~Wb#``dn@ca4^B zFdZW2&u2*j=^I99OBMO(IQQUr_nVrMS|$ z+olhfL;8-M`}X~>>*4I9PvkvGIzf>X*3TgkJnytZjRv{b}|T$Xtdc* z9*c^>NjPv-Teh`}??D#hQNIQ;lVupd#)g*)8am%aVL%3GR+c0GNf0Fc`}=={&#w)f zEjeoxukyemTt#+L1g!dA35wM1T`%SK1cpC8J#M;R_y&+FS zXNmwy&q4pF4lCcMBC$x2_8&}s2UWTE8*6}%GNpjQ_Hm@cQ_lggsu`Fi5qk+a+Peq& zp$)nO6)ON%Qazm?OoC%`H4{V>EM0)NCSaCy3{29aPJs~u?<8Z!3F2xCNC#iXR%ftQ zkN+=vTo3)0<#y$qD8dxKA&hfy!-yd9hlg@zmwtlwQ*4(DIX;(#wsQZan~>*uLXW<# z)G+)@YXS9X4}BTEEc;JrgFywF0ozHm!CNCtyAhz&MXs2;w4DU9zUp+tChg{+bExrE zxG@MD`$A9$>7nbxiRcX2?Mjpg;_4ox!HOQL9mH2*83GpkdDaZl3Mz>jiNPCDkKmH1 zROEM6Fdu(T#r2A7EPD{Zq!#)cOa%1NjV_4%hvyZGvxu`m{{4V|!{Fa|@NZgx6aOEA zf$N0-dwO7vB}(B*a%GTC8ubwl*|YD1qGM~i7=Oc{-`xNbST7_AOp-P&l&K3VRCX@hh)}sP+vgNWPvk?<-`#WE!Pvtue1Hh`IX;peZNvw6(KJkq63z@cAjvX zM+2VlDOq~F`5xrV)hI9(8^~ZI7im?nHwg~ z+W}Vet}Te}!TuQEBA9e+K&QN0eR1M$Sg!EznZg?PEaM*Dms7I{`6&LPe)uu6#48_p z23awV#Xy5QXRG!gp%@Q%WP)E&7bpWAz$}eoYMX}}h7WxZdIO;oK1Zs_>)3_O=(011 z@Xn&xf~SLdNoc~rJZ6$S)Yo}@!Had74@e~cu=S68bEGD6f+bP!`14QnG%3A!9`URL zK?QfvJxL+RI#40g0v(wwQM6ngnEwqp0ThXo!2?cRmV=R;a_?}GC#>obPWOsQHJ-4D zv?$l#QKq2CitxuQsH|b;k#V>JF!g1%5iJK z$*`f@EPB$u-;7zeOezK|#=rM;p{F+H;Ss^-=^aS%ZP-B|+>of=HLat<<3Em#d!lq+ z>uEUj$hyO~%0M$aQdXNj)rF2!WQwp6IQ9HyW?`@CgUp%G76)P^nqfMVZL&V4=>B zcQwDV65eAS#3{FeC;c!W`hoNpL6@inoEA`$MkLvrL24dGdetrj#5rMGYwBtphp%Dq zkhMUfOzIHk2neMzTXGHnViGeJxJ0=Irw_HMCD}LGODV#5T&h0_zaby@nCMa-RH482 zkx97s;!Av9@3M}C=&li+qmgo(dJ**AcbiS{|H3*bjZ@`aNcz^XUVPRr>YG!xt}aTu zGABP5Pb5PcUhBG(WO;ssz>Ia|5D$s)_P&0p*{&HCVo$`nmi{BMse-cY&9Gk6s~){= zH#udGr)@5)y}xSj|J};GVtfHbm8c~?U9zQBVD4ucXu;?Em5_eUXs&^ zULGP|>+5g=XXBSAWPktP0n2+3FN_5B3b>~ig2z$S>ap*ERuZfm#kwtu(k0ymQf&fj z8&U_Je|q#V*WE+9u9;g(@X2jn2WO`3f;)M)EiUz32Hs#{Vp_HodysuOZfmo4(2tIJ zZ#+X**G0;bxWs}Mx?JY9gMVL2obhTofL(t@=T@JAgqDKd6ejcGn~s)^`jOm)8&kN%4~NpD2`Zz8ER1_~)Bd z;WbPSdXz)dNH!a4>bm&Wg< z6*S^#gRI`^y3}?aAxP|rmnYII@wZxx3VIN`hGl%QJ6BJfS-g_RSgsjwwTJ-=s29%i zV_XIRbxpA7QR6m+CNLirvMr9}8JOo*(06>~y&6z(uF+hI*^_9`9Q2AgN*XdDi6np~ zD~lo!PC1Hf$Dr8ghks^t&E%5gd&uV>#0a+*LTd-Sw8*~(l&1n-^Bc?7Kg_REZ%Scf zKFB+g8JQ4?VQt~1_^-@X!+TUhGC+d|g;QBb>i*essnB+UYW7vj)}`oE{>9X@igM|S zPdwu|%753bIo)=QU&mcW`4V?JXXB7Ws|`RQsT&qK1{Z1Z104t=?c#9>C#U=ss6U&s ziXL(I*=z(xi0a%+zVh3nw=g>A^ye*xU9eE6lrx;OMINl}`Nn2QOEbr%S8C(>$cAi9 zP3?_#y_8#Q1}v|2KS?~-9HxpcD9g^PM7XQY@~&j~HokMMDChjO^qHGOCuSC!G8sbsECCn_hbJrmG6 zRg>jP8~$M=N|%7TfISZ)m|Fld3L_v6ZzL@xYFY6nv}nGcY0qEv>wDygueF}t5k4k( z=$+Ted;G}Jf_zAjBE^LMRm^OynrMEDG}ibY2)7!pc~GBF>a8gdR5<-fyVI9s#3dcJ zHgq{$RDwUy>JwOEi)9V0a7sVoAE*b`Fh?y!wQKSUUy&ZFMT_}Aa(9<=4?%SES0Cj( zG0(*OjgufTKEoWroEoCI3A!1z#xX<(UYrUR%A|Af-3;Oc;8>1y23F@e3O&wqW+sSC zx_|$v6(`<%vXN)KAo^<~gm$09IQPzbmnH;gw6c_PSc*H39c$N+xd}NC9QHo@fcshZ zv%AL_-f)X^>%>AHU=6Wm??OaDVLQD1zv#9yO^WA3kk{Z93hShQ(D0gBTM?@i2p7;i9|h*jeg21u3$32oDucw` z-a&UxV@Q)cumNg}5XB4pE4s4<*>i8A2c-RHQX#Nwn7K?i2k3Azp?A}Hh~rcREYX~z zPQZ{1ksT&`5O#U88f7&G209R*1k8BuK{Sgdm)z-GR&{Gq+hA^B;?<-GBr$&v;^@Wd z??e}}q8>L}n*}waE^sdFDpWbt#LWHeP84Ym(%7=0j?5fc`&EZLpg83l82o|(Ov5i; z7y>eOaXQZ)h`;Eb~UnX3z2%L z{BSyZS!r-LvO>6w=r&*CZ_=f2KT^x?g&$hXSTbv>Zi+qk`O1UXqjJR+vYr};RLPzL zUPB34v-yx)xZ6Y z-La08Ni^=u%d5osCUh#Hs(Gmb3*-TiT<-teoegPY1FGlz5`#r%S8l9`SyAOrtsICM#LMsqBwNdw zc{t|;)d2{aR{<_Nk%YPyM=a|_sG-b|8{l`Rz`d(9jKKL$?dvoN+%z-#4Y4?k&Auka8pc1$Yk2)a6W{f>i}4 zfF*|eq)r32XnV6e1wdW@9g&CGe$`nTH=K`h7;{%SFWhYu4@*vdJ6^g54l_vrk5Cq^ z!uKF|lcrX|@|T$^Ym2!+_}r24(5sm`p&s+U7curW!&lxse$~N@lXQF>i>OmL$79T+ zoix8eIwT zJo<~m!hd+JLguX)w&Cx1|z+Fyi&0KI_Br@+U!0ZVlDG>R6`+~J@-+9F@#YO@E4 zPDFbh4fT2Si1DIFPMq88?1=F7gha;VFpV{Bvwclf)<22-lNQ<%K&eYAD>!z}Tt z2aL|KkAV^ZjM&_1WhEss3d0Kx)h5VZI~kDh-a&~aCplNn(=yh{&@Q~rvYqunTLDl8 zc31{PNCWeYq=EQg0YJ91P3Z~W%NrwS8-AoO^;sBGzZy>ZzAH|^R}{1AvIHuree&Qi zl5mrFRA^ygrx5x9;w#ATdxOY?SEmY|!Gq<})aMj8_=5X0q9Up1(#cR_%D3_&waQ2L zg+~uvixV(iEVOHJlu!BmxQBNhN#*Zbc)8XCifLw&SI_KZU?+z6BanCmDFB#v!(B`+ zy-ypvbk8XClE*O9G0oh2P7gU1h0sGDAZr!Z#$1N{mYqkhrIC<-kkTWBE^vobZ4CtG z`Ty#^6H(Ul>~2$#yqR@gSm4b&k0@h0?ywduVvpy;Aw>0LE!#c}2d= zPzfU_*9>sj19#mTy-wWLSAXUC`D09kIb-*A!RP1}CuW+*UpNBQ0)yYcY6&rzTB9|m zc_f5-l%$r5hY9+>z`CiDbDyZz9R1ipx#Do|l*!@O0>?MeGOzd`g{-luB3PhbAU{SQxQFHdN%*#8ABP)O(A z-YNQvGcziO&m{c+rMquNIH719v#Cn;{>$0by#>~rGQFxD7$`>9P=+mx!FCLsuJNuw z>Fne)pu!Og-C^H@e3eJ%k)f_&VHo!H=9W7oBRU?o%hDC3ToE`HVo*V`nlB)D;W3i} zpiv!$4IUPO*GXpf*Or&DMw~$KDpk7mzI=4z`QbMIFBvC$etb%+_+xXH znSN){-eO!gCOGHe-S*-a_l=AFI00hB28z4~;Rk;oAW7BI-c+$m!M2QL`7iyrT-mjW zdinXAzd8JdBb{w@1-1#Gn=$t>fQ(bS*Cb~*Se}n+J4bO_g^MB-C|)?9boc&wRGsxV zn|^XWC6`+VAk#j)(O}M?z;5qk^McNkxfu+%tJHqHh89)4cBHi2{X_=0<#&TedYrb}LH3*KEf1pw}TOHIDc&Y-ZjJKP5;UAK`qR#ThNu3^c$7gKV z*)w&Z5p?0&*jtCy#o|hSTP`DtBp4TaPtbCV<<~vJXZ{E?O~D<>jG~I?E;P>7)@nPd6_Cgc=+r_AV!UPlg&?8Y+m>V>yrf_zjxQc~?usR@;>@4&_)*#79BZ+{W5YBHQ(;B1CmO zEDT|QcFxjHvIF90%%TuNJfTWiiX-pYTk*mg-x9F|Lv#9@3*3j_}f z8!Z}PQ08FkV#A#jSKDlod0|CQsJ5kgqT_i<^PQW`j0emd_e-O)Sk#PoBLM~U+S0-X zVB-hlKya4>?W`NGAOP=jU~h~a@02=3GJNGd?PTO~twmYs$Se0WhI>8_EFr%f?ohNqbh;lb z96Z`~gjknG+_?a}o-d*rc#n;zfCbSnl_8Xvhc2Y7JSjOuNXE%4aGnvub?M$V^_oZ` zn+?)M=o1d7fQTT9KFAWjgGNp)Ro$z=RTwSx(nG;U@ARQ^E;T*>8@{Z^lV%*X6%#<9 zyPtJ4h0)o~@i7EKc9@60!&pI6%DIl(&M>+)wh%8}u(L1pK6=oVBDn?KKKzGOp3oTvhyow}id$aMkm zOvcV?h{A#x2DLeoC@HE-<M6)a!^=+zm?_ zs-ydE42E7)qGG!UjDt1gJJBy9k9e|9SRY%Kuj`!*k&bTbtMA=xheS*zp0_mWLQSgU zkv%A`Em9&>m#mi$m*IYg*6do)o;wvAu=$Jd0?Z_mwY`Z_Phv%Kv z(8K`)Aj)NaN+pWUT}@JrOh0Slag|$&ZM|&m0zvpAUe7vg@d6KH%K1mu816mD9o{e` ztBy2O2r$FTW0Sc3UwBRx{mhcAscnjTC{uS{`gnstX2X|&{-@phP>?`(^M=lF)3RGc z<)gHG@Nix*Oeo#pb*X9`8`^`~^<|u0TKe24ERW$;ZmTw0mKIAfLM9wf8xNx@`_0+9 z-K&nn4*z`E_uh!Z@!s9(7u`Y+PoC;ltO7lz&P-o)4$wcsI4}<`d431{h)eAb>{&Es zKl9}#E#W72*%}<1ha>N^K4+J~$z*R}64k?MRh~qi7nxA+wb0-WOT?8~m ze68PE=-!ICtNT60>}V4==tv9^2LlJp>%`Q=AfD}p?N|2)WM~G@h&Auz-pM|&Cg?6m z`2SWjxMuJ)T)4$&4l0IF{o2A?*8Xl9=pYBO29)Jws6YSYnk=j$o@Q~n&NqH;>zvR_ zFnFSX3?2)}!PDf00w;fD)7ACFx2hyZW$tbp-QsXzB=sO|25Y>MnC zq7}oI^8iIqnRncSd>2#x0bOwrp+`=@sXXXC$Z?%akk%A`r~nqQS|geuiUm2RgrtTW zQ58_QTzafo7=|beqwBq!x%0pAn5sn-4yp}lJgLQnqIMey<1x_w?Eik(cjsgLR2So7`0}A&kXF$QUqQ#LF18lK7ULVb)4RtxCbGK zUaG_<*L#W=*waS|I*OpvpURhQ=bBN_V^%eRBmBA#x*bomPBn%l3Z?|yJ}0@_3p%3e z8F@vh*FJS4t1w&at02F0ZPiu(VCoOL8&OEwvDg|WE4olYC-1Iu0eDw()r+o>1X!XD z!j|h$R9P_8tavMF?a8u&_^Pvav&M-N_akvLd`*|`Ka36^NqNWu3D}@2nngpmY|ZF= zW3yBXD6vdO8ae0z1B`5uB#W_?tW(7GsJW?GzM0jgBdDmF37h$in_0WZ*2)~Orw1HW z;=9RnNshs$$@`$|E<(gNRnN!GN35UgYlHs}Ls3ghRu*IFuyfL1S+hVTpauZF1P0(a zJzmlS^nzXoaeH{4l_m_X>X}dd9z@6?nU)yD0-ad;dX4qX|9%}_0A^UJGoT*oFce(3duyvM$0$%l^2bAz5NX7jo)^?J9?ZyFmeWqY`spTUX_SHuL_5CDD$j zw{orMBB3^rMI_!50jfstCSb%Q&?!6*GKYv;Bwl*lZE)o}Q=q<7Uie2-SWV=k=8XST zhZ4#~d9E}4S@2aUX$Jlnb8 z{#MUJX~ON`{s!&uuk~#0gKY6RsTOtQ0EYge%OQ&P4%P~aHLAiuG;+(9mCi^00|`rX zWcDC~?qGe!DG&=mtJkIcQH#^i%l+krXutsr)Isn*UL7x0mH?tS>|Y>RQcNF&&gA~j zXl&I9=ih>{!GBsd)z#*^_}LJ~Cu$}<*B8P}FD$shQaLIQTv97h8~xOg8qYDhKaFf2 zkU;+lC6fMS`YV_kqn*P4cH8P&6)yh)W72^)`QH%Ue6y=kUXzFS`2^Eaz-7_jtV|qF zK8X@C*16E3+G0Y zO=#~v2D}lRcetEc*0WBc8n%I+nh2|Xo(8Ai+NW&)D8hjE5n2u%@|HHhC$I-eRTJ8p zB}YTMb}h7%cMxWKki$wSdh6I8WF$rcMODCnYyotiZw~y$A^-7dKH=AygEoft`Q64) z9-yE4FQY+;#q@nX0^0L^e=^i+aGL|S_Blh~RyL0rT?oF0`^MBv!lgm4EaDZ5!*v@<*Px7b}pZJ0rHf~lYS2a*U{e<{(jJ^UEm6tk?&un zmH>EAZ2uWvm!Oa5{W(h(Z=$Hj(vJp$fy^LK1grunH(cM)nOAei*Y+U0*M$P2)&hLU zUV;c?jc#N$Y>U05lvbc>x(8vZt!zdogXqLJ77~P_#cHzz2lNo#3_)Ms3uOCkopXTM zd}bjDhDAOQ>U*WXvlKbXim6R~ukAf*8TH`;H$jISMbUb+D zPh}!-eyIaBm3o7ggK;VW-6e3>8P&luZ`i&G_fkaIQtGhDl{k~Bzy{g0?B*EW7wX-E zF9!5Sm_X#7%Y|3)f`V$&oX*}%F$I$F80-KSAZ|Y1Cbc5m6gsHf*5&m?;*qBGNyCe} zVP>BXNyf^gBxg?Oeb<3TI>P0Ww$ek^sb zAGsXH5n}2aQ!Jz}Erw;M=zWlu*r5tS$?JB$md)b0vi&EAX7p*SHuhd)z{IU2yJr83o+V_ET-jR9#vv{j@*Es1{I}PvSy6 zAZ|y7I+X#RPOST&B8k`s|^T zOEkwM>_PZ2j^L3UvZfxp5jD$IPG<)mOxfw|5U{exlUo(tzMqQ_UGh|zEk@gi{ zTDqrC`EWUXx2FSBxf8u*1}H$|b<|7)->FAv=@C~=i3zRmOpcH6ujA-H=8e2hMRyt; zy~g&uQhHIh9rg-Y)16il+=V1wg~r1nEg}?SD)Ts90C%D}8*tJaez5}M^E}~o!if_< zAmij5$JvOJe{pdrn)+GE$<2^~TtF}fG5v&BD3qg((ZB^7T7!|Ie--X~RA4sW??_R> zW(c0LHVO!M@JfcC0cX6#Qp4wWV9OcrW#V4*S{VB6Jo3`K$BoD zUQzr2rW3c(4&w*BEW>af!aMjMxqbm+#=Sr0Ia6T_xu11!M}B(v@fjpIjNT;`)&>G7 z42f*lX}M1{A-4lbr0`A$QQ$uD{Xoxop{&FiHX;kHYyEYgAJ?YIQMC7_t*vy^ZMrod zHX$PJl<(c(LHfXM@J$Ai-&$?42f&xZy9-`@7>aRQ1TR7;sdPRA!{*>*i}Ac+)z$~v z^7D_ssir60+o>NvSJ@zbH$ulnJS(jA7y0g}8iMC*_r~uHkoWOhaZ}**FHJiHCK7zk zvpIL>r|=kxtK~u4;gKQoE&fY{u9`}ZXNOO^sGmD`H(d00^EKAc;0a8fWC4Mba*f_= zDYOBIlVlhr6Stm)xn&YCn<&sC_)rPb9J@`+BqA#JC+cb zXKO<}OR8}=GBWnZ#KvZvV=wR>I{PM1;?(t!V9!z5vo?rZ1~15By5YhPT87?WXJm*E zGVxP^-s$>JUkdM>>0Mj7CLLc|%)oP1uQxG|iHWR1YoP{G91zaq3pMZv8}*1574(XY zS{c4^o3oCT^+vKL1uN7!=)oudMnTp?u}1c-@#lt8MYANh6eynO&~3+vCo&8D4|_%7 zydut)+1E@>u3KbRrSDoVHpJa?QqPkuAM;Y=gi5EE2X>;Wj5;?G!B{se8tPwwMX|LA z1Q98A=QGFS5Q>Ekre#2S|J{_c!+8mWckG-|@H3w??3*9H6&?th-v~t=L@0v<#$N+8 z;{}OXXgr4B-(k4JL9S|4m3<9;c6qKfWr5(EFOzmu$F981c6)(k@HipNQpWPaL)W zF2Yyzo1HV>s+4L*i(6kWMLtLn2LligVyqv69{2vZQ`mp_o882e%g|(Y(mi)E^G5b> z*Mb(ZI+@SrFg3-WV`c?G311%?YJW*NWIH}YqX{x*&%Y>=*NImJ@@y!gNM!L14MC3x92YR zWUH=VDt10#DT?$yDPE#|e|z(oyHfv`dVkv|a92N`;h8J8C7ul_7uX3kzS%q!a{j0~ zf^FQ02I?_CFIp@^aia^!n)M>goNohYr1H{^B{pvmyvPZV;QREMM^w2cke3u9Zngo9 z#O5xj;Mp=wyl0Sb0_QatO&pzg`SSO zzuDY@q(C2NdmyiPT+CHWU{DEM4?lf+3Xx^rdF8H)T|r8zM5nrl^qc}p1_%Pl!kEMw zjlA=Ju=gHNO?GX%a1bOQB^2pMKtNQQbOj_fKtx0=R3R!VO^gaifItwWNEHwe5JI(4 zBE1uO73sZ&fPx5;NDCyyv;Dqz=AZYSS!d=yYtC7Jc_Ueih0os4?)xs+ech-i0ZALZHF+3ckiw>>RfeU7A8Uc;aAI_eHFci(1{bohjkrLkbH4g7*Q z;KqGsSLg(;+i)?Ns$q4XDOI0bV;ixFuERGIhK=1gUIrX`<{EE1*3grXy>c0LmW-qC z*P4@vr|=W&GUf;&e%yfWsV8zgz;XPJ;CRiJgjD|n+2Yu*>;-XMG`$WMlUFfgS)8iN z^hWgGSwfKl4Oc2K@9eZ0RJ9&!+FP+E6GM`? z^|;{_i@oGMTq}0Wp&85T4!YYaFTcxoh>9TDBCLjWw{y(gxax2k#rbv8I&EQ-D0{eC6G9Qma{FVX1p1-LRU9UAF8Vucv@K)(d6Rg>c**N&r@g+ z30Oyrm`)ud5Z|V0Q}98hurax7)#M+kBm~@=uK%nqI?GPo+#<32jh#~aLh4x=v57;5 zH$LonQYfrf2{y!}&+;sOCFgzqT%Qezm%#f*9u& zHx`fKt>8RG$bK-8(cje4u8)4OcIRhPFC#a z=`YZ2kT@k5_|!&EoXVX44NksH*pIPJSi6AY{_e;6k^D)pOCfmU!Ofa9&nLDiEO+l- zW_vsaJqnd!uFoeBtN5M0UEemlSTT6D8pCn?hk~o5(XVjgC&Q7x&zu7WF;6i~Uw2=> zN6CF%gIaF@?F3tni(3B$nXp3!OtZo{bP>5Wj+#~7rESp~9J%%+FS*!jG`+MJ%H3bQ zkUDoKl%I<1Gx0*yV}aTrw;c4Qn`pWZElo6y#&Ed2pR{!pop~;k++)?RcqsLa<_KGY z8%IKDduNoaN&R6d2;>QZY-T#vMnbFv&91mL2u|;#suZB@uND7vNAkLi2Tf}CJo{k_ ziT!ancKlSoG^TIcP5<>?Ou=KO&1UanFBgI%1~aPbFkS%*O@8C2LqDO18^`WkDj{%} zjL~6a+L^uwrN*_+3x)&v%l8JAOjlRa4iAYs%+r5dE1dsn;)dk$xJZ9QMFUGuKM+Pd z(zz$*81Z=OD$y{~9l^ddGirBa*j?=V0r-_^3!2_f8!tZgpG?wsBJyK;x{y#H5GeQ5 zgQ+wk8~S5zk-fWSg;orSHta&5hZGv8(I2k5%4m59y-CjwUr&;7IQqH8Q?K`&Pa7~x z@@$}Zu#M0>V5K}Xeu9D+&0%zi7QVC;%Ma;XqvNN&Y$(+Z-4S!i+cqH9gUL_b4`}lT z{P@zmD1_uuReQBD_9y6alO2toR&SJOA;yzXV%(`c;pQKwTBAMVj zwfElJWs5nyKT^C-dTE-OjBUV&GWe+}coR}W8Rccs=#n0Mu;9$@JDCor$8u$!W|9}B zrS37GQ1CGw7a~HN^Nr_F4Tvzky`^NXkQ{62bCrXM6%EN&{Rld_U#@nPyxT@Ky2P=YhsD3w|T65(|rt2Ysjd z^)_#c$4^fdiX1P%HOv&3HzR|9IcK5uB+6KVXGl*1<7DGAkrJMIhBRT`TRXoxdixnb+!0)lp5SjnaOo+al^aC4%9~ACe?e}LX=toP1)*|4 zw0m~VRoVJ{{|R_kl4%mR(Bl`(%oSI_I)0!Wj+M|miZOw+V`Pc)JoR039U~)RQYD6 zL7|AFBfD0xQJU}B;cH%s6~*sGgt3=l5rEFNhLjp5XDT!|SV5S>!W6ME7i@#7T!1 zgHCqEFO|tBrS-M62zP1MKYhR-oH%!E`AjU@EMndlN>jubP@<#jL;$*k+IEeK%CFsM zh%+9<@OqqVP4c;CrdjvRXnSir@XI~hj7yKt!&Us6>^P~BIHJO;r_hXmocmonsD^7166pc9BMf$pV1`*=PNyFKG0YvP(}?b z(9g9(Yc-DWn`WZ#UYWXmi=#u_D)ye(`~;t*)R_nf1UiBpA6mc`MPCUW-DD5AG%+nKr7#-t3u4%DN}_2u-*B_k4KY5mB8mVJX6j4>SQoS1{2M)+ z**U^Ap=)`yaAgP_v7siTm{v-~{KBVvSgy4ds0p6>?(ZFNp9t4S5q) z+cTRB&5xY=4Z2Zk=L^H_W&B%d#~_BZi3+^JAc7b#aexe8IdCY8JYh`kdFCgC22S4{ z{8NUMqh^m{N#euL*%MRNnXQWh!JY<69icl9WWtsR_HZV@-kD--u&xxjr&m|AT~82Rxw`h0K6=z`5n+HIt!X9BV4XuvCu*hd4MbUB(X=KgL> z-Eq1}9rX;3i*80lhA|{lS_Zx`g~etbKiu?q zzhqq62@?m#K3lvKj%-2*CWwtRRh@N_I9}PCUNLa7$TxY^St!P)-K>r>TEwR|$qB^P zd$2K!<~nf-P?N>!=Lbs+C?l;~Wr1d;z1CLhyL_$vLo2SXq}b~g@k25(X=g%3Qi33N z=<5g~Zl=WZIpR(a6J2P~1K3)sP(gJf&g0iMx1Pk=b$OC}eB=j{bXR(BkK8v-eg0J_ z{u=7sXClDmx!@)|Nzg{vcov}PC4@|~_0`zsV)Xkqj7Q%LdO!=y2kw+QLK3_LEWYW((f zp#6)^vpmG z=FJIrK*JzCK!?)7+^3dPnl=^5(P50kGgOqNEy_4};o{)^`BzI8NwK%&?Q#z`=QrOv zXC*xK`jOoT&1f9Gg`|2uByj&W$Fl*;9zW{kFUZaE>NPb5FA_$wksT}npTT_;z@u8; zuo{Xh!Y@+*OBtovkGFCF!z=u;*CSGN$LsE};-_LgTDaHE(+`&Nc^W$o-$uqm9_)zG zkTXE={C*i4jBPksbDGlDnkJwpP0b~tN+v+eL|a+PZtB}N-S|ghjLp$KkI$L3uRNWu zz>hV~Ijqc*wh>D=2Td0?J9o1FsK}xNQT(N~XTrFp-+^JDxUi;m-T0pG3@5fd{P-^j z-W|JfP;S!uQf}%QJzo$SHNWp)EcP3PnM>8B$Aedq^Df(RC4Vy6b8KQMn~6kI9{MTY zOW&8f5Sve$zEm%89P(R4Ov6(hF^{Qj>FBoE2k+l}11<}vn#!Lw74+x)PsU=R71Ftx zbkhb`$J`hO$WYuC7l0Yd7F63%>fU?ISz;7tmARbtR4C_qec$J~-Z^hy8lrP9?&2?q z%*`}b?8S&h=n#TTF%3klACIQ*pTSa+5wj#5D|Yb_oO-107i3Oj4MCSgz!8g~(R2m( zRZ13v2Of=EKWE4g)b^zILI>dgCyVE~3e>4m!&}CugB-|ipNB9f=CO{zH^6>3Y!}ya zhJtC^H}R7HXIvCjooOSQu6vc(v&$_F!~(4u`n^gHBiPi?xrJR`Uf{usN@b`st+)N9+Gr~J zZ3Avc9E3Ha2-I#2*b4vqt5}#}_ETAu;HKK-e5;D2merK4VcO}>kq^G5h8)e7VAB=- zj-Ab{kp*0>w|+tVi?I}e_kU@Z3!=G~ZcBB`?8!sph(F%GU3_xkPIYDB$o-8Yu5-)^ zjBAFEE~^?XLejy;{BvFVu}%7DV@5y|&?4>ufP}oysgZnLvrOwHO@76e)MaCJRb|5~ zs<85bQk!&c8-}QSBXV63PUX-7#tcKcy2UfXq5^^}h+-V9Nig|&374UeG!qX3xz7$jjZ)))u!hbSo?=$^Rps0>`c5JUDKyuB{_2K^=ZLyFA0S6J@tnVqt4LdJ| zNX94SR|w8=?JvFT?nqq?s5~Jp&K$L|Gb7W<#ZIqT08WoxpZsp~H@efTd^WpPy> z0)(tT4Ev7jYT`x!MlFn_|)~E z9{7mHU;1Aepc`!sJrN8EGOj67$Fyz`6P}CJ^7c(LpGdL=JHuVN);s8bTeqVBG@Bmk z|4TzGpGwWA;F_it$XpDZ{k!cg3kkbsnr?12pS*lI_@8YoT6Fv z$aOU*pdI{zP&9yM_@`FB#5Td{Kvu{AI-uMS0`-e1bcm|Pz>)~$>oA7peQeV;GzTNt z74Z|xI228Fj%LF4cE4qqg41Rg@z=)uwQfha1+ixvxO0GQ<{D!A)_+5P(|wVFhy{i` zxSB2l)Ix9@UBl3yuy_v$qjJ9)iorH}M;6-U9LEB*0^P(OjMks~2mi~68a(}<4xl}9LAE{(({?)zW+C9!#vB(Gr%UZ>gRXbp6{?nkw4`6hth}oVX_IY2u?^*D(e^qXnyEj-z~s(=rLUCd`a8eSU}?cKjEmKZGT7 z4cKSXZ&x>O{DQWarZ_!^Y_j-2T9$7k z-0j&G@jHl}1Xs+dgzD%qLn}u6ZnmE4TC^ICf4wx3?s7_R2;Am*SnoQ03X(2}Y?>AT zQtMkV*2Pz;&OeM@4Z0;ODAd3PU-o%fDs8@t66=P#zpI8>uOq^Nhy73- z88K`6T=y>iMvAXcW7XZ(Tp!6LsbI~Rk2-}M{yMQxZ+bYzv|)8zrwu_itu<}Ik>BZ_ zfJSQ|grcL(8S)YupWiyX%jI}yZ=$p8_34R}t2wNMQv!{Uh4__YuI!e2tTKa@cCL<} zez3HtUzWTYt*chxVzKo&J9v{l&wBJ+;jxEL#Xh@E#5}KW5TD-#JHpl0XRd8Uns%D1 zWnR>;s6O*-`cP4yds051e%cR_lEnu}Zxxnvn%e8{@6&wRpgYoP4PVj8VGIIWY0sE6 zNiU4(G%ZiPMLKDqK26Fk()mq1D^sGmd-#OO;m+!4ltrsrgi= zG_dZ4ks_HKT(own6RoJBT^qO2-cI<*&iN=wDw(e!*2|fFI!lMWAaPDEf&(K(xjZvs zSPkE1`o0DwmZg61YatZDmuF~}REf`|r5OV{sykd>9M@TqXf&8ndmc_JxP5OQT2}&%Z{bkphf^QE zT~2)CTwDX^+JGNe6vXl*z0N5l)0QmkyeE%I-{;aA3yggf7`3r(8o8n?KrUx+Kxx{i z$*4=@&w(BvsHkSAxoQNbO_M9j&Qa^;aZKW)`aLT`Dd#RV$;2|%?`PUMO~Q?%8uVC8 zYjHM|=%979`uCSb)v+}5fGLpTelq5~t-r`?yN~PJ>}1)> zg!8vv?2mG2WIimm*_wV#1lm?7gE>P{1T3hlG{wvr%ktofWw^vF6JvaZfc# z*bDDxqB)SkZ8H+=r?BYyo@&wTE%dx(C_de#_9#PslqEbO13i}b zu)Y1Wfy=$QW;5G~j{O-jVb>2YHXU6uV3V#mNH{^b1@i|nW%@L0ba`8O+Ci$~-5lyn zW7(5fDr$D9PAB1EX1>JuQc7pt&9SPUC{_b~NUD?w)+pl^u4(YR{+8 z=DkUJH;=hB9LJx=Cm@JPh@Tpa0qmx=2X0sTV>-;3I!1dyg%Xt`WXKFWBDD16{cO&5UA<^U%Ou-RVI<_dZ4F~jG~gC?ADF)?o5LO!Te z)x$GO#`j&Wl9I&wIJO#n_17E8tkWY0;i9njK)52RiNLmm*pInOVZK03noYgEWf^Fz z3B04_wZ$%;MtP(#b-{Mb2=Xvr^`qEkRy>fy1H%0J_^HZGGXlCURiH;D1PVDA)n0kaolEi?%sSl7LdSUg@Z73(fCb2vgeBJ8|P`M z5kDy#3|oS&;hD;jH&z=Qp_ih7K6Nko+O;*vi({My%bIHb5D4u|7=4=h4w&F{K$9O+ zjpw}SC~Anu96}h!?d+ZNv9e6`tRWz+7c49H*QW(V9@;0Wxi{eB{os1wcff51Q}kC? z8fJT*i9V)VL^oZg9z_Z*ot^DJRW+QUHl1?b-s$dL6`PjAu?Pn@-I&0bUgni#BHf;L z&e0A^r2#yG&^6tY2`<33t5C`u*_dL6aFP4gyZr0+Jy_zswH~GID2U=yrqbJPlH0DrPHe*&!_xs%a%-WKm4@F zP3VC}?WkBlvy`Qo!F)=&WbBz%LQ1e!^A}6`sOG?Rd>3IXY_1tsJVNJnsJBxm&qQJF za%Yj?JWl4fhiwLZPO1u0IGT>yNS?ZUA!_1hcQwc}vhLObFEI$3RUYSW#BnLz985t{ zf>V4yj2VlccfBHiR`})lmOxfL=M|>&$4^KTypReQ5c}}5oX-9WQmEEEvm=m-S(uv{ zHztj&FwI3rc_%&_nQQiwq%ac|o9NKXFuKcs8=Nl=Auw=O=c6Sjaj1!7X8FcYhdh?CLV68y(UP-7(6Pq`W z@%&!s5^QX>Wk)Dl4~o8O4pP@VpxdWUJ3SOre6VDyx^Oi@5~+P}99Mgro1Xb%;^}p1 z2y<;OVqQ^b4yf$Oj4LbujJ3y_CqwB$nRM(rKeE&IpDet2OH8{*{km{JRq5vdi7txq zRMDFHh;$yqMY~Mh9NxKBn%G=6?mCS7%9Hs;Dk$q=_J&mS=|yM!awTA+RBkq<1q1jj z--zp6Pp~$$`8(T6>6Ge>S=hmgcYop~yjMJ=5>Jx7S{zxoBGGk_($?XW-(vSyexs3==raeg7dU8O@jOho3& zv1@>V?&0aYZZX5&niIrV7`3;HhqtpQztDjhbq^Y3~zcwoKWUMKj~@Ycra{#Htib7+Jp_>QV93npzP16>3B>ZY)_xPMQywy|Wz~>V zpEO$58AiNhKZqx3P<%p1KswB_cA_hW|hL;fp#i1RYrp#p=cr6Bj3}QilZ1KP5G8QMThZk zzH4dUPq2<(r(~lZNd|WwVJ4~9TU_sqf}Pj`hLrjmHyRX}Nm~f~)UF0NAI6v5bcNC$ z!r(iveyAvUs9ld{!!>t==a-9um7U;pN4F;S{EuZ{OCpoM&e6lnZCEGNDB;X*7RGiKFJOpA;~FGRkkRq# zcs}%#ug8^d2b=sTFvVclHaB$Ip4w`7rZS0EhjSi5}PtB6~LH=H{E9^lT9I z%Xn+qu)i?e$i9}B{o;qqmAB>#`U}$U%uA@uNdUu1fGJg{Erxz>K$b#i?nX2a_?3Tb zMA|fNYL{I8mium!*x?@1p@z-lP=493uHW`4f}rSYtPk8o9gm1{Xu)+s_1yYcl??N;&Fw_@`1;~YZi0?+A$!JFGTtsMh8p1TN=Qa zR|K!x6xKTCl<8g191qPr6?R75SqplvL+-_KyiukiH**~%+0!38kZANnBY(y z&4iw4_0y)@=y!hf-Mmh^s=BOvSUq<%$yy}ej!im@Uj+G*jk2ccRx#|*1gFZSMG}!w z0LC1jhh8HYM(MiFh8uSYqe@pUu-hkdygaA!%07r!K$EZT@)>JGr5iEP>F+?&PUs~t z32tcVi@VVnE%K$4S6d7l>x2ds0l`#RWjUia_B@I)`E;h^XJ-;x(tvwf71nLHs}Ff7 zab)-Px~Y&~AHZ#=xOsedQ%hV?rClfrIASjt+w$bLs(Kih>@Q0Hoa{+BFk#$Gk&g~f zlceAR0D;MAD)Jq*r60*NL4E9_Sv2%Ue^iN6%X}@*Egm8wz0XEOGRm<17nzP zz!~T9nU3jyx9smR`+J@Jy*K|qb819jU0_tXwno^~8HfhpCZyUlBF4ObByM7hrm)V_ zJzu;MRe-`K6Q?Sk^>JR@LHe~Q!`1a)^gIOla`d)3TnIaXV97S z$G5!pDgTi%UQGqO^~%0~%@}WR|CamRf0r{J{d%QOg$r_wv;G+497j5f<{nyNlL60> zVwP?xCoGg)R423DKW`kTY*MHG_b-1B!rv?5?_KcsF8F&F{JjhQ!(A}Or(xWCLsh6D z=ikW&|G1BT%LXdPkE{HvXrTIkNHn-&cxIpSRg+t8E|UKcM}T1PFLHv~pXG#eXRrM` zDdE@XFCv8Jf`O3%go%j>aufW6{F;EAh3sKs`u+3Ahj}ma?++^r3-ewmD-`C-^|wxnW$K+`oVSUCHm_ze|B1PBtjp?T|M9%h!kte{EzIKTs?JfQi^%zHp%_p-2nr+vZy zLs)qC@=2+jX5}}#1wHs^zv|PN3^wUAg$)8%`p7bBw;u*_BzO48QFRSX zt>Y)O&z{paFg$N`;p(;P<`$M#*7kSq-g9tta&~j~c;bon@(z3!6dV%zJS;ZuWqd;7 ztEA-2tn8dOZ{NNDP*hw}T2@|BS=HFo+|t_C-qG1VFgP?kGCDRsOPZVi@^xYH+Y;sH z+WN-k7Ik~)cfXh*%>V4xzxM1e{o)1v+5_f*1^T;ROnW@RmzkGkuapWa-)S@Gtw;O^ zRiCo$KNFKt*uXBWc7-f(`*GhsK^gT~S<3IO{iA39wT=b+FZJwSJNAF|YaGJK%mgNn znHPe9>>RbqDhoEc>k)SNWldBY(|*oa>^|z&HDI&Koi?4F;X{uYP=4w~sXK zVCnp;B-5toAS4fi2knT_9Arq)jfe6*#Cmw06D?fwMrDgt%C9{aKU;P3CzffZI$MGgCdFje=M~#2)*?-6=sHp9$rnGn>gtxrFKt3Z7Iq*ZKIKo+w5cX$>%u~9(@L_s;3ZByhU9cQ<#gnx|UFa(dX9v=L&fc zguB~Q(U`CAMcQ>CKF!`ga2gv!ADpdSrYs>N4;3d}6@bo<9 z?0sXijPskPu0@Ql`Tanpq6*i-c<~qWJlZ4OSe`n8?|yr!4z$x^u7_qWkddJnfFz>n0~ z(A(sVgtsom&4wlwE=lug=OP-!E<_bdC2J{uiB@+dLVQ)cNUriGeHnuV9=;MI7b_>` z?Ygm?Pl|thd2-%0`zHU6I7)W0K&jx^THFis;O9EW+yM zGCJbCe{(D*)$%$a6jxkVoKFoWolDSzle?z~&oM^n=nqhz3A7M0pj*-=UvS~e^wV*^ z*%^1GGe5+WAzK-w0~#gh4XF>`5Myx$ccZpe3VjpKiO43{POw4v&gSw_I#Lgq8cJ8W9jm_VxZL}EqEt-3@>pX6MKu%JZdN(^nlsjGQT{2| zQO_9tcJSTg8rNA=5J(NgjFu;X)NK-0z=eYd$u2G{DJ?E~Y81OfGJxD9KD!t>&l_Wx zL1VHzNIyzZl8K0>o}XW=2h2((0wiR)^70Brs2&G!UchV)d};DFzJ7BT$rQ5kbM+*t zwe}*ft?b#;TqQ!7qWJ)!h^t<=n z{OsYx#KL#>F1i=`z3w(Ji4#6e-n1A(is(s#e9|W=dNAg*O;MQZXZY$k7^8sMT<-`P+Ng=-b;n0Y z2=}|^UueLbcFA$J6CYB~sfikPrs7(#i zf>hsZtb`ALc58qO*BBxK z={kp^$x>&jRX$iQ{bNCwaNL=q6U;&!n@*(r@0k^&y|*XkP|I4B@Hx+?8@0%mWY4E` z*SRT+*^#DP#S8M)G|T+bDeRdMhrx~Wtc#y6X9IUy+mX{^A{SolPsYt{(fM}1&-DZ# zT)ux>zjcd*i#S$~VRAr`XenZ1l4}}K8f(wOo0?6k!&0_DdIZ$f-AoVt0gUpje9zwR zGa?v%%7xWt3klgU5m$xv6fVu-AN@Lt!G={<3buVk(&)*X!P_n%PHOqUoH7TXYY`a2 z>q$`68oOul5ow31Nmz>K7nwVc5kpWCsR2Sd%0dmzA#FFTWslLGTC2Rps4vVBYVUhy$Ar z11OBSoULIY>bBgw8-)%sN_sVYSfk|1W=ioTiOym239qA-Bu~#a&47 zO(tx5*GdOJuUCa1~9 zS|ogBxVS2QVY%ldK6T16emvxfcVswY@tZ!x?*qVdrIS5(+3i5R4H}?Uena|EI4f{C z#S>TQ{2*MEqS5c=xAB$0Pq@Dth-9g;c{KYAB0|3be3b|uYlGNHC+`9R%GK+B6tlDu zzvet=HlSVzVEYaNX#ZN1I<_%`jQY1PyV2GJn=mBywElt!HO;Q>1=T(x&G6jF>R?UE z{=T_o;r8K(!x7Q4ujA>3^~WKAmq=6WUPZk9>B`92b?3&f%e0V)6{w8F)@Ah?vYsg;QCY0!<$uBI`t`H?<(#KZzGVo&0jyFb?&@5@rCo<4DKTq^;v zx%}tDbcBiovmj43rXZWgFNL^zk_fQx<8>v;&`pQXbn&(&4shLJ_$O9=uISA(tlSRLo*j?AW zmpkF^@cufRe=IQWAqJd^+Uz2PHzq6g?Z#&gWDf`$C;3z4#`6SMf@NmSr#F>3#&{3A?d*zp|IXBH1U zk8~0R(_tB((%)Tmy>rbS?r;&-9|unOp8RMAvmDi;Q@e*Qz+p4M-|_whxw4op%X%&z01XEk3Z z2Pl47V&-daXIzl%>d<~Q1ha;0<%8=@uM9!u#LXIj%L``%r?Z-p@$q_xz`1r^S4e1i zb&jx~>g;RLC~=T{aM3fd3e}8Yn;Gktz?yL!TU0r1S zMxX{@WXY(NBU+8$S6s|Xhg~<`)u_4U8Nc~RUI{K#ES_q~>3Mg@pxl+^0qsn@*puSm zZAa#@|9EV!Qxm}=#sFjzZtQ9^PDCBWjJZv*2q+QBlP(5PrO$4g9PMfT=7m=1_thi0 zJ-=-QlZ`Q&5@z0d8q%Mf_BL+`$DK1+qmh+YCs{SnGMti4jhydCcy|h)MPI+;bMNvc zvcTAdl{Rn4@FZdFyFG|->|pGFfZX0F1W|l<0J|{-V0S%NOnqsO7|jeOYWaJ|G6}Id zN}iBg*BdJnnc>YeljY4$f%)wec{cmBWy1@bSmkMdwGDu^`lhE)8pUOEwzu@WR9ttI zPOaV%GdRlwNEA1_$oiq*z_&l2Y=D_)E30cOog4L^oq>Np`TyooLQ~CV1t2h1>N%zX zhAa=3qS3qUx9;V)&h!&T7qlRngoFYy1j%=wE=!E&COrR<(?2&b-1&L@^X%l3=JvJt zvLIPcEKkoRqZR=KT`0h`2-_f2r`YvX{hnI(M-SNxyzeJ&2AqISnw^3py$~1ZOS{Rp ztgys7Tq#EOVBCs?4EdU!%?scfrCeN6EOtuL?WwMQ(82h+s^Ge2zbpCEuXG!Ld-jU$ zhSuqzh2}<@d%_TV{X{M4XDP526wlanm)Bb=&882vT+*c|aF8b35|q99@r2dNyvhyV zjMT{9h;zn#5WSk+f)ci6grn)$CDY~aF6Kav#yw%q`Ou7=w;n#;e%WQ}VB{5jg^WR- z3qPbEuD5?f3S&ff1N5Y<82vNs7>7KxcT-~(qG{J|&bG#@P6Hj@#*buo&&|~+kLtSo zR4Uk1^_yjP_2azzSvt8ZT{ei48*L)8=OTpklGMRaJ_jdl+&HQk7LE`A>6&4oo>!dV z`)lBN=tGN+b^L{WurUqUJ6fWFr$&dA@?!$d%5+Bl%vDiPGok>3SgCZn$$$pBty!7Z z($+amn2ZbRGS0TK8#ji9WGgdOl|5ni+}k17Q;IP_&p!>zpG8e<%|V;-Vb*dX>#z_6 zNB(k+D``fED%a$hj0;u%6f=ePed{o&Y#*h-;dHzF(fm2gtkf0wEuGu_1mgRu=+K%2 z5gN@5>AphF8Bi!^!4G zV~H_Ua`iL()W-~1Xw}y&ms$B};hvn)wprQ%ZDCL$S>KD7=S7B8EWg6hcj+j0|95Il z)DZ@o8h{U?kxkmy@nA}=3}=-&7X8Er9z3ASzg_awLOeK@cq(aYY7;~{0J`A=zcVW?*wqXTzrd)|}UxMmp!2M|E-e5R4g+TN{Mc;o^)=(Vj*eFPd>I zal9b$!#9R3nc!c7=U5uQQ^u}tES+*@?^JWIsRBy<*-ae*k^XHk&6KqCIEH9W^ z)$0ImwLzsujiTHFNQLAm_iA(6!0(LxlxLeyHXn8f#w&+phRM4)hHKiLFyBAD5>O!w zeZ8v#-ulz8gz0B_IDwd$Y*$JYQJ)eb!9y`xBo1Vx7EXNK*4@c`B+zhJ$#yJXbZAtz z<->FFJ)a7UScqIDt7Af&`{AtE2g>7Ga|T)2O>yq;%p!dC!$fA`3E>^wAKh)o&c3M8(ORiRwE4UTk^jxE#4haK38yV<4FBo3*;>(0 z%0GUpZ;y*zesu%#_Jo}9S2~c0S=PuQ&6Wr!V+4=$_;gk_W02WYboce`#~ZvBFYp~iO-IHDg*-b;eb6{_l^kF>ZRq7}_XW1&*BW+wv%N4-bkja& zJ$^cnl!6b2njl3KB-DLRxZ+v@&jg%8Y5H#w#+IiIKY7As%37udi56ym%uYba!PB)93Dn}Y=kY8tLw{OpNQ0Bzw4D9RFiN^?{3Vf&aA(bH|Iz(xN*w= zsNibH4yfzs%szAQJ8K?K}-BWoY zPkE$OQ2QKU8gmcc{Om)}5@hcBnXEApUF?Qa!{0ZrLBc%Vlf3v_(p24}6YqvK2kqdB zDW^oM^%d?kFLhohdL5u6+Llu9-BObmi>_{ETYA{pH@XJ_D;~hkvJU}H6^iFB1uUla zb*7jMMvopdxwSOQ+1m4&y0M~RQ`MWC{BIU22GjR&*!NJ;pig%Sn-dROu{@jSS%6p^ z@BxePCzan%9UL?J=xU;iWtz#Z0TZ>|RpS$5odZ_yEWcc~n>KtBX7DLy^=96FQmr(D zC)I)x0kmST2za5JU+nPvyRdd-|Ggvb8)0niR{c*o$96K*v<{Q!F@LRHMgtvL%Bf$(G zftL7}ZtSEIFY{Zn&FqJ{V=na{P4nM#`kThC)|Fn31ETD@Hl(kjwR6^}7XP1*_&ciM z_ZEKGOKha;9`UZ{okI$}Xmi{h3XuaL;EQ{(tW@Y(K;>yW<|ukSE!qM5Zfa@k*u;@* z$v>CwrF|s^nD!_Xd>)-scnG~PG%C+kbHv-LuaWHtp2|EXB01biPG8lpwy4sVNYHXt`X*J#$oBMHr8L_r{N^An;cY5r{ zU06r)3|6nz#~|m}Qm&R6KV>t9s{FXI(lfT*)?Wf|EHA7yr{KefLRf_JnBd#Wr^He~ z!CTly6>#L71&8bER0xP9>TjDh9=mf5DN3zP3f;$I&Jyc`ZL5VmxWE6tK=8C+je#U} zqJ{XrfmMeS*f#ywapVZlzTOqmDsAhZi=`T*=(3P@arJT|(N^Rc{BR-V(R|73wI`z( zhRgZtN1J{sUp9J9R`C$qeoCmi~Tdf@ZU^(vJ&dKiw!763TU-&nZZunn%>O4cJ#&U`qc4sCLXUud{})JCTfUUHzV z9vhZW^#d>TY%Zzf{jz;g#fV8wRFN_NcKWO5^*_Vb7d-9c`v7a<(G~+%9I+m;eE%2Z zhc{%qeDm^dhTma2+isG6bQHeSVc=?g)r0b!B{Q_S6{<$V!wcW@=9QA@m(Iz9()zmn zJ>~8Cu_kYH52rMOka3WRS_E^FT>f0@W1pgAU(JcPOKGx&5}eh^woi8HW=heI?nhln zxhoi|#1?5RuUgS;#kGDDh|4*afw*PhHp9+Z{en!cW19vPjNpqB9LIk_-j}~+823UW z&?%eK)P`Anbls6&g>@fSH_P&3%5G~skzH5X!|8;!=#f6%{Btpz#qYAt>UjACD-8+V zihPO+!Mei3%(0w)!qn*NLv%H~llN+b*rmbSL=K^Mn9TW43&aKht=&d)K4{-H5ryh--^ z#4{6E-bn**W5^pzaFii{-^LufbrRx$`*7c0d6(vsd zstKKzUaypMUI&6_S58To&Jo53+HmZ3Lex2{0i~dqX%bl02Z?6IKKaJx#feRiCYN|e z+a8*fRK;CnyXG4D;{f|wl8GfXgl3C5O-8Qp&zYQ{yJ-c}g+93G2}bp=S(0ymBEU*~ zrbKO?Z|q5RP<7@tyz^bP5Rq_oIG!7rxAp!B#c>EWfIUQ}u}?u^|rNt%VHw+vPEB_%M`jc{20QdP;dBu*xg+L<}i zI0Hbu@9dx)^IX%6{hH5ye?mQpqT^Mkd;Oq{h(=X~vvdC=`r7abu$0|g{pCdu^52xH zy(#$M<-q^TTb;qx81h)${qi)f+n!$9=22W5(-ye?aNi8QFW${!du)g$C6HlgLQhv1 ztOg07wpkf*dL8BlxThy`y6?LoMHI{r+l}jfgUcw4SL`NXR$f}Y1RzfZl}VBOh6!^W z*`>Q?lLkp_&DC;L8vP+nh2pVSR*Lz{!!P`4HV@p5U;3jtI;T&6a9-+@kDvti53x1t zG(CRYpc!daU-x_H{)VO9NvO(lj@GE=q^S7G*V_Ju3%Z%`g#n(?=$dTj=iPB_V|kosPISIy zbX}_~2V=wYp%f=~Q)!5Y?oJhWF0q$wUOEu0pcMGzW2@0X8d2Wo)E`4 z|DJ4q<@%Qw>lth7>rgi_{psOL8BIw!&StRx;^}FD4KuVJ3TvsISe9I9ZZ79x`?JMu zW39KP!qpOSWy*loEhD^KMC;UE4|7s@i2#O`I3?kAJ<5=&5aH{*D;X^NaJP9J!1(vQ zf4cu5SYtggC%_x9#esjbQkUqg?;u-VcsV)spf+5mV*&9}@~ckjNqFZKUoEuc%#biT zZn2+Mi#oUQgr^KUxz8V+|0d*4S1(IqgR~OdPP(zlMx9OeKmWW&p9S|3!Dayslu@)K zG!%9G5}$9kiU(k9<_cf3=r!nRFgJ&Gj&YEV#|3KhDK&qq0eN;&$l(Yp>iy%1*-i)` zo(44UY2Mt5P|wXofg=zm>m75RFd5%(Iu)aD)>U0x?AVfZ#;W=nIygOoJIwiSKf0iR z*{z?r#fmmw)Q^v9i0<-6JQUtVZI=}56ctORg3~%E5z50;m0{QhV(5nAS4DO!D zZBAH#W5Sje=eFC`w_;9nm`7E=V@7&}#`mQ)Ju6T1%i{tH)_sQ6nb+$I?|o~pQ~q#C z88VRx9OU3o5GwaUJIgZvq_4}#$TJ+OU%~wW?v-g?NH+Gpm`G$sSZb>MpnZ~Bp%eGy z4{bZ_Jw4XG)kCU8W^ImMU*oB*Wa#v?QSY12Me5UZrv2Yc?^8XmPJ=Vzza4n-q5M*W zYMn>lsN-U;OIuP9%>bD$oc-@GLnsd3Chm^65&MK zAv}@&IA#8^Q<|8-v{*D$W+Y@9rsjY-)9dWG6l<XbSyF;yD#BvDH0G&npo?b04&&~-H>2g(D@x=W zD%?z%PQqiR^A+cT?ulF?M0Fn8>9|^Pe1hvf|tY zy}Ht(q?*>F3t7%%16r0oqC8_@kJ>}B{@}sAxewby54+MCbSj$jYKl|iV|xGAc;aGZ z#RgnHWAEMHM232@LF*3a4I!l&cBH~NFLi#>Sc1gA^I+jIC_A3U$zS2US)Ko5bKp?6 z(SI~TWO$vcEP_uo$8TU4G_fqv2Qn6(ckqDfGPrQGm_L8KF{4tnB)l+3MNUGK_ZJE0 zS^+FOxe&edh5MW{j_tE=dsiYgjh;#3|*&2G*dtWnJJIXue(J*Db&Y`QsVr^!Izk7Q$^|U#A*) z*ZnK?Qkwx>tX%DQSlxLQ^QyS}=<-WepTEx47KGd6-c@*i90}H{cqum(7px@R`fKQ| zqj>!Vm7}aq5$C%iH0deO_u6|MFLP;i=WJa#3lbp1kw@1@o;@!76Du0uJXrtCFZ9=w zlbcynv!if2_-X^Hk|2Lw00XbJVAxtPk&(V!YHRy{4LD*uI;z+=g_Z2~$C>Abwtgpi z@Gi8o7CQ=Z`ey1jtlNYyKfscvS#wRxREYW=DLtc%SBAdS_NUcg=-=ZNB4zCn7J5uQ z7qj7ecmwa>2d053t#N6us*fCoBluF(3tZk&8h94-jUx<2Ok1f)lZNiK@*UpsP8lJo zI~D(7gEG{|&c=m2oZH>fP6i4>I5^FYwN635@rd*#+mBDj->-%?6k1oy2G|tfw~##M zi-5xX-xs3Yqw9|tlwW3(*tsexRcyLb_Gc!BA2wy>$gL$tReHeGLr~Qfe3Is5Q{wE8 z)*;d0;NdUp@$Vi=uw(V{8tb6gE4g)Xx)r$(2z_wVDIc;$V$`Kw#sXS+-Wk;65tl2~ zf1i>GNeN)WwlcwXee$}|=zWgvWln^c{Ty;uuA+=&=(|ys&7_S;W&bIHjV2!#c+o$V(K>7)!BGwsZ&4*qM9DC8}xN&;EBL0(U?w4Voxcb8{Y|494A+ZLWht*X@E z0p1d+D<%xO2YyGo!66?;HPyIhSZ3!WxxMyrt*?uiS*p9<#h0ll^p-1C=8b`J zK%u$I0VZ6ky(%oENU@%AddOn2XzgmQ9^Ys(D}VDlvffAY54TqP(ms;9*FiN@E}fTH z&-2n5l9t(DjKNgOYUnp&MVi+)8LbEgb#2QZ-fyseOP1&Q7Iga@XG6Yyq@1vWv9WAS z={dHtbeS+))^HMSu(Av;+41l5sl<+WUCu@iKN~%EAMv@Uu3OiOCOp7grRQ=^HEMI+ z!Rb5;s3{50^?#^IufhIwWNOf~?g#N(DPZ8L>Y6w+_*Qn_*-+tMpkh4w7<`zb6182f zJS={7oI*I44oSO5C;wse37B!)*uA*wAQ9#cZ>(fr z6Vr)DF*#(?D3QK@Et!02>LD|<*IB#r{z`3iIOj$Xm|nfqa)&)F;stwU(g;&?ez`!Y zR};49x?0+vkG71;UNZR>nGqnM#>lajkhA74WeQ1T(sL;{w^?aNZ8KO77cb0N^QWH7 z+DaNTw&XrFasPEoAst$sc8sgS=;mRc>~(uDxndXRc3Ilh`>TD1bR(HgQ&ViL&9g?^ zHoM%a_&&0-q9|Ug4AxzLR3vv=i+9pUQnUzgs@b`yPK7#6KZiH$`g>U~KVXbEqO|O- zE}~gF@YQF0FInAbYgiF7H33EKZcc#7zaNxVjIOm-i#x;`Pswme@0=NqlC;dPNj{xm zW9Muv^`yJncrO6(;a0!GsjqC-Sf*eK;3$M@nYQr4yfYwjw&>~~Ck;5qGi5)C^Kx~+ z{o6t0c7M+U&ah%^KNv zc(2xW3*j-?U%*+p{4fU@)$?Jj9r+;Gg_ze5DITgf|p zPaBv?bUX}R@O(b$(1^rRZeKWszsBSO*{h9NeCdLdSWbm{iL?-B3~R;U7DvSXb zy98Y^)T}FZx};4y;arXj9&m&4UsMMdZ(Vg2R0Ai*E9vWJmjntRXr&er-N?i}n?y94 zn8t!yy0WEc3*%1j#SmQ>5?BiX2%|K<6H`A8#$SUleIS}^MR$PgkX8=lSG$pYGz*E{ zo#Y7ba%hmnIKqGl_eIq|;Z~d>uh>Q^h1cgdgOC0O3%BZVtw3v@Vk|RX2z_0IF8|S> z+>_ljo-8=x&x3vHM|;=z6IrcNucq|>hs^nXJbVZ3c_q@MM*ssN$QnSUTaN?2=I{-U zy*L@Ji3kyom~l!RK+**8#^)GaNl8}FQgq$hyUXdzw6Q`9hU}B9!9!tO-}aL9+`+LY zSU6Wx>+uVf2M2!=+u3R<*rX@i=P8Ex{j>ohlK~*8IoZF0H*?Lq3Q5GyiGT1HqJ*>xHm+`)JARcz`-Hy+R^<}^OMvtEkJ+3(Kgra zD!T0hQ#am*a9a+e)|Y#8R#BTKabKhneg_`|zimAfT)NaFhx;sg*O@2m>BMLfRW+5? z-1GS_cgfz%z}I4wK^M>kfCwOJ2>3k!yUBl^lJB~xb%llJPT{?u7W-44q6-V}JG@!> zq`H;sazeZT{giH)p*P z&ti6|^UF`4tvE)oIa9P-<~%0Cm)!Xg`Iw2fR#jc(ud*QeE@zT{0sMJ6M}UzJh*e73 zE#Krx`!M}2B3SA%U>IAtI?x+$0h}3K_Z3A~;k5YNE%mX@x@p^qHHPLq3EEUMB1I=E z$wz}SXX80|?zemxqEGa2s#yi#``6HAN!LeNKvN7aIY->3Q67;20Uq`FJzvuv{ge_~ zeXo0qx2z&;(cxEA#mz@Y4cO%^oG*Ug@8Lg03UBqwOGb6&gBhS=U4z%wU&n$XiD$OW5vgKlI&^pD$`=&J>Hm0b4*JwWaux_${;Bm%1U52KW|YYV$&Z{t2vzI`5?aMV z*cUWO`rh10uiybZrjti<683Th63lpDCBnNFN=C)g)s2@~W|h8_GA3yhw>*=giBlt~ zD9Fn3E9>GB1XdS)8)#|oG6$w9eyZe_m`s)Nif+uhilhO%lm1_FKC!otA2EG2UfkBb z1WChGfx3h@gj)iC?THRHGy`n63fF09@OGNmIAvkPbYp&Sne)4hd(*EYbv~)eW&b|P zf_xTY9O5W^Z^?o`#xY?JIxwF7%5?QJh?_wH{#XVp?8--kwr_C7j{I!6{WSNc*V~l& ztMO$EOUv3`r=ix5fzJyA$aivt`%wblE0H;+d*;vb%QWoB%4TNLlSc10$#KUKH7jZ5q1r9jo2^6W>OXOjLy;x?ieBvN%*P?E)g*H%NIeskbWh^^ZQWx&amx zrZ`(R3;zz-U;)TZjw(GINd)D6 z?#*32#<>75-kVOm0jtU6dV#`D1WK=ehy>h={~?;21+wR_L+bu(4{Iw}ge?E`ntbL< zR{maXgVyK{0>=Z#W0M}WRc&~UVQ}$)n#;TFK7nA8xA1v8D|MzHzIi~L-=);)1X&@o zb_K{mT-h`2?Ro-9r3)`~U%v`&VIYDddTeudDYzhWArvsj^zM*b?UNJ!>MgS}`b2gg zvUajAX5B2)NK=xQTZ17uZ;W4O<vC$ zBE44wAgK*>QXeK2T?%|GM|=I2({>>+`5mvj4@AJsing(?&4AkHRC`*Xw{$IEjXziO zEX1_u6H~SneUHcujQpe`B?i*dd8bM9P&$Ys4A~a@cVDUR&+N-v$E^h!Y|Z6&>B*!% zp32FY z-Ks|IUHkH3Gx|bI5#8Z+3n}{5@$+`7nj7l&MRnM>Xjo5Af+fR)7Y) zB7!tlZ$R7qfj9*|@<*Up7G}?4_QY|jF$lzSuwXGcjwq+6EO!hF(5j`8exovI)pVLI z`+xX3< z>b85XnJKC+BH|uaRTC^drUm)J;9o4135NsVri4yUAnQ;YgMo{8k&nk=k6qR;Ysm0f zPo~8MvNRE)IybblgfA^*8jq@~iaL*YL-(suX7^FM8t0*3&K|19HH?vLD=Vg)5svllB(O{bq3t^koF3wbYWt)&@8=q zTu_=9(?~A7;VaC3b)u7O)*EyUJtA4HMy;&gLizk_$Me^+FUG$nn0%t5-#e=eEdFI8 z`KyJYa?*=8`j*p0Zb`OGR@1SU?^^(7IQ{a8a2)5oHvrNiy6OYbhVa3-@a|R4Y25Po z_>#Gn0XY4R4ehCplg0PuHjH%+?}~OIt0!+mYueA>r-Yy8>#X+ioqomZCT>CLKXf$E z;(dp4Z_Srs7SlBrDtN&nhaYHx?i-EC{5XKr&zb=2%< zjktkm=mIqf_E~hkCp?%|6voo5!{cEFpdZ1IUc#e9iwQQ++E#+?bM>6!uxIn6^cf>p zHgEJKU>(nkx&{FvWbw3zQ}e^`y`yyc;grd@6FzP$+?mp@So(#PTC$W-W zBY!pI74do3<%`nZ5gMQ?u!d^ShCx|s?*bg2vX&la(o2`tJS}o_IUi`>8g0;o5Z#-& zUtkvB=t@w1alP{S!awfH1)HFxfa}2KeL&ctSD06>Sna-LoB5@(=o6^9WXjJHW~anE&$^eMp9pqV$!fPbNTj7blL-D* zr|@``JNp=59sC~U#_t*7t1w#wnQ~zW2$lrcFs%gpz*N4}EWOKoO=-a@u1CWL3`&oy~bc9SI)yVa`~3%V_B^H+}s-;7=(50j~3zcQFg>63eBZEBWC zCmziotjy68`g5na8DgB~)9jldjF3Ms`c{6enq}Oc>m|JS`{==EDtrF-j|WHdheoTN zKN`0;r^EcE3R7P#*3E1lMA zLRVXIS9(xD+EqMZ*ZR>2iX4gE$u;yjigq-P(6*1g^|tCXa-oF&DZ!^>^>KVlcE5M7 z(&Md!lf!-OyoN@wbKXxsks-vst*$2T>|Idzdu5=NBGmlK7E9hcdK0>(iIoEc4X~8P znCfEtdi*+CKlAVj_HkRY!joMi!LU$WWZJ7`JAc&8rd( zZbS_Y?H%fm{ddk!@pn29Mfgyb3mhji58swFuv}Q)ES9?!Jax-H;t0Jq^GmYT z&={)H?`3`lryy`EWnZ%NJ#3AR8O#-$lH3?nO)&eqc4nq(9I&Ni13>E-*K` zEOxsP;rHBwv5)UX_!NC9y*(#g(#M(Bc^EZT`MDLTGVROVR}9~NKlx+#i1t`zeC6sc zmil)S=IjTm_^N;N2)8@~i8iJBwNL2~#c7|b_dO7(l}MC84}1KXXKfrJycncehzfK( zE_K=T3`V!Cy^5fIp2UF4?m;?Tx?sMF#LSIwja1~jShN1Duat^b{9%;QF#Gj7Ly_()pzX_rPNZ_3p@k3_XREg+#oatt z7q|(73q8I)PyXp0aRs}^U*WX{n+R(jjBXXWGcH#g;}ivZ?7hZYfdAN+ z=hiZLGC#&!MEa!K(e%gDT*qW}%ed_1pjNmnVRG2nkrRQOP@MU`Ut#<`6}LR>-6Gz1 z4WXt|uliL3S?RRyV9lcjdvtU%LACpLDz6l#6@}J2BQu{x0^YXEyjX#(lCJW3*QA#D zgXNR9(2-bmxeTiCoLl-G!)4z9Z>v^7hdkDlkd$(YxJ}edGN%3*H8N`Xwby8D9bawgQj|x()0qTDeO}$)jXM;?Y{}<(hbiN8raXwk{T0c&f1OO>64WYuTVl~z^B4Kt#BSk3KV@z}5 z`nim#lx5w_ihg5Vd+3a*eba}Qc`HP^@v@=4)d~tk*Da* z0J_-ou=^MMXYrMM?UVbcZb+B+*3%LmxdT2!N{`#NZ_>osN}NF$ZO<@ya+G_Vvg8y3 z>|3&j;)VL_&n^ewQF!^qV0`Ex&~CQ;?98{&*2yiOgKu=_;3ArDw)sjc;Q zlwGN~gVko5L^-B^#HM_$?3;b6x>~D z?$apHoM9IoUT+|Txn=4qz;2Z$D(#{QS-#rg9^@3cf2YK(#=hZwzxC3xMzYZjHO_b1 zybq}7s#fB_^(&Fel8bmX{gjtjn{b%k>MdX^NAhE4N^x8kx}nd}F%U;Tjf9{8>Xravt6EE15nKo3~ce*D3%(R;0DRo~iJAE`CgL>c|n z*trBQsojbq7nmor6yASpvU!rn7SglKj-5`7{DZ(I_O-&a*Ouh;#|`^ta)q5?8f81P z2lZC-R#pKH_8*F#FEKtVVUDMAxb5vs;D*EImU=J}H z=`kTd&#}{s+h8$Juo7&mi^0UqOp2O6PYbEZ}j;dSI6;4U%dSUL+ZLL8-;+{HZ-Lo%Af0VUE0*FXdI952| zE+m%_J$c5E27EM zfKAY87fi0GgK6mj)K=c11Oa2_rA3qm#2+oH%q6bn%pa{-0}w^1M;0sYCksEN6*N|L|<5s9#e z=P#melXXcGH(crv#;GqKX|6)FZOEY?2Le}mJ?Vi>Td2S#(-LFWv6Q79o6k34Nn58v zKi|{IFO^dj2Vxk5uhu9tag%=RBM9RI(4M4t`Lpi8!|1=K+7%rNTOBD+TN4%x>r8oq ze^eelP*b4e>Zc$kqd3b6!ugLWnqaMm`!j)-znD`c0QY&yEitj$Hp7>qHzB?@xiQ(j zV38D6wM(kH2$!6?K91iCRN?TMT5jLJPB@WQqleLHN=L@X>3w&rg2~3*u1{83ComF z#q^gs{o%szblQa!{;>1b@EM_K+e+`I&sCP4*9p?@{@u&$lTCSCFiR};R_K+g%fGhx zz|!cnV2fuR%^&v1?Y=zuHin?r<}APW__J1D0y-%VV}m2Y!vSLp?Fodz8f26MHTt!> zfh@hma<61dYqF{dk`xf^!bz2JWWG(&za>d-G@Pp6NWA~bK%I#3zj(pIfm|9OXQ#+c z{6q8?QV)>B?F$+@R}n~o$ga-Xoa$09s^cD)^$Pm&ZsMeRPOoHgxViAukD0K$Q#nnK zyk2bx2{L*A-MwFcXlQ-)@_^A-kSTl(lE(s5t8bg~;R1D|H4|{_KiOoITrGk!e$hAT zZc2aX7V>es(TgTU%XAV`$}{|+67EqHzI?I+06zuOjiMR`A9WG+GpEc!L6JvGPt#vM zFC}jfzCrVZnkcY+H}K#i8e&D+Ywj2iQ7*QVInBPXE}Bh}@5WX}--V*>J(u%I?YRT# ztn?zT6mxm%lTE(2Hv8rR;&M?xZ!{C1e~>|gX$Zm=Fl#)zRPT*1cRpECzIEkMw|^0} z$jl{E)>qNGTKSxk_L`_pCO|>}_HfqMN>tRovtf$VOY|p8>{-e;dX>AmtmxdP2q_kA zipcWV8jN9VN_|J8(Q=N@_M4a*cE%0kAa&Vm`nHK7hw^P@-qy(`5~f~TIxk27dq7Cw z!Gl-TeTr!{PVw`|4Qa?+*R@Tx**M6NV;B_3Y?*SG4K6-hVcx2}@;B^}Z4h*=Y| zr*o7RBmJyl(TS=iFb!v96c5BixthavAO`=0;&7s?qB=7Ub zWyIzB=-<8dOE6yO4Zhd>Q9fQCbjgN>j0+`Rp>dwctM z>16pH;7T>Ls`ud^B4z{#vpnR}H;eH4wm7H$reMfpOE;5)!ZWTVNtoiE8BfdWI1>@A zx5Oi^cyio(z!D{qZf={IeIvCd_V5G>(E0G{hnop2R?T%^Y7@-d{p&10yM>E3Qe_K~ zUZr3-aLoABRhq;V%7dmnVkles+H5?+VV%o=-9B)-V~_iB%go)Wv~lEb-pl8{a(>uf zDM*Gv8}WC$`3K4r9PL|&G(l1HYwold!1kCOD0wrQ%8jX@JhzsxoYFM$@>(UYk z0W@_|^SCPhI=9@@%bEWr$x}xSiAd7-ubTgUk{Hpkt8qbU@YotekAzbC_#@z013haUnJ=T)@DGQ^2&e$Q}JVk(uW2#y2>Gg4}esfjlS{b zj;;{hEG*N-l{N1kx`hnWi;s%>uocWdc9^_p)RkQMNJWq6 z?>wfoDtB*zLPni>v%9a}IXv}DohP}eNKb8#pG3EOQ09c1=+DAb(TU_c?A$431BJWn zVHdq*D|*PJ8R^E1)WlQ?KZ6Y%GMzmET4!;c$Wze6F$F5`a)()e8B@jQ`2AeiWV;H zwcq?*-`tJa8FTeJTE5OkGn_xzRHGxMyK5CjtJ=ksZ^OA~)8N#1$OJ&lDhLLwz}b&w z`bmV;Y7P4k+)gY$sAvvh&D}nh`=jeYrHxx_M@#Fc45QLZ1$8QIQYWIsx@7F_8tUWPXignS&-sGyC{CPZ5d0u_*;EW+u3orxB#%7L3|A4_1f6UW1{iD#`-Id)Q zqJ_GI$6GSzCY6NQT)yue$#&~pjxK+8)lZ@dIpGu}M@*+W1HIp!dTlVO{}9E72&9{I zS~&uqW5uhTW{Z{pSdedXr@*7h#PGQEKcqv>owmN!kU$94RwgJDq(k(l-&2nav`PoxC8noca zAH#f9C_u~ea$_FV0p}nr5)?6c12l|$*8CV~BL>`8MY-k*lgrYIf)E}yBeg+N<}vVx zI-Ch&n{DCu;ufOZim6)gV+&oSngXQ(j9wIWpGUpO=u9wUi*nP7xH^^ zGW1t_S{BYFbhu((n%XlqoZIPd%K6Ny_1ZUyPp*C{l;k7c^fs246BzSpd?2$3#) zM>v1-SZLV&&k1M&y$)cSVEY_!uQlB?feQ*z!FypSd%z5bJ{|El4;PHvI*{L6(z*oF z-i?_{`~3*W2uP|midwuyz7x)bmFu?wC@uZU%-5w&`}70-1aizGJ4?UKohzwEyl+s! zfPW37UHxp%8f{_rE7bS+<*p$mU&s>?kn9#;O(_AOx;EBCBb+YHVLEs(bPimreFm^H z^c|&k$>fUvjFQ7q1;)l{rm{GnuCn@QIQwF@xi{DTTWKM!@E5$-*UsMWWQV3xFt2rYc9UuGD&D%uHw_j+&R<)h`ZSU( zezs+-bspN>L?v0e+b^}Wew}9#IN=y=0-V4HUjz0Z)ZTW@ht7uCh)$=KP#ibWSe1u3 zOy+ficuK`XBqU!d{gV#D{gZ_4?bQB9eJ+dUBdawr?bh)$(Y|oHos0fd|A4zw>n-#s zZPHHvoI%aY|`2xrL85ORC~CXYb7ki)&upJ|C2S&2Gy#JX1@>%7WcTf7x# zStibt&TUQn-yGcwx4cjEHi#qf1~lZb&yW2I$%Q#V@=UzG6pg69u%s?C?UstlZx!0? z|0r^!f{28cjG|f&4cd2F1;1}k@1?W>ObW*|F+DvqArG*`O|{7zZzesqGMAT4fn2)E zF`43B>>v^MI${g=<+{@i$#kVMsmQe^DsD3Cix1%e#Auc8POYftdeXSiyPC+ZLj=FS zr1PAaS>vPp$*alg-PSz8_BVay27;t~SJppWnFzx)vYg{DknbdoYa*AML8a%I&)>`u&%IAqJqSVW#(GtHh7hEi4{Ax3A*`NatNWB1NlmwWfl3wRB}AFG;iD!-FH+oBd+x8a#b!gt_e>Vl9;bL6-zTnm zt;;tigG@@dI{VrP3=uRIH(7s~-`{5gf~9n8NXH|Uh}9DL>L zh|g4ZHAsnQppk2x5u#~yEa3?8+Y7GL6+d_Mh+x*h45b2E&m zjqmqIevNezof%nWce^1ZI+grM(hc?IrKaiNTyZoJJ!F&S18`axaR~$F@FC?`?}6M~ zSa<|NsRUYRJRw#*b`sE}9Zad)7z76}E`xVJx{$(OmO{x^C0=zmD}j3RF#0qR){OW| ze+=qitB<}0Y2RXVu%XK3BY*Ns^Q=_#AK*l&$hX?E#+8_~O(HIJ}@`yHz^M9{6i zcJkQ77#OA3>L`shwEp0@H;}X0SUw`f>4BQJzr%eI4?4bN{)Hy1tL>k{WMp-f;MIZ^ z>f&*jiFdg<>j1uB!w>9F;=TR23A_RK;LUxSC zy8xl~0)R&%VKTGTFdByv4RpMX1s<@gH{Hn;%Uf%!_7&F75nC5_*QK1B+HVZf6ws6I z*k4F9hJTi`xzr>~5V)>W5FCQhiWa5N?!6x4Hycx@!IV#fb%&pPU^A-rnlV%vIO4FDHz#TXrxK3w&!0i0mqhv9rvOR(G)3F z>6iCSf7ZH@iQG=G>4CQ-_8c-0e(PWzc(AFFAH+PW+)gZpuPMIf_0=!64>-45Dhykm zAIaY%*{HQU4kP+R{MX0iBNX7T@~j?4ftW+Mpn5&a9QY5FoxP?_H7DDR@9Xf^dz!5= zDBP%F9CF(s#>L3~ftekd>~LiaWchj5-~V=m{$Gj8%>g@lE{5}W72GkZHO$SEF#lD^ zh@oC1rU_T!h{{=7`o2`W(Hi5tnNbY%L#0Ms(P|hhf6sr_LG)PggQ>Ymj7%b9@vjS- zP=rSHPV<@Md@SCnA(ZV;^sjer^uMZ&tjzqGh*BS|zkhTXk*OEclo&(1Cw~y_Rbb}Q zZq=7GNIWux37;@AYS21)%3Jo7*O?z$a_hL{ z?Qkfu<%wdVwI8%vepv{TzMfUZCK4!yFg>^!`~W(UPZuRGHCZ% z>3e?o^p_FEqeTVHyk*88-D2`f95@K3B?9K~$I6*uVb|O&;bQsKR5gF|7xHs6H91Fv zR7Injcz6@Ir*nQqf6`sibA3US;pI$z+QERHg+ z$?wn0OhG3J6uU4ZthdRG8$l4C@&x19iDh|#uPTEouV^&p3daWyRhdP7_uk?!+ME!8 zs{BN**M#a{0P;1+$58F{yx0|0%w~ONe_1Y99o9Jkpj6sueGhm!vn_T1+}@f0T$jra z_ES>0zIjs2Q{eepORgE8o&*!Gh#4F~w5zmtK=bze8(Y=k#v((QLtRh7>p5Lp?Va9rlv^BOdyk{NQkj2mtq1nFDBp_wsT_A)Kwgkjz zp^c@nnmp-bwSs1@pXsKn09b83`(pD^gP8^og{QYW&OS!rYZXuUGB_oo=0*E;q=-z= zwe^_+)~_NUxCRdpu)%aS^U~Y*LbdnlQ9nsrOj~GcEw7{5&P8(VZVnR7*5EDrE=&>> zHP%Hz>QkbpRuGoLa{$&9-E|%twx_YbYUJuQ&|4ld0)WqQBQ$%mC!wQU8J! zvw%qCSlnAEIF$%y1K{3WJU*HlafdSEtoYagI%v8IRyNwZRP&9)n@Wf*>ic3~=zwf( zU(UgP_5S`xE37W>|7mg&0k^jk%mHOiNM zmitH`xdKmJnV~%ywvo&jWV8sat9kKhzv#)1pwIJ{w)t@msOPu{5}s+~&Ir+4`0Y-6 z4sgT@og(FWhHt#2j>Zp^bw^R3-B-NzY)`xT-@wM1d z0Wy4-w8M3+=vHJOkdrm-4T|}E_kVIEi20r8?dkBQT#s`s*7Qr%8lr)&k|MaNG@04K zw$k5Z4At3dL$eKHI02K|KxAc+5?H0;5kgX-0)v0K#e&^F!?UlnFO_=ul5)MWIel~r zmx|{u69zhamp?2zd&zg0_d;Cx@ zP@y_bTy<7YHszeYw?vOu-);G9%jU2)Xpl<`UriyfPb1l2fGlk%^KbwhAc)a96GXM+ z<$7ez9TW3+QP%8~b3SkF8}-m)X>W1n|qniXnWLLPT`T{X3c2}Y8Z#K z#3D#w$hX&!EStLC4fi7Brm(7?QjdE)sqT=sayNl-?xU$xc}{u5Z3?#Tk@R`$K=a0J zw`N1tx3xoz<-&HZlAi{Y9-V!?RaRj$*Aj90x6{7;Qg>LdE!^? zi?JVGykY!y%3EKroNurToqdwcDGjdQrqsS0&(oG2_4Sp$wuE1IxIP76HW7t%a_<;& z_GKy2>KUm$D4&!z=%Wht|0EptUp-_&K)DY@LBJ4vH{iFC(gSPYJFfCB?J=@Vv%V^W z_C)1!$<4P$x!~rf>{~$O6mCsBd+OrpYjX$cctAi?i<}uJu@$3<>J)PoGKsc;>RmJi zmK=Q$MsNQGkU5Lyv6LN~G=Hxv?*uCL$rZsPR&FVC?aBW%t1iH?Y%tH49DU3Xcs2Bz zf^{i3=i5Lyg7Pa=#wnQc*?41T9Gd?s$G@ljx*!>($oW$2gJ67*fdGy3WL$syNyVu>16En|NME^R7C#_)gl z_nbU?w+?~o-L-`^RxmtEA7gLs+uR<@)Yzl%Y$ig7utPiF&FpA-69v~X<>R?{}}B+aqXSu>*2t-KU~v|!dJr# z0L(!NpM?g;A{77(Cy@-MlE9_iy+VC}Zf=UUoAG#-w;mh2Z=q{CVUv5HASA`#`i}Fl zifJ~t1zNlZ9t~pYbQ+BTdbC&Ju`_ZPE&VzgeMkl%EKl*SP_Nnf_&h>ETS<3jVppiD zQZQS&hqD3)$*JDqkjqQM;ZP`Y4a z#3Nf)nD$TM^w7Y6h_pH}q4*$8W$uoc%{dQbYg^_|$jaT(%VQ2p1vd^GnKK;pYc@da zi@I{S{(m+uTrb^$blPbdPZZHY4Ee_O9Hb!VEUJEa%Z`-hTRsw=8NgF@l2nHHU8ZD zRn~Nrh&a3dgR}PxXFGo1zp0k0ReOuptXaD?60~;FQhT*FF>3`Ot=2AzmZGJtz4sdu&j=VqBpW zO~^_^KzUKuX0=NqVQ2wo;WvI3QI+jllr1$uuQWlQ3Rsc+8S42n&vb!wPk5$qUR%x31wbFiomizg4<~(n|KU3*awkb) zQ0+0zcE3|2j1(j}-1D>i$pz?1j(m_gE{QHBBbodJDE z)nrgm^Y!cq2`XOZl-k#b26QUPNg#34!46fx_Qxsi2`M40xifZ7H`Q<)(`7y#fJTlbx!emkfKvZ;p?vXVSr#dAM5qycipwBO~*1f}=*j$N? zHAI&oRUgE11mAukvG>R=T=CA~nF|aTo`nym^=Vl#i%4*a~U|eMj(%p$b^{7QKphb3GE(H6mm@aOjt}Ja?e``wWhEW~f5E z?R6AYZW{W{`x4~q#-n`O{^dTxr_NFStw6ZQCimQgOJ8Vt2`HF?ya8u<*K-Vl*Nb(P zfdfWT+Ik%*Y5ZkW$-NtjSQ~8l?Moj@IOIOXzGuIdcPtCEQ_ZU1zqm)_G@T}?RRcDT zhH2@qX%7|zv*<3RKTu@cDg3ZL|K=4@X6>fdicT2!7Jr$TSpNTdlKdY(+$b1SsfMK_ zV~LOwqL2ly`4XL=E{PcrvdOcb8yGnH>YA;>65I9RF=v1Y`;{`Pmu%0h0^UABF@D+Z zsyHV^E&3|?YW?x&v|0YW*qZ4$_gH1xYf8E+Oh>fkWUOLS&S5e}S9fbXjLeNMPJ7d(5gt^Aw)1MSY|Z5M zlM!Lo5pexJCO~aKwFeys^TGM&xK#m$y9_-3j>f+nyV#u^GVi*2hYrl-59Oen7=n zX<LJ$75Rqr)+E&c-UC~JN@IYTVzOS>ET!GpXI@%ac5*=P1saD3@ueH^eHUqZ zSXCgpw8R{vKiY+tjZvz$!VL7p1@G@0n`~g0Wpkl(dD|CI<;5#X@&--ijEq+>fgi>r zIR__~Dlwo#=B;czFnoY^k>ZOCK=RvR-86)|tszw}UD{p0SbzJCTKn?thR+Ar1+hrG zn0qZXQHI@y^)ZAONhXYDrnxulM#MrIXTR?J3@@s$%gadXm^rue+ECfcS_R3H#}GJ6 z5GVX0@yX_Au>mmKlnhc5m`+S_2l=$tq8T%jLxE`TFx!x$Z`0StyF$m@M7FAIoJ3*z z-vb?EaR4rc7JJMM51cv(nY>0+c}N5dhCD>n_kW@F9PJ8w^&^bymhNK0v;30<-rj%}nHx;kg`1zq4y^D}g(OzNjmkCNu!LvhXzO`( zXQY34PEEXFr^?-hQ(lC@O*)CC#k-EuI2BZ1^2{ALXqB$eD>Xn|JNslV zNaxLLc8A4Xo|`-8x&>voRquZ&>Oj4+6t)`36x<|=V0qH(;f)VpG#5^{aS&O5?`ojn z=3AN~z&s=U)r$6m#tZT!`I_-#G}HCnm_Nj?BtH*k<g6-jZr~EiKq;4X(P^R# zz&7&%^r7i0CtxRTegvk$0oP7NSte;@$|e&73^n*~=&e%Z>ZQS~^~Hk$hMX9s(_i|& z#UAYi*ImW)XnxGwVHXd}x72{j#b$w&3RDTuAD4})y$Q z5+7Hxk9WWC$joOeN_ugZ;y9kft}@V)e~p-oU;@(e*zpG_n%=N{?gGQMdBqf=0S>Up zTz(%73;Mgr73Gv7yN}afa;{5Ij7J#(bZ#UiUbmRAZqy6r?C$clU3JU|mty_VHs@QN zJF{_Q$t6F=-l!-6WC&U$wAhD<5CRoZWRp&>bsEV{j2_*vmO6BlRo!#Vkd!RyYQmi>FYHs*X_COm7g3G-A^Z@0)^HVkYziS0H};6whyn3?gRj3fG_Tr=@J|wMoOPMY z`9qX;hHeL59os&>a_`{_{qaUmbtfRHwpodk?jUjluy)5AIGPx=K8~iZhXJ^r7?&m~ zttQ*$Qzwve7#!Z~#tD>Q= zI$pjQ&*P9hNy<>a{FSjMxz5j0MqDj2?jR@lSD2(zHK_~Yd;FZ(iK~Pkc$$~Nptz2# z%1y|10PuBne?u_dv?nq}hM*xWi+++72wHNf*URFfHx^ql{U`#GVIx55fZRU~aGKf? zo=yP?vaJWW_?Rk^RnZWZA3S6<@802{W+KPwCF>{lR1=^KBHsDx6P;}gG(xK{?!&08 ziA!i^bYJLA*mA$NvB}EzHg)@ni|2EN{tPJt{cT}&$IxrEgZ0ET)Rn3+(OgAp9=63 zL%tHv9dYiNm`N65gcJSpzV?vQj8fuG!iIEnzhro5%k-+zJ>?{wHQ0%!QV*Vr@OO?R zi>uu{PgNWwhLpqaFun;p(ipoKd9Oc z9hl>i`vur=6R*N?r&`&l4aPUCp8pI-0{LT`bluwIMy}`!wOf8>vE;mBJR`G}VWO$N zFhux;FGz*>Fmb@B`XKZ*XZ|!W$N(ppdhYD6j}k|e!jrT|cIZRNyo9X_wul#FQa2SH zJyxs@MV#KpY5)}oXkfFP3h{JWD#Wy=A#XF$0VYo4En^=3=c_23@wK9NqYAuSzb-Ox zVOKBFRQ?eMvVecs>B^oH?@)g`T)`6wmZlr4LInl-i){6!@X6cj@Fg?!rw<+^o$4O2 z9Zp<%6@~%zrqPlpakfPNULJ;mICVAtAB*W;E&jJ!S~EP*V()9le|>etWgEq-Ob2=?7FV$^g1NXuNi20eZo=0}w|$CaXv^>T53ABLv(px5z16-uq}955 zQ_H-Uy5snUXO`{RoAEG0+&giua4(w)6P~MV{xD zC#DEDk%}DmES9qTXV+4NoCEGs>{LVHPQ<)27!}4N!A}u-RzLq`&g@+pSd;9iF2fpe895wP4eu`Un)cidwJAG^7l@*wI*otlKbW}<++SeS2FZTPQ#j<7$ zvQABNDFZze>q`r`nVP3RQhss^fDV!cqZd_c;}PEFU{)9u(~p#ymr~?c>9@@)R!Q~= z)|Zy9t*MbNw8nnsF-l}x8U0b7s$6TyU5W*6Cq_6R2w-&SF~|{7jO-DX8@Z~m^uxtQ z=B+Om^OcF%$E<>ztgZK{8^My%!(j2b|4{hH66Z08Ko@1g)oA`1MR93(VClwW)HmZ& zw1Y!b&7d{&2mXO!zRkEqlfYhlTV&GRzjNNBvG-BSZ6zh&3c3Y?7p01--8()l`?=BE zyNz+{3@&cBT7)!4F-&rhH}a`wd%V-ANkX`z`XX3sKl0Y1k~FKekx_{*Zp+4NcB2#g zxTyA{$YE5=6DqwHb6cp zDL#ST{`T-h;mBe+hW7rGZ8SCHnQQ`r4jzf?>)RhhUYi4RllTD+BHZYkgx8`=f9~jh zptsOi*SmtoqO22d+4imVoeF9--dpWs2%-p0LNSp8kUU7?Ux0s3wHi3fESVJ=7D@;M zc{g{>*3{Hgozdy9p1)XC&(?8|aZagcT=+X&`%Ob=*O!K{PE^AB$94>*N+HFFT3!5@ z)sIqVzK)UEO|OlzHp^OV`bX2a@*={0MmaRUS2B->rw#fOB_Q!+Hj+-$Y;Y|VF>NTH zGm2At@mO7_&zPln6&) zA@QO4ckzWpnaP9PYbif-MUVxUL%WM7U6W1G1iR!M$6)(1eV3DW*(fFROTb~g`B^~X zvJ&FGu7{P42bzaLjAIqMxdI=c)qA0w*#hD?i^04y2S;Ik*?jVY}MJ#S6QB<=?*5+?fWeu~Fn_^orF#&-dt6ouP4Ifk#LWH|F{s_FvI} zARhA)X5Mc*QLo(fPwWjz=o?9G8hmRFmgMk4ZJ#~fth2M(E+k8MRe7PA*ELzx@}opa zV7?lr;DCDzO+}s(V3|Maa@q7k3J{qFkqY>@92fr*2buPA5VKzRMQ0A}$!(~Y?Dfkj zNeP+g2faUo(}*IFB$60zwNE+1DHSeC7RE8fsneAHW(p}?`>cF!jzd<4mr-|E^cT3a z+to^4y*%)Hg$sSDsoD>`*D6o2;5!t?iCzbrmjcB)453Wv9{nTPg7YfZw2B{;tDkd* z$zi9L45##bhG)j)eV;4}{H}_g2I_-kli@-bQyyppB&I8BOZ@AFSX{0D!*ga+p>SvF zT&0sZcZM7K+e$fe3FiWmsG);EyaXcFi9<1sd6_&}!0H#ROq|#s5D=b*KX55WeYMpR zE%1W+Gt^sz+rmVfPw9WK=je+&M+4wW;OtS#7|u9bEMXtS4vS{kgRs_&p*GA z-#=J1)B7pJ;kooROR#uz?ZM^3^S;&S!5(JP{g7hg5?E(%YuY9{r6X;ifGLkyE2J8m zU*w;%T@dvvMyM_ALfc8gI!u>rd#u%7Asdgzneyv>-_V$CF2yUqhAH+FQ|qXni{>G6cK`xcMwr`!>ja?`N-?DoOX4Qm3{a%uOm&P z8GNO1=#f6HRIH_TKyj*${}LRAbF9QConsD56N4{$_nUn)UF4X(-Zj#RttoJM%ko4kky8LtVw1qF%lI`kBq^}-t@B7 z7$;@IF3AH7&F?!>s(N+rArg zdU8K~t7&RR)GepDJ>(`xI(?qh>iqU?b&QiC!cnxo$HH6oruco3Xb6os~?jFJM98~lT_{X=Mm^fGiEid^R%g=*;j^f(`@zkTkI@n!A(i@xb(w2`cdV!erEWSwrRo0tVF2 ziHSh~k!`V;QzhQ?wMG8Ik!;i2+wJRzE4PA#KU%6;3Y}D{tY3Qr=DMs0^S~Cj{vhZT z5p1h_(eiD$!`fu&XDzK+c4~+6KNTJ*su-4Vj^2X%f$ne#|I&-ygWyckK+u94(EcqCG~;Bk6GYlLFVWt;8SGUHhZHGW(A&BZXm>$F^rA}84gFb>197OHt_Q%G;gMYgVkNF)&-fnB}e_8WOOwE&O zWA|GUh4}ySMTGwU+{A#hBmwb@FYP}RF1iJzl_ZLbtR(R-6!f1M4XQvy+gw;@d&~^F zzvE+LPIXDP@zk-b^n%5`V3r|n?NOK@Ys~dM$*--z2cPgCiVe_28zRPuyBJpxBh&Qf z^R(~T_)`k5P4|E9Oey(^ zMPVUx5JdfqIEGPMRcUESlo7q*R~Vf7+fj@r&ec!8ZKq5rtsMRk0U|}>HvaOMHh8M$ ztMF|Fw-jI;s0;D#lNi}o%?{3F-pV{Xx^3O0r8^&fWW+vJxv5$=VfJ|jpy=t@B8YGc+ zubTMsX_SWdAT5yiE+_2zb1X-DZ=}BNT(7zNvw?=5mY)&2?UZJl7TX2P zde`2!&5=T?h?1z4YiJaj&hCM1qB@;e0%HZ<`(LKfwUl9ApB)R>$P3==#3WLV023Xe zk~~`auX|)9WPAjXoNaUp#U1_(_yu|VGkfm7Z%DM=v9Byb_WOmo6<#I}bLjMF_KZP$ z6!myvJ7d+jxjn}@?{U_>6&s;MM!F8&ssox^7Kxh{pH&HH+v&qIUfw+Dr83U1rK>r| zqov*6BlyKF@h!ez2JC-lakbdA1h67`)#<7q$qfrh00NP0O{NKsDgdKi{?z!rrvPP3 zvhn=kAGGk_EBD^CV3&qufz`G5>3S+Q;8QEgDFMN$&bqme;PA9fOZ!PIEhmq4=$2v- z^v#8O-}X8Nx(EL430Qutt8VXXeuD~VLAh{q2fVFqiqhH#m4#&10?iM{u}bT*hVxwp zm~kP?MXs8h=ZFfsRfoNY{B1Fu0Vki)vcrs~{IJ?LBt87r&|(1t@{S!`005B~0jAj& zB#yl{o`k4aifXi}p9|r4xV2C%y#am7$t?0(OQCXb%;&VjO;fyE_E5+RQ?Lvr{K6fz zV#dS45~PRoJs2)rd}0?3wwuS;-)XUm?^-ez42h`Z1Z9^*8uBW(& z7I*Z@Qq=Y0JG7vZ8k%CVO*ZR@e>hXxgUqL60MDtJhJ{G<`umm1ADrC3@k&p)o`TUt z^3=|en2evrp1?-BglZEV%&m|cYRit zWcm1>1L56sH)0yrZd?M9kb4|UzD=^kKa7|>Xk<8#5t=W}ds>GF7y2o&cQW})_hf6# z_Z)EzNF>THi*LT8fEc9rCsb8crBX@W8Gfd#MbjqqX=V|%GMP%2UV%r=cWJ9f;u#lF z`8>qdO%Ux8%oc6>)-%%+uw9oRpz6c7Ee{aYvW?=$$>_1I!d3ZI`?jw&yjtcz8 z7f|px-D;f;^Sg$4$~an@chPZqPJr)4v1)sYq=8SH+A){bhCa}0S?wc zzL?t_NAPSau8oLH`?dpmxi;H2)s(Wix8DxCi>eNh9iNCDa@B@UJ(@C|BI-|#3GsMV zEW_lh!(-B&=N`{7%bTZu)nDrqz+)xv+dNY(uC&lY-&y+&8b>iRwuiem==p~Q(aQ`Z z=}AhoL~im_)(~w`sA&1{Kj$%AN=6RHP}Z^(R|AT*3RM=~PZ3s=NEzY>zJTVkP)ImkCvsZ0 z&y#u4u+SL?Srk%uRteB5Ll%y6`=-Biyy~ZuuotST>vX&FQ;jP3pU@1rHv9j@*sQ5n z6jSBUK4W~Lu=^rtKi4NBrC%ATZ7P9^QKr5X5b-ubfZU8fA8=aTAM_a5xH_$98owbr zMG7w>*~Nbccod-3D%sLGZ2Od~lB66v@%FWr_fo8iThNEMKG>MtT~R^co;uNa>a1j7SH1_m5unt6P z5cSGndH@+N;ntMTpUVGGaR09Ka@qbqD>7JEK}mpK6QQScWyl#7bmmfWB{>lBz)ylQ zdUb;Xcb*8>5<@Wo9D7(UM31U9IOFxEZ<2k#K@4Ynpfb*OIn8dmNAaO2(S zS}Z#`0*xgHymme~DGWbcV`J(ZfMc+xM5)%;GX@a$H4!Kq0t}Re0-?N@qp8vc`NP47@lR)g6K@X*@(01{RkG@fLi7r zW;KQtO{zK^zVfy6u`=qIdow0Ja*0_1h(F!g)}6T+_sGh)50n-`x1_$DQC$4oSyU{v z>$;vKbEG}g`s~z>uC?U;7W=935Q)ujVQ9+>=TDMEaBq02`X`apHlv&d_QN~`3Vh4= zLql6RMAf1;f=6wSgg*)jm~5g||J37lfVbv_SHt?hR$OI@2TMC$1M<2Y&6ol@*yJH0 z!*1`K=aI?_2Lpm&^fa5vf)(?3JLx-sk6QjG)$ZJazfik^^nBmwC|ldRIKg1V3MhkrhIt+uTkTmD122;IMz<075~yk%C0ACpf?ApmHK-UqtQOXQoV0OV!Jb zR^O5_J}ZvFh4rp6wZ@%?BlP_l|0l*y5yh!AmrctvBGl2>`B-jPYggl%o|6i|_PPN9 zlC%VKoZf&zmt?7Qfg>Iy8G{JNB7V%GYTy@Df^p(n)6kjL)e-B|iEt$p9>y=767kMY8{AL<~K*#^u+vutIsBhXs0srtzgeDx1-F zc@d+Z+_Ff4?SMMPtjNCZdPX=Ux}f5Wg6&rMbs02nd!V+yOe~tePT$ul9Yipq;a&?! z&}B0Q4}SH{nizK`b5qAVE39$c-^bGZmaT5qhQ;#S^N1bx6B2Wu#S0#b-}CQ5P48_V zn;E?5)jL!^I*&M7-f<1LuX~)E>3C|oZY<)tS{Zp1v z#2~P-eYu@#d}G*N>ZU;a_`qCm$yEhE{=1Tc@F+lX%fj`M*g9~vG3qv3pH)weR{J_` zbu66ymC0Jvn+mb6t=fD!#L}eD%QDW!B5SH5+m$hYcGdCTlo$@XZ-9j`AQNwQmXvff z*VhLM9N$em|Lfttupaj}v?cJhWpzk5n7ptO8D;dSJ?0&fK-W?+O=~xZA1C^Ce83TbHwd>W}B^k_d z?q1WjVpkQ_bZ6f&1Ou_99`ECL&~;z+ry*&ayOU)^S(H3%$YT1y&!uDD%HUx-d7i!P znY*R+r<&ObN{o%c4^Yb_$_waqG=2KouDzq-^6Sx;M%=BqsixfIF^W&znfG5k(YK-V zaCukcagU<9oIJP^+Kp4)!4Qh^vRi(Y-_Uv@)$8l$Nri25{!&HF${E&13ajZNjSu_Z z4Lki1HNI#sgIiqC0T(CbX#VM=v)TlMO%NTbP|IWUUY8~c#lj#sxPr)`d>|R_PcO6i z=xQ~+TkjxKxo5QERM!(XjfoFf5aV7;ODo7FA3J8z+8(#Dm9{p#`^(HxZ@6DYC@E$i zYZZF{m*nXAuZ&C+Uy6{Cc69KYSNshMbVz>TdXnsSEi`UIi);7`lSb2JB8l6h2!_^n z&>P(O-g6T>UpP-<2|)q1p;+ZIo7$hPt;^e*GX}y9Wj3LD!VU7`>#uUEfO``JPK)&g zZ=)^e$+x_FR*;m4xQMw$RkAx?AmLGoNZ0MV0^3yTNQ=JMkc}|^+Gh)07KKFTy-@}$ z`>Cd!GY#}}0IX#eGk@Ij(z;JLL_YQ^i@D$x+I`a~gmGHGWMNBru}l*o?lKq^c@qhJ z7Mk~p`uJ`ilYskr`?01#NZ180@do+AtSw{J!6pP4^`lkVJ)4!aLyQG@J*9*l@1?jj zy@p2#-%1n;3Fmy-v-vq2$#>bH$RrS+D;uxQzFF~mW05pJ3W|FGJ>$eoX-tsxMv^tv@S>&H_hfyJ9y;S7l~+%u&@gOG5MYt zz}nG654%b1JDLPkd)8lgl{Ek0$n~%RbJ+Wmp2*9Lr`9OK$3o3pPtAE#@=6TctlZZJ zWnTjS1E0tgKxJDIAfH#09^uXgJ|o-RN_1SXX74+m$J?t+zknU693<LqolSJP;0hx`V%voYB-s8uK!PP3Z zQS$p%(}y9}RRv~|w8xkvy!1`X(Icrott;<>ZaKWB5q+ti1X&X6Z7HCJ8*M^mZ<6$8 z714Z9o~&+c=f1{^duL4S>)v<&a`yPTP$m{;Nmth2)+}burk4+q0w`=WM5jSjj!q1} z-XNOsI9Qs-7}qEk+uZ0u-B|3^gU3K4GKBNP-W5-~@w%P-N~e3b76xGeixXY8Ok(^6LG^~J}gKcgdI8Id! zCU8F4yXnQlzBef)7NI#a?q6@yab=a4s(b}n0jNO$z7k`FVhFRCnOQKp(9qOlS^Em> z30M6pe1#U1dVk-R%!LPFIS{Z0tM!bMf^S@oRH>fa&PyAscT7T}4bdVb7N_KB<~ixA z>Lcl+no?br`q#H+_!06lt1Do>7n=Yyw-1BOxV!}EiTjjd|!dYSo;Gh1(OFVIQ`trwj&r>P0W$=@wllLk|U}#km)jnHIwJXWVh$h`GEBYTpBi`Ttgq z`d03cy0ba5Dyf7k{R)}xoV@>1+x^MNO}PGUh8RRRLCbvc-t9bk(?EGWp2m?|H!2E6 z5_c(%{%e*af{)3x`*@~@I1xflDa^Dt?B90#g`yXNPG}+fNlP36`?iI1%DtEQy{%FG zJ^$VUY$?_{V87`O%L<}b4)9#dCOPuXksy&6{s$;DQ}k_Hs3XPf(NR7##`uO6axR3+ zofiT_uY}3zCNi%r)V<`2nm+ez{MhteN7T1hKBZKSBETr(*}u79%D<>$M6U``7OsO+ z>6exrjHbq=WWtT>h|wXP;!3`vXrug7?S=7)vU30D^agw^E5m4yiNA&q>&I?|Qbhb0 zLk9@P9MfHqm>z1PsH_#ZV0dv3zxYXrx?b3CrqN~;K|xQrU(jB+$AvjdK@#u`7iVQ)oHgt z^dxnBYrj7?P;>J~GY3(@=U>WSX*ccYQ}coB-fs{oox)-!rHHO8uq&kV3|p$B(1 zT*j&!e+3?yy6OND)~0SjU9ZH6I1PUe$PPDulW)&TJ5RmaMH%}l6E=*35bAR5>6Vg_ zul?mss-AH3LY`?wDK+?}T4_zL-y76r)R4aiZobLIMtNF$;C7)(%p-_h#v@tbYPiXl zB$j;1rdj7%l9QC`+t3zED}>vpmQtZ_oO3-hSFcZ(Gft2Gvc9?kr^D19(BdV{amW3? zi0QZ)?)ZKL15C^B0DcE+%Jkcqy1UTHRBBNjRO{DkMALg@`xo!pN-3}NDFKj3tim*v#vb@ z4MGH7eemOnKmGQG=Z@KMO-ey+*%u~YJs1-NX9jdu_u8e}?*&X|zh_KP0@E9xG{BYsvZI6JD*vI-_PztC zmWaQ4Z==c5vovINplN08zr-{!nha*W#E|y0h}ZDWAX?asqhXP@bfrU^Pao$RGo$22 z?v30V5}UFDzTj7U%)_bdhGnka70z&Qnbh^UT~5QbLsMcL;7%NEv!@nf z*H)i>BO+d@5-z-AY1@fRHqX3Ut_lmEE2_cf8BK#%E(ZzK7sezo-YV4xWHML7Lm)Fd zPptUh6ZuAwwpkK1X1WnUPLkx=9_B7LoB~kEv*nby!m!<3MKs5xp_~|7Q@eP5wk_t= zA(4@lbloNk0-DG=D9rEaCinAHta^-Y6q8v`J3gxz_z_NG))!)xgO-|qNbmE0cZl5SCf`?>Pw-RLjdSOUpS zjs-xNT~56%cG6-@Sdx+-mtUQYSC}cOnK}1sngTMpQShgt`$g@x&4uC{_|6&I-9M0f zNZ@zbs~KYY+KCF{Ud^kI?^n%O_5xG2;%o9MeiKso*V7VYZ8y{=4|yx!bcg@o&e3Ty z6Lz#ZUh@CG8+|u=i3}lrz5qv0fhiY_vvL3f4>4+s4w2N6THs31Ieweb`=$9c$9P?P zy@Bl59lt9jdP)RxF|M$WXVxwcO-)q(vtk1rSax|S)H6+5Ycx8V%*^9X9DAUwH>d*TLlR{o&^c$#;`OpCtSO^ZY~7$ zCK^01j2G>}-0MHROpQ|Va5`vyZ&y@;rQ*GLd{Za%ZOfTAV0DBttsGd+pC8i!TCbnNpzL`0;Fs!4l`@LvSITbXPLthhL^SfjG5y9 zta?_PNlO~UsnG>_c&0D)Cwuv6QeS^*ug84j%V3)sn=9Xq=dEHggU(JQ0+>|A+TStl^*KTP{1gs;|&)L<8?W0 zCzH^RqaNx#=O01*3BJ7`BI4rpu+VeR+A4)S3Bo-=B#;#@KbfWiJ}V|BPg|=rDfm73 zo$}oURgp7tX7yfnioa+YfxTNU+4b)ezcZ^=&z}ZwP!eoW%SE0%{_%$VDn>JZ8rh7l zx(|*@y6_neWQm;2IV-ulUk5+_(NAF@CPHR{$>=Pycd>4kII;1stbD#Q>DLtIKR$JW zzmGm8pN5T}hHSqC3j@~Qm~@inYr=ElbG%T0^c@^B4U?x4=`bBFw3jQ8Q{o-b85M8u zAV0A!t#lNcXY)9%Njcv=>%M5>H*@CkNI;FBys)w+-I{g5O~ytG{>~R5CPr1-|9)(@ zvh39{w!GF`VpNtF*&7vjtvNT(&7jo;Yak@}@a@F)vu&cm=FXf{&1!EnJ+xbS7j5$+ znD>Fm=iVDUu)MJ!FFq$`+siF|A{BCkAmWjHcsi31Sq2!?99r2HLc4Nm(9#A?F4JA| z5aV&^s-w`xL79aspqj*c~EULvaqIhmY zbZ+ZY^JlD)%BoK_f8iC{!*A)#8E&871Ix!m#O4>WI#JE-!Y~E)8)zVDMWxYsJ1XMB z;(|563tQUl(}%|!Gm<@rbT;|Fe!V^5v|NO;Rp3WmsO8##X#==emL%5Nc(;I=aUW7E zOCI8Eoa{VX>ej6Xf91?@Ti(S>Dzx9aezmgzdmm7ZuX#FN@0pYNqd}!Pb##`9oGzOz z`Tfq;sBC0%Qf)BF%|EC8MYy}hgeF+9>#`_X#XAr#;NXHSh=Yrj?gI^$KEYfhBRA^T z5&2@%M}H*q`jh_A8I@NZ{)b}bt6U$)@(cuC(EFTBKb$Ss^>g9g6uC;WT>y#q^}JRe9UrwVQ^pc zCCmTqJ`wm_H_gTX(d(af`j})I+tPCv?W5@bP-xy){4WveN(SH+y%VDaIKxX#fm4eL zC@)_5mLyI&EtVruzT1Ce8_ee^i!SZxJoEh!w9|SM?ENrZZ%LuSh{fJ%v zSV1jimofBGvNVy@Zjq)VxR`9`g;c{SZI_p>z-%up8huTUjy8^FPS$i3gC5P)GVYC{ zqJcUnK)*-{gsQ|KC4NUJf<@*@dZqh37VAG>n?|Ft% zqG*QbUrS{liG)-Gp|UGE_=}+iVi?9L;ZW*~Hw9v*`bJwh#wzJqnyLqVn;`q-gW;M- zL;Um{j4jIEd+$Y*vkr9}NB=oJ$mVr;TqrT_prdjwnO|A~({-XI=37^nwyPQ+wti-B zN}VKlvE!D6Gn?Z0BCTG+Rtdk7F|{N%IzysD&F&qbYk?US-RG+?@NM%=T0dul<7K9P zyZOmrtDN=ZPlH~s%(0(sgSxfQaE!4zX8y9LA3Ax{wA#q;bhY|&>zDDq?!d#BOp@2x zD&YeS0lExfARdHs0WCZn59+^2W3}H?35Bq!OP0XQPDTam5w5_~&mMpvdvi3T<@gom z@9MvKD`YpQXYR;EhqqT4w)o_w>X(<~HOkVX$Tn-jP@UVcu_J1cQ|0P$)YjJ2>~++h zRgQd%A+rMya#MBMTGD-@OVpNZ6EUpcQGM!9l#5leNmJIT9xia+0}7>#3hw6;YWw^s zkfqAtSB&g9kT(MU(1=92)qbY0^qF-CN!dOd0%BcysIFDWg_(tgi&9T>cSyH@A8I}B95u-SlXaLT8I`W@ZSdeGk}SaH^s}8Q3;3W4gb7HdE#S-A0TPb0v4aElHt1{TB z*8I+_^zq>zaeryIrc5o4x1$-qN3NbGkX_Sxj5lNMMjQjOR~mM1!n2nqHm^gi za1I5DW`xM4TW^WE7f#5Si^n~mbg%Snz^PoHq$E|hLd1osCYwx&cc?%w(d^B44ivjx zun-hx%UF9+uP*6HiD<#MJr(6p(=vN%h`WmW&B_cOGZELvW%|{?R7R9|7%e-~4An9qx>)_ozI&X%+{v#`y2t;9;Xeq3p7(QniywwL}M1qDM&5c!5IZn8I@ z4?j_ig>^-4&f>7Xw>82{7u970sY}@+s(V{zk8Za-5E`eSrPj3CF)Na%8u0c4wV-3+ zdboHed{#E5ASt&940w}+r6aXlaGy&1dHC16B-OudkAH@@^~>_TI)67N75QSkr7Iqj zO=1BV0;k97rLt3^Z80gLx**oqU#u0^{o&2I87%B6d8(l!eR?Eo+u7A9$5ra2spL_) zh2a5N649%wh)!JDsE^LSH~)vQh`i-U((117n63^nmf1GymXMY^_|^12VaM7f!PsB% z%SR9ARbGn6*U$+CICI1s5Cch?x?B1@x$D7~0@`KRHXs?}j^mC()O=}ly00!fI5ljp z{>9iA!94o?epFO;bmSGR8UYU3!y5(kw*R590N;+%_tZTLCaB6osfW=ig6lgVf8{Tu zw^v)a%rFxxe~p06HHnpVC|9g^1VC^|EvP$%(7}0kit}?620Wzy z$kTMk*g?6u`Ie~&KW>wEf#_9S_65zZ0OMs9$q4^IYpw}ah>xzl?5SAt1~b+5^w^zx zwq%UERe)uDGF(&d@F;CC&nL07-2K8hL~y$-RhPbSB;9>Ih@^cz0Kz3X`TD9oSqQdi zYFZF-Ti>xQ>QS$!dN13=uo=5{cj(<*&*oPIW0%@D;>x~#Bj8<#lSP8Yi+am}Pw;L-?zL|@zxDj$O9_)tiK%wnf& z&aV4ZlhIfW4Qcn+!yZ1DDd+kbRa5X?oFE?u`_G=MfZm){juS)Wi`CT;<4XWyJegw# zE>5Q!D*8&u=^b{=^0HDLvN_2Lqd}b` z1&9e`X<{Z+iJXn;L@+7lc7LvtWii$G34UhHBAU-n4D$h7*l693>$&hJspHOvA`9YW z!)hFfNeh>M9t64-H~q8E9bHx;PMhrnFqCyyOSeJj!Fkzvo-KmV}2(ckMYp70?{f9`|A^GBapzU+YGHLtNf`Cq?409(KJ zJNhK;4S?$}HT@68@-6`Tr3PTXaIwo2RRnnwkeizUx%o;(_A?q`oFttgUbi0mzP&&T zXO(cauDr2rCx@UX1Uyhrzn^LxWAkCOvQh^7g{73w^0J2n7rQ|HUHm|jAkKo`knWfL zF4+@bS#+4Ey7lf#(T3rYkb20aCsjuyUg>AFSeo7fT&;@e|G1+$<78Uak=E=;eA&{t zmOhp0PVi_8BNuZ%)YOa!r5WhCzV5a1=ACkZ;vi6(GvGW8wrCZ)0)EEC{vEiSv9My9 zhSskOkH|GlDY0m``F<^FruY;*0WtioZi5d;-h`_yguHKb&^eGDm2NctMI-;HJmWiy zT&ISbm9lCHPd-QWBe(ANE@EnxHBYkrX}wfP~y0Fe^LZ z8k92*67NM>@m7{{q1WfZKd__F%?fQNGXvrILjecdj+wF2%_uqCES51HpN2Z`sEw-; zo%?gCX48#3`|)i>pH!_qiaD%#hN3VqC=4FPwEQyQTOcX3uHCc7yLsA&aCcp-dtTK+L;;aU8uQA$_zgb&tLHr zP&|D&;1nRdK=U22B*S$s%MdP;3AqLROhBNsnTe4fvbm5Fxk(gToam&9Z@#U>?|XpB z676iA^GkPWXC7|&=v%Dw&UI&x>FFiaoVwogqW_1z_YP{ZUE6*^5JaT+8WjYUrbtm* zM3f>RHl!1k4nja$fY7CP5D-v8FH$1C6ChLp=^&vd^qwdsK#2Q(-fyp&Z=U_mylbt! zXV2_C>ko#(2_#BM#j45^Wl4`jj&lOC(yHp-ZcD4*8sOdq z*aQw=E)vZp8mbyk)g%dAA7{RwEb;WT0DBNf7^2!B){lY&KvzD9b?_oOpgiq(NH_1j zk?J#J{jq%kC+#T80(EmcqnyS}j95zW4^JPP&&BcYB%S~q4{dbaD5 z0*YwzErQ<{Y>UaOTT!sR*Y(Uy3XrQHIjqGk)m7con%{XkwX`mVt@3A$ewX0V#1fxH ze^$5-;QYi5N^Z_~kO2tFP>9TF#Dw_;n}*wG>0AkB7bC8j?m>(R3z#hkA+Ea#LID>h zc(xBk7UMG@!~11W9J?RWC<=QecK7&NU@H3Dx@L};p=Ffs#QdeiF4oQvc#mrs!9k0# z6Bdqg1XH);J#kF2prze{1mfF62D#fm<5)A3=M^o!51tjz57|3;)!AC4uuxHJo%R7p_j)B-4~S<1%ZuW*lJH#V^61E1oLKNW?9Ls znO(Y5peth@hBnRqm&}dAv!b7atff}Lu5H3kY3JBBaS`zu$i_5)V?o-E*&K zcYAeg?Y16!{NpUv#2c@8HrkN`plO>9OvqooXrdZT8UfK^e!c$ei+k|W)|_G&UHKO8 zTDT3Wvd-7xNvvYSQli~8lU(S%U+@)c&9)03XIiIhTjtV4iBkzXZ^3MQuz58~k8RUi zGKMDEPVRGh%%_mm+9`K>pP7il(btU$N`>l)7HrKyld>dT6rIF%imqFNq=PT0(7_)NxX_4b?^mcrRJ8v^5*YZg zm2Cny=B9UP(*VDbKG(9n^s44mX6l#e{Dbe4BeT`6CN;}+!r@MTx)2USHpQ+?Y9#7aU1!g-q|^ zn8N)R3(du+NWej|DtDfkJrw3HAgWs5JX`~%8_M(jw0>l;0sCgyR!3u#COG!vcXQfv z^@U$2dL-6hEH#4SFM~KEiN^<+TG{{(RG2shaH7}PE*qIVkEtDUVX+ftqW<(sO9(0v zcC!S~bmaW22A=;CsHOgGmjL>2CD#}KNBY^uWy-?)UgtxcQMCID)b_2Ms3w3g3%^(H zaK|D2Q+><0c;YJVXzwn7IY}m7Ruj(L^&;ISzUSV@rrgT|S*!t{;n^DYM>ik?$)DWb z>b~7Vu_`eX0d%r8Z2~x~T&l+54fLieJlguBrraf}qaQeBAPQl|1mG9)k<3m#iAilB zT0o-~nc@d9h-tB95cDgI#9Xx?+nCgDjR~=Q_W*nM=HU+1DY&0+F~4En6zn~ zWQcz1OLz8J(~ROBfhED0NKXfi!hw~3XP7rj>bIoSI|fdsGhz}>CvGZh8n(02Re&wy z@?!Aj&DywJBbcjBXpe4X<(Rl*ATwsVEnbz;u*&fvrGfZB4^?7krWP_4&RR;YK!(&9O=*ui4u$Q?e#~UK$zvU}L#pw)rzv&|)O=aeFM2<& z#LkodK$ZXolu5*!VR6kb@K>k0J%P$%S0s@@l3+s56nEzrtfwBCUv4^8Zb?3v8@Swi z*lUq#5+!SEsdcVkvRx&A2@wGiR8l-OAPEyK$KY4ToSH4LU^aX5m2?5c=K$u{hpR^0 zZif>x@A%A=Pf??_4_G%_c}d>;VFjr!10w}!}i(dfTWV&CAt7MFD=FLRg zGPY;+u+FH%S9Kr1<0Yq6qt!Vw0+`C*yF4y^xy;H`1CPT&XV!3@p?>V7hvWHpt{^8_ z{}6wcd^|W()=XOWg~6~J5JV=;w;@}+vy*^~R$VnTzl-)oh-h)=+(yEP4TQ!iU>`^+ z^M`_a!+0-fS>=j?V{5Rl_mOA%lPXvENR(TxwyaM{t_rE0{ibEA*pc5`3I(Zia4~#g zM?EsBBa3h_mkU6CV9(KJtzJoc0|_^gvyT_ zw)YZm6AI_UqL>|wyMveF*tTwOR<5hCHjUL-6qM>*i5sdmsWlS3xA&ln{&M7thCrn5 zB-p{xqzzRoAK6T~B*CVG_YZL^yZZYTeqhSkytQI!y>$Ee?xvZ-Yt(C3Mk_4|`Lk^5 z2M^?KWih$U`s#}4twCsJ{o8d~Wl5@MNri-?uvXr66((Ot5~vtvVl=tm@^U@Z!+W)u zA!B1yhLGLR5_vB>p|EC-O5+YUe!Z#nmx)7(j3G2-S_=0xRr{T3TNPy(OX~5V(~r3Y zx!akp_i11~_H={)X3%=)|1#FB{KHrS^s^V;w1Cq11z|l|rbstZ`Jv7kc~cb>2-L@C z{;H2Zi@f`0oPR3WoIKRHQ&3i<5`)`PR8~0rnoTUgn3T`+Q}Q)oc$xRdD2i z;n+ZeLBUew8j0Zz4ia(lXtE5*E4#GLsrIy~GU$gdugP?>;~LDfIn(>xz|-1ui|1%` zDPEKA5*3SqKv7YXNw_Lwl`F@`6uAnMpBaWYBLSZ2%59_AE~+g4gHi~_1zGCH1Pi?< zap#TfS@VJ|o5;iq=dk1-*p1Pq1``u}Sf?5I0kFSgGY>NuD~D#+SBx!J_`C|zOiq}{ zi&yyeky(|RLy6g;9B89hAq{n=0BFGZ&I))^CB@gD{EP|)jCO&rZ3Z}(yj0DHY#;O4 zzLFVj%-Ii9_w(i-*6W-sC(d4I^m1ih!>iYPCEcyV|M=o-S3EOoa_zUScM^QT(HgU; zUZ5qHe5Jk3R8qPDS%AgMMHRpwkHgIz^D%0;FHgD|ME3Nb`i;5TK*bZ=oqFz0^C$MA zsqVQZ!b;x@25|L+Dva3%XmUS!&`RJ8ZADl zw|KH(v-nhlYqwoh((wAbj0w!nQA2x@736vJlr(%$`D#veoMd0?l(MJ(*%u|sa=6$e ziNR}ehjFdFRk*o+++9~-30ehUNKJjeIwbvu8l^8C&A*kd?4`Q#{JitfDYGrFUNo5> z!7yh`e9^YR*sLoW%Y{xc_RMfCbJ$@R06R90TO}99OV3sT6iiok3m7D&(GL6${J~zy zc7q_*@qC7KSG@H`y-6zbI%8FaD+f9mQ+3?dvwY5K_Wty5NnO#yLcC4%z7+K)5QQrP zX1ATcOSp^wQnqZIIDO2OX*wjVek0k^C!CbXJN{KW$b{m?(9g_1L;<;Dfu=g{JPd)B z0}dw2k#h#%Ft0+n4@h!jP=SM-mn;?4-6%37|tk8dTo4hM;9H~>Xf)tLfeQv%6{ z!D)d)+H(gqNA1V*E}?|933%_ayJf6qU0!+qv$Rk##1wcj3qil*j=Hd(E?MD;i(@0H zj~+f^+Qn$OoVmBqv}fC)4bow1|C|MEu3i(A*eUtU_VAYS46Xv6@Kqg z#sk8anwFFXXpx?4Q9pn&{ycMkY*t&ipP%9P!=lZr&@avlvhVD`l}PlLi%T7BGq6{b zK8y6HUtXZ=svz^G<{Jj`m+4cDmecaxKb>ddk0#Vke8-EW;t@Mji$nY{y~XajLJen( zO}8)OQRj%Msc9FrAN!d2yOh6&YRR0_RegKMH~JwVqWSlQ-<)76w{W$~uhEy;))y)aIzUV?opm_~Q1qk; z6?fOY0W1{@vdx^vxkC1G3B~cp9!E5H`_(@^tA0oS144*8G{V<)fzJ_pbm~TwM6HU6 zOS)tGU!EVm7@jhyLsfr!KN+w0?$>3v4|l?+1%#tp4;;TUINE`#r2^q<1of@96VFJQ ztv&9aKmSnJnQq+bxufks7h)smu>C8?l|2FVaaWk`YK0E(@;k5-Kx2F5$Bqa3L=nxd zSSGqn5KDEhtl7;DM>DB}>~vN3_r6~_em#`9!IIw@dNhb`*ps0V2JHe#dxB;BMwpOHwC%~ zl(EY}p@4;&nIvXMxahM#nu@M8V}aKtJ`{N<2b&A^d1A=v3nD#M zuSbs-E=ypnlS3?lLDO@P^;d5@yq20Ge}3)d)tpG?PU#3eoiP!1iN9oU7Nl~qFME1J z7r&sUpd7M&60Kl^aq0@pw9C!3xQ6<&@;FZB_G60#qe~G@PTFh}KmCSUFO2!gX%R%E z7%(6~O#n(hsPwy!N|#mb4w|iZxZ~F&UiCe8`yWx&iF8AXIMj?QRy3H*W)8za1Z}+$ zq1(-3QZWMSGJGjAYH%(w}x7%z|Y72Cia=V$qkpQxF!M$%Q7y_x~zRXs@g55J zfR?hBXnM-O4pI2WnT`0*ox=p^snh7zFNicNJ_f=n;CC|gN6Ov6tSMmzt@SZG)eXB> zQtp)tzI1bYUV5>9uf11UY^JTRZ8^OixNVCIqnbWRNRYIPqwb|+?A+!UfBl-XdJ(27 z?($K*Fo^Q*<=C8nyU5U1;R%&1a7gv|%4kiAGFWqDw`@6+{9RjLdQkk@Go?0qe)Z19 z*OWuXlSC9j{wV;f8)BZdo!YV}Q7y`I(LAS^F19U!=kHd9VW@jb%KLl(DVEE{iEzkW!sz;RzEIN-_bGH(rz=_j-! z^WIsDl!^?jNejOh`Fsqmpc0X_adbGO)F`_Wva`}}e%@lacCWhUTV<6{SH0t=(?892 z7==UsDqApYJMO`s;UEk|^GUKR)KR7C>&(W8X@B&`m;!gkHvagAwtIJxsf*DiciTph z{PVgEk0ozRxEnvF)1J38{?$~Mwq^-?r_ZhXHpA0?3ZVj|L42R`{)uPMIAoX%+eW)D zo-(nm-@rA2qQ*#XMv%Mj3Vfv<7Ix|;BzB~ho?lD2*bu74oN|d}Op!u~DF$V=z-15O zh2OI(H!^onxkgMTz~EYh*lcW>0uFQuW}+2`p7OpG$v1uMP{AEgVa2K~(;h@^I)$lJ zf9qR%%b97MC0?UQh+1%QRC4W@OVs5ww*gtNjK-pied3?EK&#ij_Q0y^s_QH@*k?zI4STDPlGbg%QPa zbt%6v#0wOLD{)wy&~}&cs_lXwDSt8S zkytf!rrek?AfR1!D{}@7=B#eop>Ow?Oqou??u2Ihh?MwK(Ap33H|7PI0X^yB`x#-B zo@4=Xd*hEZ9E1`GZjQqq#vzG7R$yVQdtbG$n(?GwMEcCb#?d7Sxq>|z*Fp??r#yS? z^^RR}N@Nb;PYM?lm{c+=f_e9M`U$iWIODTx7(}LC201=xa~YW2T-EHk$+*HTPPrWU zGnOma^y{4qsXp=2EUwWARiM;EUwM|+{HD-v<|MAOOx}E0Neatgjrj$b>=bkdNvVQL ziQ3o8wpYf>to~4_MdBtON=gD!9p9Vc&uU-t!%~1lJasu?Er@*1!Jn~OiXV2)Obn;j zF-MOLi$S_8OaIc^78)I0WvboK{2+E*JM})0pubSDt*T+~f;; z#;et_IID;~{$l3@F=os6k%KkwJxcgrvps)GJD7RvEMS+UhC}c!V2Hu$bzxop>?9E3 z+#MG?;nZi-wsMoqd^hqyTcSzZ?|Ml&Y45!T-7(1ay@(Ukj{IL12FsH*^oAsyLo{OF z%dPj#xWwD0;RB5tA9dDq1F{!XdYC^g^(Fay{HgCSZJWiE;3o*&dUupm7yZNjQ1C0= zCEvpjeDfPIXGhf$tR~6>9IZy-+HOv2W_Ilts5GZNj;lYWQHNVGHF`O0?1ggrT^Up1 zg?S8h#pP(@92n&0hn^>Ccx84Ny^YprWxK_?WA)UT6r|bL{bU8mt9ueDlBplV@ntqd z|5;+^KE(^oo0>|@&KmjubZ+wB^OfP+fIx}L|GNqsx%&$k5VYjP5%2PmpWzXQGC()% zRGFRfZ~|xzGS&UhjGP)waBxC9N7ojp3i<*5sMKtebt9Ev3i&=Zbg27yb^eAhwE2{! zBh;d=jYnyiIz6>h1L!OFpcwWN@=is9i$=;`_$Vol3L&l@?>rMnu+Y(Q^Km`D97GhDO3@`g&P{OU% zZhVxe+htO=@T7_V)3JW6plc3NI78d_-AQLCs~l-ALVvA9l+hkmN3@@d5tnx zhJC(Tk5266za{K;56MVu!a>8p=YVE*&ODglzrzB}mO{^2V0RNN7Rw-=-eUIY68SY% z^YY;`2LsQA4elt;d>@Gyw5@gQeL(Bwn@xI140c`g0R)H_4UK-nJYY&QCARP0GF5Km zOY#aO-!BX1UFPJgNP?)Ak-o|Fvqx79__>jJXu&WyWuKF&erI)ujHP0O6e#CsD-OO< zw~QxTsf{x&`R+dZO51U4QDt;zcFMdp*uF!j{ic@8RInV_1_q1n9ZMWcg++(!6 z(!qi5dtoXnsQ_X4FmcP3KG>I|Cq&#R%P4L}Xw3R5cUtS_$9p#dp@49=b}Ei^Yk@*I z!?^B3$mrD%GBdsIA=##> zzN)NsAkw;c#54eAC;Inz{Gp(O`(Wc3fuIpRUp-CYZ(rI&{b04CmQ4p|?ZRU&L8FvA z(rI#e?{eHN0y=L%bn$!LdJ)DNU2z}44Dj9j=-Pt~o({8aM#WK^7OVrjNZ&y|qT0j{D}0Ci16 zI$FgDVPQ3Qn?={<&&U`l$vk^2`|ETPf7n@m(B&ENZlzVFG(Cf%9*scPE2`!f=-jU2}v_rqYQ zA|@oQj1<()eftQ)M;<`YFOK@ZcSm93b>5Jtb`kOngXkw{yks{TI^d^I`d5&S2hs@lig* z*6$U&{|o7~7D2l_ z{IBI#Th*)0zGM#KZ*+P}K20o{w|PubmOf*~OA?x2_ep%TI$wbLopgS9@7>E(T@)Ub z8{T@stLX<=im4$0A#ZZSzq1>s#9+|Is%IYixb znwp%d=!-*cUw=}A{GM@|WK-tQ%J~hy5S{6(yco*xe98yYxhZ7CdSCu#?1#`b!Q|h1 zJMK70)I8l;7MaCPkcn7jgKeS0?M3F}`@UtgHF^}Bu%h$KAw0OuS4?-5Qoc4>a_c8o z9I3k?m1GxVxLy%mT(Do%$$KyB>uY^h()+F)w_^GmdK04GJ6=3T8C3snr2_H@2pj-s z!?g)@K{a#cd=0XoFop5P!wNhZ2?Mze$wMV1(6z4JO~uKye>ksueP|(*T$9;hTX$E$s4hSleZt(_$ zfusiPHD68mF#~85j%M1@t)p6nkmR$NZ-I_04YlDZp&If}A3H8MU(ggcxvBiv|0fl8 zr>A=cAsB_^HQDsc6ie;?ZI`fhZ^Uw9twOiyHe=lZT}*B!`5s z=guOwKg}}(wcby#sU8DRF<2Rl~Tth z!nhK`9$hSa1UyUU&G3f^EHc8tvz2<%sRk07nkiU|c#K~NuU=r0`<6cU+t8TL=M(>Z z5$zA|DKziOX#}d$_fah5cGN|s7q~J5lLq|2@Mh*^f^1y0Y{ZT`UA97NzKxZ{2d29W z!Jz52k4kpm6dU5dEGP97?~v(y@J;P1Al{eWVV#aIo|xUaKQhUn-I?i~Ul8nF*Qlf& zo@$Tjy(l>gjhR(heY;O#=C@F3NMa@wMmmuNbaMe^)mmh^k`R*UU_{7FEhwv*ebwi7 zN#R3SZ~gB#DG~|Y`TY&gX}ay#;J}J_9smj;Lk;Hd;E`wuAMog#^WkX{2%s)`&bmos zw|V}t1H*I&KES;=I5_r*KT)CkL3bqMs(6Wc#1izUBB1w;g&T>q{)Y8v5U4Y6wGev8 ztG~FEIlUQ&Rxgi9y^?+wWHM;O+c^>l0JDG|E)2TE2b`Q@f^kd%;!5WiwR@G6u|~}| z3W;JbCf&j8n_H(^4IBq<0rTfwE`ZZuT}Fjo;H%l= zx8e$seuiF z{&1<5=;@EQ(L;39)+xkwhht0tB|Y}mSoZL?$0vVR!LJMJM|INT?buL(0?XJ3c^qA$ z9blqp;RnBS7|m8DLSZ)p>rOK`pJ*&bBt1zqGq!8AM2aAwEDrA@zlkS4q%~~gO@#=WiaX#%U-g-VW`{Q+T zj_2(>Mi%Yd*PNo_bRfoqEL|Ht5NVE>~m zTo)(Nzah}yZ>Sspsp}}dM?a%Q{k{VWTDfM(|2e=fr#T9k47wbkQ49QT8{3k(+br!Ty%aywruNBg3BN}s@mD+36{$WY$hlKlMA)I;xp*lP_B2!6lZA81mu zq4`}W8!CQAa>FG|)kTTU(fJZmyZN4lW(w~22=3XBKYo2leC~KKBH^~F<$yPNg!haV zocF&3bMAAB^06ptFU->+!rgcSc=IFAGenN* zx(W+#NNkh)p`Bzpy;1Z2Z(+^1$)7l$Ditp86cXc>G`h^h4Dr4T5&II-J$h4?kv~}q z2I(TK>VH~z*mU%Nn5PBbplvZZ%Wb9K@@MmtBT-JdC09EF@bXy6nzMI<&O!+i!XwDi zkDY~S0Esv5=Dlt`RdXpu8zZMT7m<<$8%1}EMfIi?zbo}&z8}Rw68ck?t{X(Rd@y}C zF%~Dexuq?(F}|&Ik6@uSRa&DR)w@R$6O0~_m?m6(l)o+YE>-y4FP78#1y;+L82;e+ zrVl@kSzmNL@_oVZc)>b;QeR0ph3J}2zLalXl1{fD*p470IfeeX~}ky*a_ z>`U`)tRu}(yE!Gnfe6BhhRW`E>Ne}Ury8OneXoinva{+rEFG{njS#-1@DxmH2S4kxr(OJC2st@+N<^aSw)EZy)~-k z3?%W|&JiF0`rtw4m>>y#C5q_~KvC9{O8Rz1unbL<$z`~NXFl4ipr>G_9m*5Bv;FCM zjAd#so7@Fi!HiQzxIbP#;sC@q=!oCz5mY81faEEEfjPP|0okbExNA+VvE2d>*qq8Z zGZ-2%{qI9L68M&X0BEuI=ESA@V=~+|4oDWF)0k#`2(Sc-PVD4VWpdsuxvBF*2-ass zIT5+?(MI@X-Tci_85E1r`jqjHD7!NIn?qIwLq%#FJHLvqrE~LqNDQF7_edxhLPr8- zydZO8s4o+CK8ZlpHqS7=j|el?=xkB>G4y=gQC1PQI)%(>9L1^qR>p_`f zS7|(iTqw>iCUx_Qt8kI$d|lOaslK0T99G%iDo3o?2-;w@y-e5nm<@Q-W)x&ir7j2g}~IIQ7p1jv~xslEEGL- zUK{JfHL@#_zPj{8)y2MUd0}JH+kITZ4(h`3wMU|*tIbxa$LiP7brFv?Slw-Po5A00%;`yH}5X);*x%QFw!Q~OxN zpQAjQfhB3{aYzQan+d~07EhmVfE=SvAEG~dp{(atkG+=s=2{Ozmh@uhsfjLFGc_{; zZLAU~&(`}%u3%h|J=+#DpRdY^hT-U_^(&Q^UDFyV#5bSgDTHCsfdAYe<{fx3Tktjk z@xiUhpO$#1=a4sA@CJb<*Xzw!vpLh&G=>6%Z{F9ead9WshtHIxwGTaFDZb}u^c~N% zq41CQ_+U1N52oN^u8Ss)8o2X-RZ5IYp)g|KCytRgQZ4aTamoRfT@$mIj$?6&`l4zM z+0VTc-FAnWX@r=s1sIMKZ@f)V%S)(F5PmMrW2AK{@BIZ~mR^|}fuzzD&h2#XjSIus zyxt$h3S)AQsBHSe$!mSTNGK`p;_H6})ciYNR5-}rD#Qr9v$PiiulErm-!~Kq9HJ5Z zzva$=4OQ>Z8ejma0}LRPrsM{IDeniGu&NvN2zG~TO z5ij#;D&Z&AkGbXZF3{^LZsgZY6@-Nm{STc`8 zO!M!5T4c7i88j(LKjwVKx1IhV+}U~XY}hALL-3%>exZ5+y~hDtZQtK{j737RoZ>qL zL)-}iB`d{mv)qMk3T1zfj)W%s$9VsFs#iNOrqsq}l#lAy`|OiB)|dy5#!ohiF=!ZjGWLj$`c7&bP0E8rBQ4RLdnTAbQGSVV;@1IjIgaI#5ei6rw`WWSts`V&>R^1Lr)b3?d4GX9AJ92R^|P`5<16& zF{9ZlQhu>}v%)ja?B}CZJx7;|PV@~+H>K6@ueIJo4*5rd>!3YQRuTsh3v>k;4AFK3 z=pyI?{4w0eH{W)fdy{7G`*7jdvxr{y1rIYqY6S($yyM-;XU=8Dg_jJ+4!2VHBp&C|0l& zIAY;1C4}08Ve(}X;G!slJ%Ld6UybJ~nJMfAkhK43h)%K(}cZU?5w(V}rSBUVKXau!mlK?dna+{lUyH zE=8QDN^H7SJU>G!GQTL>^O73JmGu0VkUznTo;!4e7dX)me#%C$7j1Qzsc1M;`PK-` zM1cYDYS?f?&*|KO->+ z%%Jz?Yk3B=xa*B;cK$(aeZr7)=*fmZx$9{wU*61py%*F*ex{gn*rSggcT&RW9arZl}Sab#FOBJaSsz zrF1l=|8(WTa-l?B(y%`?E{e>3mI%yvEHDiNqC2*X3ft2rb%~_cGP)}R@aT+JcTWHg z6m_~$zpCxX4CSiTs(Sk?A?M4W0(OJW1s;DWphhK{9A-{cYlHZRAhfeBga?2t{TRbe z>_!R9x0G*iExhrGakP=9n;O(E))7xQzVHn`^Et6K1kZHXFb+(zd47^a4ICm`>E0WF z;Z+v17wPtC`^`5&{tEm=3n?Xsv{|9^D~gZ8yiu1&DZ`q^IeF@Yw*yH2G4>7AK~^r@XH zjmz=ssTZ3XC{EQQiFa^-R5lm~m|r>Y=HTG;5Fn_l6GY`;E*909d1KVAj78~Dv0bVQ zrZ!1jKJ_UEQ}A2t$6=-R^%&~-bAjnCC5MlI0B(?=-z22^RVtv>G1*MF390erln`pi zVSXYQ)jV^i&F`R{!v<3p22*TLXR~%{C|!W z{-N03e@Z*^7m4X+g*vsf#?|JfmuKmvQnFDqC16$ zPT?g{>dywb&+hX>(R)E2U?$Qdky3YGs4>dHx++rpSZKzPO}grX;fT87*X7TjuTyb^ zovOn;jhcu?xn}og4?;Nx2R!5> znd!Sfeof;>rq4IwGI zWWIh;`Y-iAMlyqHb#~F`XM^{2kn|z-#yr9|yJgKZo^{nOU6@I^NOz4orQe|ChZ0iE z@S04)%D~vfwaOJ^^?u7&<1!`;<_^VDZgbKn+Up;<99?bL2R)gE!}BEz2clUu)xQUI z1}56qRxP$Lb=JxQF!bQ8O^>=?JudDHs~P7zJzr?l^`|^?zp|`bQZpl-{qh_INB$$< zXPf*B=%El^J;1ugO`PdeFtKMUBdJY}K_-z-<(De^lsra^+u3cXEScLc3cLKSlYFnu zo7xWau#`afJyX;;>E7Z3Ez$MGL=@lFpqN@sWqr%+`woLhS({f%$!}Ga;&q`T_qoY} zL{}{00?g+Wq=83;?8or!fE*OuW8)kht!;X*T3o!WcKK%BJ-)z4?R@G|F-&?~S6vD= zau!Yb+HY0I4RW3L;ES1YU1L9n&+g6q+U?=6j-==Y8rRTEupact`pHlTT&&b~#MHN3 z8SoFP*EUo|`e~)Tal2m0I+#jvdv;n>RC*T1hCcEW`!=ya^hh;*V#r-}Iv$c1Lkf!(vbG4@)&`7Zj%2l+N0H(=IkD*P0RNzmZI1 z^o=7_UQXfj1~4MSBzb&SWye)~T?p31QMCp}?fs~d-e>K{X+0is4Q;ub?7WkyRif76 zEsasGAKqtiyw*6~Or;y}=vrgqa~jJbdgGm(S@!asrglJ*Y~XF!FVh-3Z(P zC*aW4m<#iKRyDD|L$|xAD?;CYU$->u-PILb4`<`zjcW zL~$d4so<_V+e<|qsf8aO)6xfasWP?i(U`#G*3bzsv36&o)8Pvj*W8{gjK~+)8<&q0 zO~Ofa@UPf3Ek5-NZRMen6g>nG8gYr(gO&p#lCb{tTf7{>>XYeK7N6e<&RPmG8u)f4I_Afu770 z#1xtKk(GaY&txB9{GtBQ7xcER$sv+71S6HR0`pym+}mjGTn+`u+? ztyvWKz#FVmvZ;RF_&*d=-3vtZO$6y}9MRD~jNA&C20G+l{-IdnNIER`U&})rjg&$-NiP7z!%W6? zrAN302H1OFO*R|OveKEKT5go>w@b_OLt(v3N;Wc-KmE}TB=7;Dj?B^sY+1N-$$Sv( z9|~j)3Li58CU#k7q^YiI(ULBV{Ns;jF+l(J#_WIl8>NUEKs=@l3~p1Da z*RV;SmsG;LBvWxmcM*CommpP73{P$O6ZX%q+3xcp@R&aoOlNRMpx)>NbUC`O16+|Q zqW{(slqBS?N4{<3s-@gq#vdcCz%N~Kxp@C-#=rM@K=!F*Z(wYFc_}9U_7)l>ITX+; zf$;fb`M&xSBKuBXqJEF+ov#0z|Mma8)qe)?|9q0&lydTm7%iFFP4&E~z_ z2Au5=#WOJ=Gi>AgvDia`KN=bgsZQ-#Ca&0Jbs>pOo%l^bzGeb#7(_qH1m9ax7x{g( zixiUcWX%r(+uA#%@P&K)F-h-bZC#II;7$^QDtusE6=`^WBSv zIXuO<_APzyc?!O%ah5N`!x1bR{Yl~LEfwBz#uZ2-({3Z<{6|W{wYBbr*j96InHk1Zc~*i-X{9VDy5GJ@|A_VqOm>TPj(7wR zT~gbg6O+2B@KsiLlJ~~ghWY>S{e#>-4c|>i=r3U`u$as*<-To(=!_c z%8Lb}1@FgR2+0*VEakIaxcyB__K+G;-5nx!+lr9OW#u7?_6`*9KhipzdNwl}c&f8* zuSL}zkYWQS=x!=iUAhf0(#B)y_U?n^Y+J7qlkxqy?e~34h$uhFk!D#!&kOuTJL3<} z<|WIpU8B~`th(XMbEdwz_UU-O?|))ZB-!s=m~a2m1A=LzS)^sq4;$ET?)c)1d6 zA9B^#)eui?omA;7ZL1u#x@3io$T8ZP5RRm^#w=UL)rIZc$O^%*b*nz~djiJtj^D1j zH$yHd@Np})JS%eZ%8;CwdwZu3znbG64LY`(<$p>WLb4}tbs|DQHP(T6D4Kko5L%cE zHxGM~ai5@k)@e{jW)0IwMs!-O{cOr>s5%wPl}=clt>=Sr9LNqt$#%vjlxl82&e>PZ z4Bjw@NCCnzO1-5mD2G-KF^!~URiK?Sv&n#sdDsuN5-*ndcKO%g*XD%of%~%_*MHE6 z8IXB}(Eh*oaC;;XDd3V#PJMDQfD0b5&=eH_s?4!DtrP*EuSCls(h zd`UU`%oI$L(FpxRv9{2)K-7W!p&)z(V{??`|4^uqC4r6P+dmXQ646symq*TkuyI5X zP#7Wnx%{!W{eeAM5wKwj$1jjhiA}&$8TE&PEPG#|iv!p*p&!DaWM7gQuzLeK#(yYy zbN}-h|1*yN-u*a=59^xa>QR`no1`z(fW`BDNo{;InE0ukB7QU{=)DHTYJd`mOs@f) zA{&V%&-tDP5FN)HonhCQk;nO&=WXZhq?I4>2<8NEdF6WL^&YTWdPh>2{r~;t<+M0OHiEn+&(f(hu;#04 znC)s50snY=sB=e%+@1NxKvC~~Vd zW#mEn!Eh`)OOaekX8;YY3PhH{3lR;rA7n$Ts7#XX0)kR2?dU3ZE6& zegtV|WvVCMa^jU1{`Gc#^8+W@JYa$ zx({mjL(!%PCCeLmnqM8=7Ojq4G8}Qt9~X`ldzkSwRIb^J^0v6@C!R|I0)cl>v`#KY zQz({w2g@6eAQC~iK-Tu`Q3*A%v%I@FIt7Afw;E-NNWDpVI#oj$TQxOAE};fe0MaW;p@)3=GsGb zD`EWgO0p)mN@#$r|KF}=98v4w55)=7EMTt^r~|M?D}A~cL@vI~)biM}VSX3pd(!63A(8bxnHKWD#o`kaSN zGoTzR(WY)4WB5Kl?EhfzJD{4{nuUWjrS~dDstAa5qyc{%2lN`=!%I z1hr}Ljqs={*0D4O({~{$u!)Bg3ie!XJz zQeZF=sH9rWCcaP;Sass_u^F#P3ml;+hlWHfrEvl=i*5SGi1NfXf#bVl_F764>XAf( z@ep-i4tm6jIw}@NSAGiGQD9f=a6O`t=LT{j2qCbsN*r{6)nKmUXl!Gq$o@%RD3-ne z^A#M-!U(u*Mir}GM^t#pDcG)UxzV@iKDgn`bBSMx!th~yhq^!3+S~H#gwcepb2Q(;nV@I^jtI*1-YTJ4hP#CvMBLSHwr}V00JTM9s!$J4ad4 zKDvmIjjk0%_%Q8E*Y4_AJ5NjshM>Q1XrYN0iZePCv^6ju^V`p;pl>kj-*2OyCzgz> zr_`?)(sujWxbaAR8{|;1j#eF15prHu`xYdP_oMV*0=2Upfv^HIK;g*mfwma0dDAp- zmj&~vr0I$5V#g6KtkB_!{F4wYBjaO;aS^-LlxA6D8M%U0%H%DQXz8){uP_}qkb+{4Siv(GK0M*e-hA)U4~bC9@U$+N!Tt4(WthH<8OG@KODaTqLeNmgS!V>;U{J>g*}P_y6- zRbeD*ZX#@-4b9l8JwDe$Jq|<#WVGFf2b#=1<7rO%c7YEKl)M(Q9o71j9j!%YuyfN^ z*sETBIgWryS9@0RpIPW`qQ8)~pd3c9_4bzg7uX#Ez!E>N0D>XV;``$0u67T;GP`*M z>5BIpDhk6e2q)BfKUth!!53C&ZobAMUw$+FW{ca!PpbkDfjPXf?L||SlW;FEGiJT* zs5C;J9;qPMGKhT)rg3Q`J75?~vfvn^RZaLr9)GzeY2j&cjhJaG*@I6W4djqp|AN&o z#%}1K>pw^mEmOMiIdEs} zy`lK89Qw;!K%T>YwfV;p{1<)xHrf8SdEz>M)C*uck9INkkj5+aXcyQH zy-|8^5WGZbcM38CmVtg1U-)zIlExoutFrnjC{7{c3uxZ$m|_4}z`yN8#&kdp%bo%sa+ zPyLxuw!0n*Osbs(Uj^@3d(9c*qgip9-tniP2R*ST%xoYSV7($tINNzWTLephMD=fk z0afA8C@XQ?z%1f!rCH$`mw+B~8kUGX#0N~nW57DPM{NQ5PdXQ5^xyQa{I%X)Atx->j$D z?T<$I+8{n3n+a23?QD_Ji%m|G&@uChicl&EBC5`^P^ate<4oNr+a{pRl@f6r?ab5T zW+2k!<8=gPlLM8$J~x9pz~S`*$xQ6=E8f=jVYI!rw=hou`@lCqFRz5RJ}rg&w9R3N z0Shc!CdJptMKDrf4AFqx#^R>}ffNIPD!=-1fmO`9a!MD!w@J6 zSDc(0mcRbs00wvm1bD#Flc0^i;P{Ik{}1rTygxU*`|cokgBh4avbhW4|1=t$`0pni zx3lwC4{uw4&~tL!@1`%JWOCjpGD@=jzT5$V0d}l^b0XFn_^)~G{-ZzA-;VF8>+|g0 z(>Nj#sPJ$VIq`5kA?U|%R6s%)FkLq*M=E>e+S=|6Kct;Z?Y*m>3!O@|)sG1H^qpBU zi@y~Teyyu%uaxA9+b)~QhqTkAB<%Rg4aJ}ssRkRQ)Qh_+N*iOOk8VRD z3vvM;x0LAnpu`ZANDz*G64TTA4ai{47uF{3DB5#U%-fk=tSQ{s6DY<^D*hU8mc&*( z?%X^HXbp@s!ffLLaa4fAQ2X4O3y^`L9I$S}pMg8O!5JE#2fN~a2 z9fsbsl;voJk}Ox-_{r58@tH2-2|lCgu$aW2tcN_UQch+td-U@_G|To~1ez!!fAxqX z#Z!b8D)BaEacZ$z5|msKFKfg+&&-j2neWnbSL;Z zVyCrnw+&v&7>m_^x8hM60lN;!HW#kOm#pb*^pEQ00RiD(df2DCBTV%1tY>!e^qJqM zBzK*-wyP@KZ8NJty*vs57OBaj9VP=$QHY7=U{1QzBWjmU#Vu7l{$kJ*SF9ih8?f$z zTRAW?z&shP10+T2>Vof4qQY<#e8AL7*B614A1P84!itrbqXh;eHZAsXMZ|mKl}4v`00hv>k3&TBt!Ec5ULwF3*?I;jM7|YcL{Ba5L-zsYsuIM)Q2fNw%=C zt%L;*+Gz)a>YDl>vo3$}m2YfcCgkk#ClJ4IW<}}74_DIl(IeYB2TNzK zr})!Tl8HlRDqNTD%_r$!3iR5Xt?eW7?*}3zu$E}K77O~ikw)$ubI8h*=fy?ZgpIPv zlO4I4aaqqg1R|x981RP0g&t`zbCPWPUtTMG57S2>@7>Hz&VNAr2A{VH1nc@Q%ok=N zSPAw-p!c)^r;NEm_}L=*%LOI25qiKeyzRy+WRCz1J~;wR%D{5{%Q@bR&R@^^Nx;A8 z^#_mq+dRR2bg|UMUhzwXR5x+RAvxL9td7^X_9(kDIduyIB?y zKKWNBYmo0nU5BHALCJ_JR&ZF2)RUMNKGO!M(8F}yyLZ1ElDR+G?SqYK|H|fn=6hBt zi%T!=RHe^OAx}oCnugOKq1p(e-KN0@Y03fLr-SusVtx>Ld(8J5r={80^7U89o{aH- z>Gs;_iXV^s4j2(L_ezh>K{IeCrhs$7B>0ze(4022MLQjTke38?;T1xL2V6C?`fVsy zOaQXsTXr2<{vgSH>dw5}L~#kVRT5)lcXlG%lEnCCsUml8d6!03v1;` z^G#De*_)lCJ&1WQXA^2N|eVrZ8g+NpM?#@D88VR3zkq<@KX)Kh-yRf-cO^e5EUAxws zaP4}LHpX5-5Eqcne+ohXx+c~e@Ynd~3jq>ev#1+G=dn^?`|8BpP)OP8F*5$S@$lB? zGWmq-{OMH+UE#}(we?A^pjmB6EZ5|ghGn@=L-@t;W!_yb{7(j-h|1uO+S(_rLb1`A?^5CvG@36mjM z|3vtFT?R`8U<6`p6Tvc=!_h~V-)&fqJ2oD_DFQ;QT|1Y&@!K*;KTv>|7`=c5-V$?| zaJ5CB6VeP)1j_K|6&>@8`|rlzS@^Z#xTrBRK)#;ut=m}o8Tif}4m4G4=k^h!A|rgh z1b{lh{%!1g-3_p-;yipF$pKgo_O8MJ-=G%ru~QHappa6Z$GQMJ1Rh8q9LW~qkT3G` z(Yvc{`$JooPC>!XaMB?7!W)cv*U8Ni;K$cMd(xlnUC-K|U5dQIADUaw$Wjh$3;p_A zEG8Us202h}3Z?%WYm5Jbov<_Zv}vy=i}xCD=x+Y`o~A)@~i5fu)Dym5t`U>q0YPlC*y5ybpAf*=5b zFyH`!y#Jjbg?bz8gE#&yc-2M<_9-1Wfuy=2IB9r)4XOd?M?(nqh1#E>x}3c>d`w&! z_h(G9{{$7?86AQr)&B!b{tOn-B~^+on>;|@{({P%!BU~~Gs$@H7lHq41QsZgRBZLK z{zn4;96S!ds3j)By0{)Do$=4_1FG6c(>%hg=65;v0f)VykFbX)@8lUN!@L+!)qyz?9Ua56 zQt9ZlTcwxFE6cB6F5V?HJNNkRV<50;^*JQ0<|Cr?b&rs_(<=ArgpG@}ho`%Zr85a| zyejtlv6!gL-;O1vB{|JWL~gpeTidvEnqC85a1A)W z!TArbu<~?pbphVNDWb2dL2~vV=Vg0O&&M9JA|iGUp7vf3uUfe}iwKY?DxQ6RM?p;b zPr%-@w6h@*xo7Ea>6M!Z^aRg3f zemlKmW9?vh(-ry~w362)Ib|fo%>UudKkqvu-Wd+cu8&;Z^&VSV+5AN5p0eI!&fnj3 zb|`Yo#@oTlMpx}7&{XfS-5(d!+%0|29x0*%^;FaIwDkP#b~Qaq&a+O?{rTHHOHWT5 zcbBsVpEXqlQ2F^1RcX%OL06UG{2gjlQBh9d`^T+D+7BJiI_V5V4QDYKPT>0!Sq*1# zF;3w7d&9re^C$XeSebH){{#W(w?DY$452>|zHQ@T=V{L=F7?~)w;NASRIMeq-==4gVf6*!K zek|-B0%vdvo$D?4UE$T}5#Bgj>^g}J5iXh3Sa-wJ9^wpldZgEARStS*3n8J<)3rpm znX!3Vn9isPi`}T6Ltj{$X&N({&@L5|` z)^+h8AzLY8zU+-(!mNIb=OLJWO|{v*>w*0gSuI<%S@Vv!7@0X+=OVIzpCt*T|;p;(W=ScIDZ(le9BK{RN5H zA7-{ty%5(klX)R-=f~Hkn3#LfS?;pP12OwsjQBc4brw0vUw@SE)LHZwldrKrHJi$| z64mR{MQEoFGFDaU%53OS4G(a-R*md>%;3w;$|z=H)E zu@Z552cSr8edF@AV_(Wt)DfVoifChXGLv5-S6*IU8t8=fRH(jWuQqpQd7Ut4*?E%~XUeNL-b|44G{$+%bAVWv7VZVD5RrU%=#+E5 z0mLM>#N8U}{<(PdoJIx6xm%y2C}gb_B(H=KfPjX++ObZtFC!HgD^kDj9Om{_xx~zn z-#)0R)kKcs)s)k{2B9IX5sS94h>NS|(y6rVcG4Q2yTRgcgg(XvZ-_9O9~kB@s$Z(P zA@TT}tK;(*eJ9PUsw{qV;~vuv5ps8s(f#n#JOP?y<5)iV2PxUSb#vV#^fjYHI>by5 z=FTl-6G{lj_4hCc3>xp$j|uADchJec|Ly&{O#z9sG<;oK)y|)`=hltSUXz-!&L5qd z!HE2I4vR+x!zXk6iQDlO^j7BeyiS6zt$MGN$K-f)TpBCm2Nl3n_G>)xsAOcUv_A#$ z?tOZ-e<52fU|KzfTq$!=h2rDd2PdKTc+5TE@OUH%oS{0JHgH+gzWRItj5z$BZXs!JXeYV)ZVA_;B78NKN*VYR$sO(Zeo= zz>8=@x2f@oSFUr$eUXSVTll=y4)m4@tCOG3i=ZVUS(2`Y=O2Xa5w-X|9(-feZN(IB zF67Ou{@u2_J5(|~)98ifjq8Z5i5u$=%iV66@d!&dfvDry($Xhe`0a@Z*U3M~SO^B> zKTna^Um{<{J1dW~PgIau`i%Ds3YW4FV7MyQh4}eQM3pKBleLwn7Vu3> z2bmb;nf%v2o4P4=Lu^69F9o6GVNzpX^VNjiK3S=zpP z6?y)$TEHdrHJ!Oc>++5MZ;^CU2F@c#nR=$uqTEyOivbGWH&xho^3F6b4>Mq3XFfF7 z%n!bLcX|9XqwrP2kDgBi2Cnqpgaj5y>93Ir-DA;}c-$(q^(IDuvS4{=44-&v&wMl) zKka^Nd3K|g470cxO@BUxw$j*14AV#Ur&JeL{KLeF`YA*L*;h966n>0L^JkeDfxie| zh*e{rUpmLj%q|v{qWGdd6hz0WksWVLf_*Sp^GcO;UXJ2-IwG}m&||ed_7Nq5 z7G@;122WKX46a!V4Q?^%@41)ngNCfCi`44%S?8**FfPr!nOxfwwEQ-G;@w%3uLnu* zx%A|rI0dSx?1n*I_oL7`=6TzlLZjS_i^e%I=j=bb9%Svk%08|u5_zn@F)L3!lJsv_MA@t?mPSiS4~@AdV36Ye|!3LD57HK z4!_z_-}B*yCGZfFFZj<==$Q<0CRd1ElM?+y*pN7r5&sZ2eiK4}7iRuJ4ApS{De4(I zSOcPn#I@@rBI-5{cJ_eOCnF9>n+~2H_iWsiU7a7hx}3>^GXEr}{VtRJ6v0G(7iT1X z6P-lw0QJv0Pe;xcq zgugi9|9TWGYmEr0c;JON8N~l-{`fg)&!!5Q>k@yPKF(B||BdP6r%I=H$I`s_OiV&fLPAOge26blP?D2ToE@J%pjT#=BJl9stHt9(mERZU$(Q}4dMfuWJHiIugDt)0Dtqlc%Lw-40!N$|6f z(6I1`$b=U!6O)o(y-sddPNgg%n!yv+#IEa1k(fw~6{g)>$4Q93q@V(fB+?m2n0_=hnxh;=Y~@Rn7+d^ROk zP4Fz{S<=P)Ov-#kYM2YEtIPb{cOBH%?EUeLt0rIfFL2wtJmS|2jWl{iq)I$&sKfR} zPlHQMLQI~F;l2ia3Iq2gGyFszs>hEX-&k>4a>ek4eVoy5<)HJwf>{$5F-C0%HCpCN zW4R~23vGp5d4hX=3=^is&$kK^SlspPq8~{Wp_1NYU2qoB7{)3ipNcSIjfaY=oRx1^ zlS=I4w`ulO8>-%ii19#V@XJ1ewiW^u40UZNIS+vI$p;)uP0dAD#LVXw`yY65KUYXFW7 z>=BDGw0BY4&o&rz9QA*~^-k}#H8Oxwea?!z?Y#^J_h3PA|HuqrlUiIH~<6NsOQ zHYi2TQDJzaT})yT#-yt&DWYV3JO6I7)PfW9ecrAk*_Y8@tO!ZyVwP=q+yW?b z@=M<|Gh%ho-o4k(^9fyW-+^^m#K>`C_hxN!C6FOy^gp zuASoS+WN|fEr^42eBn*f>Jh6d_s2Plps{CIlanHORy3m$x+?Eu1_|`hYWl@7#fx`D zBKJ7R+nuXEZOh+`pEi=<8zz$S9!M7Qk>L8y{n@71*CJxRk73PU8YLC+7CO{9gqdqY zRKdqc3IwX zVHF66Q}(!MnB=lQNKZzi#PwNf)L?R4hI5gq(A*!`lcm^zXP_wa`05&T zSGdGB`5lM6wG=H3yJU^9OZw9sq9Zw@n!;=1goL3Rh7@y*KPV$~aJ@i{xH>fJLRTA; zh`cCTN?5DI+eE(FWV6W1yo$vyCvT%%GF!5d`lTL|?=6zo58JO1Pr*^!kn`~VzD4-) zDaZ+kg#@;a!=7N2aiMTlz}4g!f)h8LD+`O3_gT~)KSp|2pjCVKfGwHH($GgGjcNo* zt)u!4_B$pxR1lUoLZcaL?Lr%LFo`-$Vg8&*iW>x!J{I>iGi1oq(@ zxD}l2)jqscWic()+xb|(MCv=>ORP5}zt9wQ%(ss7%LCG1ktjdpf!3xY2jN7&^GAGqeKC(y@t zy@(E9R9U=uxzq@47+nd*b10j|gh7BrU|~YJubmg`MkTfU6q#)lxD8*A8226A;Ge8T zgdwWPc_MW?WOmBfJ-|<%gVNn`D$qY7%%! zaH~@CN7grh#s(G*>X1V(Dlh_{$g8h&DIWew! z%_GImFdd|6Y7@w1q!>9`Y@MoZvFRCfb97Da_07Q9r0uTeiysE71P`&=NPEu{x{%?L zIg=_=ziaV{j5N$rH`O^~ua(?j>IL~u`F8>JE*ZENz-&y(Ov*mal~#G|cciq(`&xI^ zzv3>T+vF}LNgLz<;SxY9OH3LX(K>g&nZyy;{Pc1*U@awDN#fQ?y*H@2rsYAaUdBIx z&E@j;Wuw-=F#Uzl&7^B{9aYnY17pUJTD|W395lE-6DBl&Oy!UbF5WrMFVQ7sVHnuf z*0{jRRcrRhms)$4AOMVGLbM|GIDqVE1ZTr55{!((3Ib6Wz~lm-zf%f0T$bJddZK-s z9FwX&U{^g>X`hQGilPkPc;r$#Uo*@z^e)2IG^yHd*FaL0aLQ%4%^KyTg`*)C|GW#X zmVk(CygdcM_oG?;KR+?I^H%%{|6X%cak2)QhY=r(0EH~)o_ivwjGZj5rXOhdvNL_J ztriu9PKwu_zX29Z4=WKI)-!~Ti%qX_K7Xdc0g%FszXK85%!FYpf)cg016kLmv3Jly z?9>2|6V7*9we$N}yIDTpU^i2-dT6~wA=AZ)QrpBwa)c`iqDP}_-gh~`QF9P>rZI5k zq#P6AggKG4-V?a=n2%j6^z%Gw`hjhLGy-`J)ith?)n9Zcvk5oE@NIOu_3Ca$<)a<+ zH@md)Ue}VkD_?mTpS+5bnr}5~As%K+>~Q2@s!Ro6gqo@b@{I^#&GY8gyeCgWIx3HN z-!Ryha%;Qu&dWTc&ZekSo$ij*4o0xUE*#&2>JFKqh+|;Fiwb-v?54hZX{y&#ZfTI+ z{U#Klqu-)y&PdFj&B-?1e5K*_NXEU9i5K33c6X)yw&%C!MpI_FYwd$rH;pp2g#=!U z)|AZE4NW%o?MI&E!B~%lV1&cXe9)4e{l%6e)5*>Q7Wxk(e05H#&uRjc;y=)ACEYSB zw0biN|42DS`4L_j8oMBu#ir&EWtp`;sTREf?8V+;-)s9`B-ZlyqfHh$*GpSM zojw&A(2sHXJ6?{T2Ikh#uZG_yrV)+s%W&}D-p^B8SRc<#P#LaDOfYk`eEb^rSj^ia z^803^WtN3gy=^jMc%#%&ZjJN)j8uo0la*J2X?(mqRoI-wEq9(6WA&wrhTyaP<9g{x zd4YvH9``h`tT|9qlluN`5&1g`)oCu<_|J|tHEl$re6=Wr)8eP<1r`G2LPuV|df}0u za92Z2EdjnA39d55`Ac#fZ-H@KWk8+1m2~D8T6gpysI8=4Fx-C6Jiki`C~ZJ2}HPF7mj&A zJKWW>VIa4v>?yUZ>EQ>nPv@p7C0j$I>3AVznjhE`KlBp@7K(jmlnv*hK3LomJ^75* z;uw!@ed71#o?0npK1Zv{!^7zfd@gcdAv7j5KtAl>7`+zb_T%>WpWuHQ*V(_`{A2U4 zhyK4o(<_wNFs$<_DDZ3@E{Ev@amhOvx5aGY{iSB^QN^ByPsf%OsogU`-eh8+*l)jK z{UeD-5_m{|#xsEkD}$2GIEfGm--A~<@8UWiA$P04yKLO}D(3d1;vZMJvLDb^CC5=fOLKtiqALgfy+?RJSW*-aKqr*cNQom= zKs`0U6r*RRw@;@t+u zJuJeGLC^{VOvEuEjArb|rs}rQm}Dvbs&q=o$=2E89k@IY9seLoypwJIi(wqr5o5xA{Jdr9mM!mTEJCwxnO68+z~#*~Z&xk&YtW(ZiZHQvh9&)^@1VW~N|kzfIOK(=?3ATX2N3;o~f%Wa^`Bc zpWh7qHj$&I$L;%6iss{0rU0O_J)oKO9jn0ZVTRDe?US*=Sdtq5Ybc*~FoD8oe4%~4 zYKFvZbKsGD!!&I(zF7`{q z{(Nc5$K2{ejS4&|X>V7@ob2)ymW0+gO-+-GmhjRl65g+o>dS3oE`x)xa*MH1vR9ij zG2d@SA^EB-k%v_Ro(S2o_Jf16sjA|V)T$aSH$n4TW$hnb3}_hizk(E@C&{NEYe+Z; zaWffK5<|Uk3L-5|iuFU68lZoi^d@Cdxnz?Nrg5eE|#>yQ(HiG7Lq%+o| zeSlJlK!ycq=$!E&%s6LU1G!f@usC#euV*~3(rLZnR&*cB4JX+aH&*N#YhF5r8+ZOS zx|wyGq-8;b-rg`pzcLJ|S{s$m?U#)-ZyI^G{QgQ{MvaDqIB|sU_{w<>DX<|n;~l&9 zV1ObFAVLJ}JUY3HRQnlD07djVXI+s8BZuZ|>nekK?;LuL|5&%@N)dRJRrkOu$UGsK z`Nh)VVq-;ib^89upn+W( zt;(RBR`z3zgs~O02zO?eG?sByQrd@*Xj=3{q5xlAdkzdgdm&02sSuJBza$;DQAsL=Fqai%C6~pGnipOt;4W?? zCMHN`>n@SrPXVC01J&*{p=LSQuKWIjx1xX-W9`qwGQ01NqDXv7o)E9)(;rm@-%u0%8CVyYAv#7^){(+-TwTT-FNF2 zbg0pcJZR5?YQdr~aeKXfZ-Zh}-rQ&F$U%E*1ACVKb5h+ezkK0d(Krh0KGIn_xs1Oo z=o$>CH^h+=d!e1N_zxAC`%vG&p*@8h3=>t8;XfD=EXm_;-@;RU5|`}05V(A$I8yuE_OiQ=?h!&RALH)({{Q$O({uojLh0@!gV7cxc9mn+T)1r$e#RjbuqO_77Y^M_ zB*WvBeQ9~J%uT@YP|h&g{|gJQ0y#GkrPUbO)>it?STRey&*yzF+518t(T7&*9C>Be zOK+QQGl&IAxSqER;KuO}?Sc0TBx@A?o#*fz@+e^a$kp$Gkdb27ubXSd(i%0DoyM5q)e4?a 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 Date: Fri, 18 May 2018 21:40:10 +0200 Subject: [PATCH 233/567] Bump version to 1.0.0.0 --- common/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 5fbec40e8..2ef396d09 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -32,8 +32,8 @@ namespace System { internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics - internal const string VersionForAssembly = "0.99.0"; + internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0.0-rc1"; + internal const string Version = "1.0.0"; } } From 4b281de14cd1696777d46e3ca3656320252b6ce1 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 21:57:45 +0200 Subject: [PATCH 234/567] Really don't need this --- .../Assets/Editor/GitHub.Unity/EntryPoint.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index d7400c52d..4c605e38b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -24,8 +24,6 @@ static EntryPoint() LogHelper.LogAdapter = new FileLogAdapter(tempEnv.LogPath); - ServicePointManager.ServerCertificateValidationCallback = ServerCertificateValidationCallback; - ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; EditorApplication.update += Initialize; } @@ -87,12 +85,6 @@ internal static void Restart() Initialize(); } - private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, - X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - return true; - } - private static ApplicationManager appManager; public static IApplicationManager ApplicationManager { From 38553b7ac1fcb16fd90e95368febd4ca615b7810 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 18 May 2018 15:59:59 -0400 Subject: [PATCH 235/567] Removing credit for Octokit.net --- CREDITS.txt | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/CREDITS.txt b/CREDITS.txt index e7137d888..7a9773139 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -50,29 +50,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -=============================================================================== -Octokit.NET (https://github.com/editor-tools/octokit.net/tree/net3.5) -=============================================================================== - -Copyright (c) 2012 GitHub, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - =============================================================================== AsyncBridge (https://github.com/OmerMor/AsyncBridge) =============================================================================== From 64147309ac921c9badeac535c537216a290d14ec Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 18 May 2018 16:00:22 -0400 Subject: [PATCH 236/567] Updating url to swf --- CREDITS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CREDITS.txt b/CREDITS.txt index 7a9773139..0bf8f1207 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -25,7 +25,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =============================================================================== -Port of Axosoft's NSFW (https://github.com/StanleyGoldman/sfw) +Port of Axosoft's NSFW (https://github.com/github-for-unity/sfw) =============================================================================== The MIT License (MIT) From 4e45af213722b306a1ebb41221ecfcbb34700b24 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 18 May 2018 16:02:06 -0400 Subject: [PATCH 237/567] Removing credit to port of Mono's http library --- CREDITS.txt | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/CREDITS.txt b/CREDITS.txt index 0bf8f1207..b8fb5d7e1 100644 --- a/CREDITS.txt +++ b/CREDITS.txt @@ -1,29 +1,3 @@ -=============================================================================== -Port of Mono's System.Net.Http (https://github.com/shana/dotnet-httpclient35) -=============================================================================== - -Copyright (c) 2001, 2002, 2003 Ximian, Inc and the individuals listed -on the ChangeLog entries. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - =============================================================================== Port of Axosoft's NSFW (https://github.com/github-for-unity/sfw) =============================================================================== From 66b9d9ba193970fdb79c133d9d5afe54d2f47cf2 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 22:08:07 +0200 Subject: [PATCH 238/567] Fix update check error message --- src/GitHub.Logging/Extensions/ExceptionExtensions.cs | 12 ++++++++++++ .../Assets/Editor/GitHub.Unity/UpdateCheck.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Logging/Extensions/ExceptionExtensions.cs b/src/GitHub.Logging/Extensions/ExceptionExtensions.cs index 91879ea53..b22772799 100644 --- a/src/GitHub.Logging/Extensions/ExceptionExtensions.cs +++ b/src/GitHub.Logging/Extensions/ExceptionExtensions.cs @@ -20,5 +20,17 @@ public static string GetExceptionMessage(this Exception ex) message += Environment.NewLine + String.Join(Environment.NewLine, stack.Skip(1).SkipWhile(x => x.Contains("GitHub.Logging")).ToArray()); return message; } + + public static string GetExceptionMessageShort(this Exception ex) + { + var message = ex.ToString(); + var inner = ex.InnerException; + while (inner != null) + { + message += Environment.NewLine + inner.ToString(); + inner = inner.InnerException; + } + return message; + } } } \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs index 1e2fbc5ad..65ccbc884 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs @@ -63,7 +63,7 @@ public static void CheckForUpdates() var download = new DownloadTask(TaskManager.Instance.Token, EntryPoint.Environment.FileSystem, UpdateFeedUrl, EntryPoint.Environment.UserCachePath) .Catch(ex => { - LogHelper.Warning(@"Error downloading update check:{0} ""{1}"" Message:""{2}""", UpdateFeedUrl, ex.GetType().ToString(), ex.Message); + LogHelper.Warning(@"Error downloading update check:{0} ""{1}""", UpdateFeedUrl, ex.GetExceptionMessageShort()); return true; }); download.OnEnd += (thisTask, result, success, exception) => From 18e751a7671e1e14df64a68d24ce32bf059bb4b3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 18 May 2018 17:04:39 -0400 Subject: [PATCH 239/567] Shortening a few more exceptions --- src/GitHub.Api/Metrics/UsageTracker.cs | 2 +- src/GitHub.Api/Primitives/Package.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 0edab82e5..40c113b30 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -95,7 +95,7 @@ private async Task SendUsage() } catch (Exception ex) { - Logger.Warning(@"Error Sending Usage Exception Type:""{0}"" Message:""{1}""", ex.GetType().ToString(), ex.Message); + Logger.Warning(@"Error Sending Usage Exception Type:""{0}"" Message:""{1}""", ex.GetType().ToString(), ex.GetExceptionMessageShort()); } } diff --git a/src/GitHub.Api/Primitives/Package.cs b/src/GitHub.Api/Primitives/Package.cs index 1a7fa5b21..7b90aea47 100644 --- a/src/GitHub.Api/Primitives/Package.cs +++ b/src/GitHub.Api/Primitives/Package.cs @@ -38,7 +38,7 @@ public static Package Load(IEnvironment environment, UriString packageFeed) feed = new DownloadTask(TaskManager.Instance.Token, environment.FileSystem, packageFeed, environment.UserCachePath) .Catch(ex => { - LogHelper.Warning(@"Error downloading package feed:{0} ""{1}"" Message:""{2}""", packageFeed, ex.GetType().ToString(), ex.Message); + LogHelper.Warning(@"Error downloading package feed:{0} ""{1}"" Message:""{2}""", packageFeed, ex.GetType().ToString(), ex.GetExceptionMessageShort()); return true; }) .RunWithReturn(true); From cf0c3ac3638e308ce576e5641c2939999e8df316 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 23:01:25 +0200 Subject: [PATCH 240/567] Rename some things and add meta files --- CREDITS.txt => credits1.txt | 0 EULA.txt => eula1.txt | 0 .../GitHub/Editor/{README.pdf => QuickGuide.pdf} | Bin .../Plugins/GitHub/Editor/QuickGuide.pdf.meta | 8 ++++++++ .../Editor/{CREDITS.txt.meta => credits1.txt.meta} | 0 .../GitHub/Editor/{EULA.txt.meta => eula1.txt.meta} | 0 6 files changed, 8 insertions(+) rename CREDITS.txt => credits1.txt (100%) rename EULA.txt => eula1.txt (100%) rename unity/PackageProject/Assets/Plugins/GitHub/Editor/{README.pdf => QuickGuide.pdf} (100%) create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/QuickGuide.pdf.meta rename unity/PackageProject/Assets/Plugins/GitHub/Editor/{CREDITS.txt.meta => credits1.txt.meta} (100%) rename unity/PackageProject/Assets/Plugins/GitHub/Editor/{EULA.txt.meta => eula1.txt.meta} (100%) diff --git a/CREDITS.txt b/credits1.txt similarity index 100% rename from CREDITS.txt rename to credits1.txt diff --git a/EULA.txt b/eula1.txt similarity index 100% rename from EULA.txt rename to eula1.txt diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/README.pdf b/unity/PackageProject/Assets/Plugins/GitHub/Editor/QuickGuide.pdf similarity index 100% rename from unity/PackageProject/Assets/Plugins/GitHub/Editor/README.pdf rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/QuickGuide.pdf diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/QuickGuide.pdf.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/QuickGuide.pdf.meta new file mode 100644 index 000000000..80590df09 --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/QuickGuide.pdf.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d601ecb6855bb432bae2aa49d8fd82e8 +timeCreated: 1526676893 +licenseType: Pro +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/CREDITS.txt.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/credits1.txt.meta similarity index 100% rename from unity/PackageProject/Assets/Plugins/GitHub/Editor/CREDITS.txt.meta rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/credits1.txt.meta diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/EULA.txt.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/eula1.txt.meta similarity index 100% rename from unity/PackageProject/Assets/Plugins/GitHub/Editor/EULA.txt.meta rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/eula1.txt.meta From 6152a43a2416e48226e165ab96fdf0b9827f0571 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 18 May 2018 23:07:56 +0200 Subject: [PATCH 241/567] Case insensitive filesystems are highly annoying --- credits1.txt => credits.txt | 0 eula1.txt => eula.txt | 0 .../Plugins/GitHub/Editor/{credits1.txt.meta => credits.txt.meta} | 0 .../Plugins/GitHub/Editor/{eula1.txt.meta => eula.txt.meta} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename credits1.txt => credits.txt (100%) rename eula1.txt => eula.txt (100%) rename unity/PackageProject/Assets/Plugins/GitHub/Editor/{credits1.txt.meta => credits.txt.meta} (100%) rename unity/PackageProject/Assets/Plugins/GitHub/Editor/{eula1.txt.meta => eula.txt.meta} (100%) diff --git a/credits1.txt b/credits.txt similarity index 100% rename from credits1.txt rename to credits.txt diff --git a/eula1.txt b/eula.txt similarity index 100% rename from eula1.txt rename to eula.txt diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/credits1.txt.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/credits.txt.meta similarity index 100% rename from unity/PackageProject/Assets/Plugins/GitHub/Editor/credits1.txt.meta rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/credits.txt.meta diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/eula1.txt.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/eula.txt.meta similarity index 100% rename from unity/PackageProject/Assets/Plugins/GitHub/Editor/eula1.txt.meta rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/eula.txt.meta From ddf734f46aee1dda2b71ec85a450126edb929321 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Sat, 19 May 2018 00:19:14 +0200 Subject: [PATCH 242/567] Fix bug where locks view crashes plugin when logged out --- .../Assets/Editor/GitHub.Unity/UI/LocksView.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index c03c755d5..a85444dec 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -577,8 +577,11 @@ private void MaybeUpdateData() if (currentUserHasUpdate) { //TODO: ONE_USER_LOGIN This assumes only ever one user can login - var keychainConnection = Platform.Keychain.Connections.First(); - currentUsername = keychainConnection.Username; + var keychainConnection = Platform.Keychain.Connections.FirstOrDefault(); + if (keychainConnection != null) + currentUsername = keychainConnection.Username; + else + currentUsername = ""; currentUserHasUpdate = false; } From fa087f1acd6a699cb2ff64dfeec4e9978cf889cb Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Sat, 19 May 2018 00:19:36 +0200 Subject: [PATCH 243/567] Fix positioning of Initialize Repository button --- .../Assets/Editor/GitHub.Unity/UI/InitProjectView.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs index 2587b4a5d..1a6b8a693 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/InitProjectView.cs @@ -46,7 +46,13 @@ public override void OnGUI() GUILayout.FlexibleSpace(); GUILayout.Space(-140); - DoEmptyGUI(); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + GUILayout.Label(Styles.EmptyStateInit, GUILayout.MaxWidth(265), GUILayout.MaxHeight(136)); + GUILayout.FlexibleSpace(); + } + GUILayout.EndHorizontal(); GUILayout.Label(NoRepoTitle, Styles.BoldCenteredLabel); GUILayout.Space(4); From 3ddcdd34605cf24882416d714030e64974b4441c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Sat, 19 May 2018 00:38:34 +0200 Subject: [PATCH 244/567] Take out debug menu --- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 7de0fc25e..6fdd93f40 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -55,12 +55,6 @@ class Window : BaseWindow [SerializeField] private float appManagerProgressValue; [SerializeField] private string appManagerProgressMessage; - [MenuItem("GitHub/Select")] - public static void Select() - { - Selection.activeObject = SceneView.currentDrawingSceneView; - } - [MenuItem(Menu_Window_GitHub)] public static void Window_GitHub() { From 7d188de6deae83f4cd232d8b1ed1cbf65f8d49c6 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Sat, 19 May 2018 00:38:43 +0200 Subject: [PATCH 245/567] Fix the names of things for good measure --- .../Assets/Editor/GitHub.Unity/GitHub.Unity.csproj | 8 ++++---- .../CopyLibrariesToPackageProject.csproj | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index d520b2916..1fa8438aa 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -190,12 +190,12 @@ - - EULA.txt + + eula.txt PreserveNewest - - CREDITS.txt + + credits.txt PreserveNewest diff --git a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj index 05105c57e..179d6dbe3 100644 --- a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj +++ b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj @@ -71,12 +71,12 @@ - - EULA.txt + + eula.txt PreserveNewest - - CREDITS.txt + + credits.txt PreserveNewest From 8a58e3ec23a6ba6ea90e699879ccccf1377e1781 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 22 May 2018 11:42:55 +0200 Subject: [PATCH 246/567] Version should be RC until the release --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 2ef396d09..a524bcb46 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -34,6 +34,6 @@ internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0"; + internal const string Version = "1.0.0rc1"; } } From cd1d63fa6b0cf94e68d7b337f2d38f8941b94599 Mon Sep 17 00:00:00 2001 From: Thomas Aunvik Date: Wed, 23 May 2018 15:29:27 +0200 Subject: [PATCH 247/567] GitLocks NullReferenceException (#779) Initialize fields correctly on Changes view. Fixes #787 --- .../Assets/Editor/GitHub.Unity/UI/ChangesView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 021e8eb2f..b7421a381 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -31,8 +31,8 @@ class ChangesView : Subview [SerializeField] private Vector2 treeScroll; [SerializeField] private ChangesTree treeChanges = new ChangesTree { DisplayRootNode = false, IsCheckable = true, IsUsingGlobalSelection = true }; - [SerializeField] private HashSet gitLocks; - [SerializeField] private List gitStatusEntries; + [SerializeField] private HashSet gitLocks = new HashSet(); + [SerializeField] private List gitStatusEntries = new List(); [SerializeField] private string changedFilesText = NoChangedFilesLabel; From a07177e0bb537e1653e8f5457289e24b6bd4171b Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 23 May 2018 15:53:58 +0200 Subject: [PATCH 248/567] Bump version to 1.0.0rc2 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index a524bcb46..44c1a4644 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -34,6 +34,6 @@ internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0rc1"; + internal const string Version = "1.0.0rc2"; } } From 3170b57cdd023a4216853f3a45fdb34c55f199c0 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 23 May 2018 17:55:15 +0200 Subject: [PATCH 249/567] Allow lock IDs to be strings, some servers return that instead of ints (#788) Fixes #786 --- src/GitHub.Api/Git/GitLock.cs | 17 ++++++------ src/GitHub.Api/IO/NiceIO.cs | 2 ++ .../Editor/GitHub.Unity/UI/LocksView.cs | 4 +-- .../UnitTests/IO/LockOutputProcessorTests.cs | 26 ++++++++++++++----- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/GitHub.Api/Git/GitLock.cs b/src/GitHub.Api/Git/GitLock.cs index fd1f5d7e8..ae57d3b6f 100644 --- a/src/GitHub.Api/Git/GitLock.cs +++ b/src/GitHub.Api/Git/GitLock.cs @@ -9,7 +9,7 @@ public struct GitLock { public static GitLock Default = new GitLock(); - public int id; + public string id; public string path; public GitUser owner; [NotSerialized] public string lockedAtString; @@ -21,6 +21,7 @@ public DateTimeOffset locked_at if (!DateTimeOffset.TryParseExact(lockedAtString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle, out dt)) { + locked_at = DateTimeOffset.MinValue; return DateTimeOffset.MinValue; } return dt; @@ -30,15 +31,15 @@ public DateTimeOffset locked_at lockedAtString = value.ToUniversalTime().ToString(Constants.Iso8601FormatZ, CultureInfo.InvariantCulture); } } - [NotSerialized] public int ID => id; - [NotSerialized] public NPath Path => path.ToNPath(); + [NotSerialized] public string ID => id ?? String.Empty; + [NotSerialized] public NPath Path => path?.ToNPath() ?? NPath.Default; [NotSerialized] public GitUser Owner => owner; [NotSerialized] public DateTimeOffset LockedAt => locked_at; - public GitLock(int id, NPath path, GitUser owner, DateTimeOffset locked_at) + public GitLock(string id, NPath path, GitUser owner, DateTimeOffset locked_at) { this.id = id; - this.path = path; + this.path = path.IsInitialized ? path.ToString() : null; this.owner = owner; this.lockedAtString = locked_at.ToUniversalTime().ToString(Constants.Iso8601FormatZ, CultureInfo.InvariantCulture); } @@ -58,7 +59,7 @@ public bool Equals(GitLock other) public override int GetHashCode() { int hash = 17; - hash = hash * 23 + id.GetHashCode(); + hash = hash * 23 + ID.GetHashCode(); hash = hash * 23 + Path.GetHashCode(); hash = hash * 23 + owner.GetHashCode(); hash = hash * 23 + locked_at.GetHashCode(); @@ -67,7 +68,7 @@ public override int GetHashCode() public static bool operator ==(GitLock lhs, GitLock rhs) { - return lhs.id == rhs.id && lhs.Path == rhs.Path && lhs.owner == rhs.owner && lhs.locked_at == rhs.locked_at; + return lhs.ID == rhs.ID && lhs.Path == rhs.Path && lhs.owner == rhs.owner && lhs.locked_at == rhs.locked_at; } public static bool operator !=(GitLock lhs, GitLock rhs) @@ -76,7 +77,7 @@ public override int GetHashCode() } public override string ToString() { - return $"{{id:{id}, path:{Path}, owner:{{{owner}}}, locked_at:'{locked_at}'}}"; + return $"{{ID:{ID}, path:{Path}, owner:{{{owner}}}, locked_at:'{locked_at}'}}"; } } } diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index 0b01627b2..f481e5292 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -474,6 +474,8 @@ public override int GetHashCode() int hash = 17; // Suitable nullity checks etc, of course :) hash = hash * 23 + _isInitialized.GetHashCode(); + if (!_isInitialized) + return hash; hash = hash * 23 + _isRelative.GetHashCode(); foreach (var element in _elements) hash = hash * 23 + (IsUnix ? element : element.ToUpperInvariant()).GetHashCode(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index a85444dec..8717f7aff 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -215,7 +215,7 @@ public void Load(List locks, List gitStatusEntries) for (int i = 0; i < gitStatusEntries.Count; i++) statusEntries.Add(gitStatusEntries[i].Path.ToNPath().ToString(SlashMode.Forward), i); var selectedLockId = SelectedEntry != null && SelectedEntry.GitLock != GitLock.Default - ? (int?) SelectedEntry.GitLock.ID + ? SelectedEntry.GitLock.ID : null; var scrollValue = scroll.y; @@ -249,7 +249,7 @@ public void Load(List locks, List gitStatusEntries) for (var index = 0; index < gitLockEntries.Count; index++) { var gitLockEntry = gitLockEntries[index]; - if (selectedLockId.HasValue && selectedLockId.Value == gitLockEntry.GitLock.ID) + if (selectedLockId == gitLockEntry.GitLock.ID) { selectedEntry = gitLockEntry; selectionPresent = true; diff --git a/src/tests/UnitTests/IO/LockOutputProcessorTests.cs b/src/tests/UnitTests/IO/LockOutputProcessorTests.cs index e96d67137..0fb8c4a44 100644 --- a/src/tests/UnitTests/IO/LockOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/LockOutputProcessorTests.cs @@ -9,7 +9,7 @@ namespace UnitTests { [TestFixture] - class LockOutputProcessorTests : BaseOutputProcessorTests + class LocksTests : BaseOutputProcessorTests { private void AssertProcessOutput(IEnumerable lines, GitLock[] expected) { @@ -51,25 +51,39 @@ public void ShouldParseZeroLocksFormat2() } [Test] - public void ShouldParseTwoLocksFormat1() + public void ShouldParseTwoLocksFormat() { var now = DateTimeOffset.ParseExact(DateTimeOffset.UtcNow.ToString(Constants.Iso8601FormatZ), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); var output = new[] { @"[{""id"":""12"", ""path"":""folder/somefile.png"", ""owner"":{""name"":""GitHub User""}, ""locked_at"":""" + now.ToString(Constants.Iso8601FormatZ) + "\"}" + - @" ,{""id"":""21"", ""path"":""somezip.zip"", ""owner"":{""name"":""GitHub User""}, ""locked_at"":""" + now.ToString(Constants.Iso8601FormatZ) + "\"}]", + @" ,{""id"":""2f9cfde9c159d50e235cc1402c3e534b0bf2198afb20760697a5f9b07bf04fb3"", ""path"":""somezip.zip"", ""owner"":{""name"":""GitHub User""}, ""locked_at"":""" + now.ToString(Constants.Iso8601FormatZ) + "\"}]", string.Empty, "2 lock(s) matched query.", null }; var expected = new[] { - new GitLock(12, "folder/somefile.png".ToNPath(), new GitUser("GitHub User", ""), now), - new GitLock(21, "somezip.zip".ToNPath(), new GitUser("GitHub User", ""), now) + new GitLock("12", "folder/somefile.png".ToNPath(), new GitUser("GitHub User", ""), now), + new GitLock("2f9cfde9c159d50e235cc1402c3e534b0bf2198afb20760697a5f9b07bf04fb3", "somezip.zip".ToNPath(), new GitUser("GitHub User", ""), now) }; - AssertProcessOutput(output, expected); + AssertProcessOutput(output, expected); + } + + [Test] + public void GitLockComparisons() + { + GitLock lock1 = GitLock.Default; + GitLock lock2 = GitLock.Default; + Assert.IsTrue(lock1.Equals(lock2)); + Assert.IsTrue(lock1 == lock2); + // these are the defaults + lock1 = new GitLock(null, NPath.Default, GitUser.Default, DateTimeOffset.MinValue); + lock2 = new GitLock(); + Assert.IsTrue(lock1.Equals(lock2)); + Assert.IsTrue(lock1 == lock2); } } } From 6ae0b3f06b39aa113b1547d6917861011968b1a1 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 23 May 2018 19:09:20 +0200 Subject: [PATCH 250/567] Fix package generation script --- generate-package.sh | 103 ++++++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/generate-package.sh b/generate-package.sh index bd8785267..e9ef73a4a 100755 --- a/generate-package.sh +++ b/generate-package.sh @@ -1,42 +1,91 @@ #!/bin/sh -eu DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -if [ $# -lt 3 ]; then - echo "Usage: generate-package.sh [git|lfs|ghu] [windows|mac|linux] [version] [path to file] [host url (optional)] [release notes file (optional)] [message file (optional)]" - exit 1 -fi URL="http://ghfvs-installer.github.com" -if [ $# -ge 5 ]; then - URL=$5 -fi - -if [ "$1" == "git" ]; then - URL="$URL/unity/git/$2" -fi -if [ "$1" == "lfs" ]; then - URL="$URL/unity/git/$2" -fi -if [ "$1" == "ghu" ]; then - URL="$URL/unity/releases" -fi - +GIT=0 +LFS=0 +GHU=0 +FILE="" +VERSION="" RN="" MSG="" -if [ $# -ge 6 ]; then - RN="$6" +OS="" + +while [[ $# -gt 0 ]] +do +key="$1" + +case $key in + -h) + URL="$2" + shift + shift + ;; + -p) + FILE="--path $2" + shift + shift + ;; + -v) + VERSION="--version $2" + shift + shift + ;; + -r) + RN="--rn $2" + shift + shift + ;; + -m) + MSG="--msg $2" + shift + shift + ;; + -git) + GIT=1 + shift # past value + ;; + -lfs) + LFS=1 + shift # past value + ;; + -ghu) + GHU=1 + shift # past value + ;; + -windows) + OS="windows" + shift # past value + ;; + -mac) + OS="mac" + shift # past value + ;; + -linux) + OS="linux" + shift # past value + ;; +esac +done + +if [ x"$GIT" = "x0" -a x"$LFS" = "x0" -a x"$GHU" = "x0" ]; then + echo "Usage: generate-package.sh [-git|-lfs|-ghu] [-windows|-mac|-linux only if -git or -lfs] [-v version] [-p path to file] [-h host url (optional)] [-r release notes file (optional)] [-m message file (optional)]" + exit 1 fi -if [ $# -ge 7 ]; then - MSG="$7" +if [ x"$GHU" = x"1" ]; then + URL="--url $URL/unity/releases" +else + URL="--url $URL/unity/git/$OS" fi -EXEC="mono " -if [ -e "/c/" ]; then - EXEC="" +EXEC= +if [ ! -e "/c/" ]; then + EXEC=mono fi if [ ! -e "build/CommandLine/CommandLine.exe" ]; then - >&2 xbuild /target:CommandLine "$DIR/GitHub.Unity.sln" /verbosity:minimal + >&2 xbuild /target:CommandLine "$DIR/GitHub.Unity.sln" /verbosity:minimal fi -$EXEC build/CommandLine/CommandLine.exe --gen-package --version "$3" --path "$4" --url "$URL" --rn "$RN" --msg "$MSG" +$EXEC build/CommandLine/CommandLine.exe --gen-package $VERSION $FILE $URL $RN $MSG From 7f9e91c7094b3c6bbc5beb179b45cd8ffe14ee08 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 23 May 2018 19:40:16 +0200 Subject: [PATCH 251/567] isBusy should not be serialized (#790) Otherwise the UI might get locked out if Unity reloads in the middle of an operation --- .../Assets/Editor/GitHub.Unity/UI/ChangesView.cs | 2 +- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index b7421a381..0a94770eb 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -23,6 +23,7 @@ class ChangesView : Subview [SerializeField] private bool currentLocksHasUpdate; [NonSerialized] private GUIContent discardGuiContent; + [NonSerialized] private bool isBusy; [SerializeField] private string commitBody = ""; [SerializeField] private string commitMessage = ""; @@ -39,7 +40,6 @@ class ChangesView : Subview [SerializeField] private CacheUpdateEvent lastCurrentBranchChangedEvent; [SerializeField] private CacheUpdateEvent lastStatusEntriesChangedEvent; [SerializeField] private CacheUpdateEvent lastLocksChangedEvent; - [SerializeField] private bool isBusy; public override void OnEnable() { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 8717f7aff..5d06f7cc7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -372,6 +372,8 @@ public bool OnSelectionChange() [Serializable] class LocksView : Subview { + [NonSerialized] private bool isBusy; + [SerializeField] private bool currentStatusEntriesHasUpdate; [SerializeField] private bool currentLocksHasUpdate; [SerializeField] private bool currentUserHasUpdate; @@ -382,7 +384,6 @@ class LocksView : Subview [SerializeField] private List lockedFiles = new List(); [SerializeField] private List gitStatusEntries = new List(); [SerializeField] private string currentUsername; - [SerializeField] private bool isBusy; [SerializeField] private GUIContent unlockFileMenuContent = new GUIContent(Localization.UnlockFileMenuItem); [SerializeField] private GUIContent forceUnlockFileMenuContent = new GUIContent(Localization.ForceUnlockFileMenuItem); From b0a65255e2304f20ceba842aef0ba4296dcd1de4 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 23 May 2018 20:47:36 +0200 Subject: [PATCH 252/567] Remove debug logging (#791) * isBusy should not be serialized Otherwise the UI might get locked out if Unity reloads in the middle of an operation * Remove debug logging from release code --- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 6fdd93f40..34e2b5228 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -415,7 +415,6 @@ public override void UpdateProgress(IProgress progress) private void ApplicationManagerOnProgress(IProgress progress) { - Debug.LogFormat("ApplicationManagerOnProgress {0} {1}", progress.Percentage, progress.Message); appManagerProgress = progress; appManagerProgressHasUpdate = true; } From 6a41d52bcadb959f0b7917ade821029503297c16 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 23 May 2018 21:15:18 +0200 Subject: [PATCH 253/567] Ensure environment is never initialized off the main thread (#789) There's a potential for Environment to be initialized off the main thread in some cases (not sure exactly how, but #783 has an exception about it so it can definitely happen), so make sure everything depends on it being instantiated first. Also clean up some confusing properties. --- .../Application/ApplicationManagerBase.cs | 24 ++++---- .../Application/IApplicationManager.cs | 1 - .../Editor/GitHub.Unity/ApplicationManager.cs | 8 +-- .../Assets/Editor/GitHub.Unity/EntryPoint.cs | 14 ++--- .../GitHub.Unity/ScriptObjectSingleton.cs | 4 +- .../Editor/GitHub.Unity/UI/GitPathView.cs | 6 +- .../Editor/GitHub.Unity/UI/LocksView.cs | 9 ++- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 57 ++++++++++--------- .../Assets/Editor/GitHub.Unity/UpdateCheck.cs | 35 ++++++------ 9 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index b767416f8..58d3f1eb9 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -26,27 +26,27 @@ public event Action OnProgress remove { progressReporter.OnProgress -= value; } } - public ApplicationManagerBase(SynchronizationContext synchronizationContext) + public ApplicationManagerBase(SynchronizationContext synchronizationContext, IEnvironment environment) { SynchronizationContext = synchronizationContext; SynchronizationContext.SetSynchronizationContext(SynchronizationContext); ThreadingHelper.SetUIThread(); UIScheduler = TaskScheduler.FromCurrentSynchronizationContext(); ThreadingHelper.MainThreadScheduler = UIScheduler; + + Environment = environment; TaskManager = new TaskManager(UIScheduler); - progress.OnProgress += progressReporter.UpdateProgress; + Platform = new Platform(Environment); + ProcessManager = new ProcessManager(Environment, Platform.GitEnvironment, TaskManager.Token); + GitClient = new GitClient(Environment, ProcessManager, TaskManager.Token); } protected void Initialize() { - // accessing Environment triggers environment initialization if it hasn't happened yet - Platform = new Platform(Environment); - LogHelper.TracingEnabled = UserSettings.Get(Constants.TraceLoggingKey, false); ApplicationConfiguration.WebTimeout = UserSettings.Get(Constants.WebTimeoutKey, ApplicationConfiguration.WebTimeout); - ProcessManager = new ProcessManager(Environment, Platform.GitEnvironment, CancellationToken); Platform.Initialize(ProcessManager, TaskManager); - GitClient = new GitClient(Environment, ProcessManager, TaskManager.Token); + progress.OnProgress += progressReporter.UpdateProgress; } public void Run() @@ -63,7 +63,7 @@ public void Run() if (Environment.IsMac) { - var getEnvPath = new SimpleProcessTask(CancellationToken, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") + var getEnvPath = new SimpleProcessTask(TaskManager.Token, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") .Configure(ProcessManager, dontSetupGit: true) .Catch(e => true); // make sure this doesn't throw if the task fails var path = getEnvPath.RunWithReturn(true); @@ -96,7 +96,7 @@ public void Run() } - var installer = new GitInstaller(Environment, ProcessManager, CancellationToken); + var installer = new GitInstaller(Environment, ProcessManager, TaskManager.Token); installer.Progress.OnProgress += progressReporter.UpdateProgress; if (state.GitIsValid && state.GitLfsIsValid) { @@ -132,7 +132,7 @@ public void Run() progress.UpdateProgress(90, 100, "Initialization failed"); } - new ActionTask(CancellationToken, (s, gitIsValid) => + new ActionTask(TaskManager.Token, (s, gitIsValid) => { InitializationComplete(); if (gitIsValid) @@ -358,12 +358,10 @@ public void Dispose() Dispose(true); } - public abstract IEnvironment Environment { get; } - + public IEnvironment Environment { get; private set; } public IPlatform Platform { get; protected set; } public virtual IProcessEnvironment GitEnvironment { get; set; } public IProcessManager ProcessManager { get; protected set; } - public CancellationToken CancellationToken { get { return TaskManager.Token; } } public ITaskManager TaskManager { get; protected set; } public IGitClient GitClient { get; protected set; } public ISettings LocalSettings { get { return Environment.LocalSettings; } } diff --git a/src/GitHub.Api/Application/IApplicationManager.cs b/src/GitHub.Api/Application/IApplicationManager.cs index 736c89957..9b8b5f638 100644 --- a/src/GitHub.Api/Application/IApplicationManager.cs +++ b/src/GitHub.Api/Application/IApplicationManager.cs @@ -6,7 +6,6 @@ namespace GitHub.Unity { public interface IApplicationManager : IDisposable { - CancellationToken CancellationToken { get; } IEnvironment Environment { get; } IPlatform Platform { get; } IProcessEnvironment GitEnvironment { get; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs index c03329616..e3c6a2742 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationManager.cs @@ -14,8 +14,9 @@ class ApplicationManager : ApplicationManagerBase private FieldInfo quitActionField; - public ApplicationManager(IMainThreadSynchronizationContext synchronizationContext) - : base(synchronizationContext as SynchronizationContext) + public ApplicationManager(IMainThreadSynchronizationContext synchronizationContext, + IEnvironment environment) + : base(synchronizationContext as SynchronizationContext, environment) { FirstRun = ApplicationCache.Instance.FirstRun; InstanceId = ApplicationCache.Instance.InstanceId; @@ -31,7 +32,7 @@ protected override void InitializeUI() isBusy = false; LfsLocksModificationProcessor.Initialize(Environment, Platform); - ProjectWindowInterface.Initialize(Environment.Repository); + ProjectWindowInterface.Initialize(this); var window = Window.GetWindow(); if (window != null) window.InitializeWindow(this); @@ -100,6 +101,5 @@ protected override void Dispose(bool disposing) } public override IProcessEnvironment GitEnvironment { get { return Platform.GitEnvironment; } } - public override IEnvironment Environment { get { return EnvironmentCache.Instance.Environment; } } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 4c605e38b..3d99d48f9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -33,7 +33,7 @@ private static void Initialize() EditorApplication.update -= Initialize; // this will initialize ApplicationManager and Environment if they haven't yet - var logPath = Environment.LogPath; + var logPath = ApplicationManager.Environment.LogPath; if (ApplicationCache.Instance.FirstRun) { @@ -58,7 +58,7 @@ private static void Initialize() LogHelper.Error(ex, "Error rotating log files"); } - Debug.LogFormat("Initialized GitHub for Unity version {0}{1}Log file: {2}", ApplicationInfo.Version, Environment.NewLine, logPath); + Debug.LogFormat("Initialized GitHub for Unity version {0}{1}Log file: {2}", ApplicationInfo.Version, ApplicationManager.Environment.NewLine, logPath); } LogHelper.LogAdapter = new MultipleLogAdapter(new FileLogAdapter(logPath) @@ -66,12 +66,12 @@ private static void Initialize() , new UnityLogAdapter() #endif ); - LogHelper.Info("Initializing GitHubForUnity:'v{0}' Unity:'v{1}'", ApplicationInfo.Version, Environment.UnityVersion); + LogHelper.Info("Initializing GitHubForUnity:'v{0}' Unity:'v{1}'", ApplicationInfo.Version, ApplicationManager.Environment.UnityVersion); ApplicationManager.Run(); if (ApplicationCache.Instance.FirstRun) - UpdateCheckWindow.CheckForUpdates(); + UpdateCheckWindow.CheckForUpdates(ApplicationManager); } internal static void Restart() @@ -92,14 +92,10 @@ public static IApplicationManager ApplicationManager { if (appManager == null) { - appManager = new ApplicationManager(new MainThreadSynchronizationContext()); + appManager = new ApplicationManager(new MainThreadSynchronizationContext(), EnvironmentCache.Instance.Environment); } return appManager; } } - - public static IEnvironment Environment { get { return ApplicationManager.Environment; } } - - public static IUsageTracker UsageTracker { get { return ApplicationManager.UsageTracker; } } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs index 04f1c0026..a5628f284 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ScriptObjectSingleton.cs @@ -21,9 +21,9 @@ public LocationAttribute(string relativePath, Location location) if (location == Location.PreferencesFolder) filepath = InternalEditorUtility.unityPreferencesFolder + "/" + relativePath; else if (location == Location.UserFolder) - filepath = EntryPoint.Environment.UserCachePath.Combine(relativePath).ToString(SlashMode.Forward); + filepath = EntryPoint.ApplicationManager.Environment.UserCachePath.Combine(relativePath).ToString(SlashMode.Forward); else if (location == Location.LibraryFolder) - filepath = EntryPoint.Environment.UnityProjectPath.Combine("Library", "gfu", relativePath); + filepath = EntryPoint.ApplicationManager.Environment.UnityProjectPath.Combine("Library", "gfu", relativePath); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs index 7ef580d6a..72f774b13 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitPathView.cs @@ -173,9 +173,9 @@ public override void OnGUI() { GUI.FocusControl(null); isBusy = true; - new FuncTask(Manager.CancellationToken, () => + new FuncTask(TaskManager.Token, () => { - var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, Manager.CancellationToken); + var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); return gitInstaller.FindSystemGit(new GitInstaller.GitInstallationState()); }) { Message = "Locating git..." } @@ -226,7 +226,7 @@ private void ValidateAndSetGitInstallPath() { new FuncTask(TaskManager.Token, () => { - var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, Manager.CancellationToken); + var gitInstaller = new GitInstaller(Environment, Manager.ProcessManager, TaskManager.Token); var state = new GitInstaller.GitInstallationState(); state = gitInstaller.SetDefaultPaths(state); // on non-windows we only bundle git-lfs diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 5d06f7cc7..6ba86b54f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -57,6 +57,8 @@ class LocksControl [SerializeField] public GitLockEntryDictionary assets = new GitLockEntryDictionary(); [SerializeField] public GitStatusDictionary gitStatusDictionary = new GitStatusDictionary(); [SerializeField] private GitLockEntry selectedEntry; + [SerializeField] public NPath projectPath; + public bool IsEmpty { get { return gitLockEntries.Count == 0; } } public GitLockEntry SelectedEntry @@ -69,8 +71,8 @@ public GitLockEntry SelectedEntry { selectedEntry = value; - var activeObject = selectedEntry != null && selectedEntry.GitLock != GitLock.Default - ? AssetDatabase.LoadMainAssetAtPath(selectedEntry.GitLock.Path.MakeAbsolute().RelativeTo(EntryPoint.Environment.UnityProjectPath)) + var activeObject = selectedEntry != null && selectedEntry.GitLock != GitLock.Default && projectPath.IsInitialized + ? AssetDatabase.LoadMainAssetAtPath(selectedEntry.GitLock.Path.MakeAbsolute().RelativeTo(projectPath)) : null; lastActivatedObject = activeObject; @@ -235,7 +237,7 @@ public void Load(List locks, List gitStatusEntries) var gitLockEntry = new GitLockEntry(gitLock, gitFileStatus); LoadIcon(gitLockEntry, true); - var path = gitLock.Path.MakeAbsolute().RelativeTo(EntryPoint.Environment.UnityProjectPath); + var path = gitLock.Path.MakeAbsolute().RelativeTo(projectPath); var assetGuid = AssetDatabase.AssetPathToGUID(path); if (!string.IsNullOrEmpty(assetGuid)) { @@ -611,6 +613,7 @@ private void BuildLocksControl() locksControl = new LocksControl(); } + locksControl.projectPath = Environment.UnityProjectPath; locksControl.Load(lockedFiles, gitStatusEntries); } public override void OnSelectionChange() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 436390e3b..7b8e5bafe 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -16,32 +16,33 @@ class ProjectWindowInterface : AssetPostprocessor private static readonly List guids = new List(); private static readonly List guidsLocks = new List(); - private static IRepository repository; + private static IApplicationManager manager; private static bool isBusy = false; private static ILogging logger; private static ILogging Logger { get { return logger = logger ?? LogHelper.GetLogger(); } } private static CacheUpdateEvent lastRepositoryStatusChangedEvent; private static CacheUpdateEvent lastLocksChangedEvent; + private static IRepository Repository { get { return manager.Environment.Repository; } } - public static void Initialize(IRepository repo) + public static void Initialize(IApplicationManager theManager) { EditorApplication.projectWindowItemOnGUI -= OnProjectWindowItemGUI; EditorApplication.projectWindowItemOnGUI += OnProjectWindowItemGUI; - repository = repo; + manager = theManager; - if (repository != null) + if (Repository != null) { - repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; - repository.LocksChanged += RepositoryOnLocksChanged; - ValidateCachedData(repository); + Repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; + Repository.LocksChanged += RepositoryOnLocksChanged; + ValidateCachedData(); } } - private static void ValidateCachedData(IRepository repository) + private static void ValidateCachedData() { - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastRepositoryStatusChangedEvent); - repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); + Repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastRepositoryStatusChangedEvent); + Repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitLocks, lastLocksChangedEvent); } private static void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) @@ -50,7 +51,7 @@ private static void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdat { lastRepositoryStatusChangedEvent = cacheUpdateEvent; entries.Clear(); - entries.AddRange(repository.CurrentChanges); + entries.AddRange(Repository.CurrentChanges); OnStatusUpdate(); } } @@ -60,7 +61,7 @@ private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) if (!lastLocksChangedEvent.Equals(cacheUpdateEvent)) { lastLocksChangedEvent = cacheUpdateEvent; - locks = repository.CurrentLocks; + locks = Repository.CurrentLocks; OnLocksUpdate(); } } @@ -70,7 +71,7 @@ private static bool ContextMenu_CanLock() { if (isBusy) return false; - if (repository == null || !repository.CurrentRemote.HasValue) + if (Repository == null || !Repository.CurrentRemote.HasValue) return false; var selected = Selection.activeObject; @@ -80,7 +81,7 @@ private static bool ContextMenu_CanLock() return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); var alreadyLocked = locks.Any(x => repositoryPath == x.Path); GitFileStatus status = GitFileStatus.None; @@ -98,15 +99,15 @@ private static void ContextMenu_Lock() var selected = Selection.activeObject; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - repository + Repository .RequestLock(repositoryPath) .FinallyInUI((success, ex) => { if (success) { - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); + manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); } else { @@ -130,7 +131,7 @@ private static bool ContextMenu_CanUnlock() { if (isBusy) return false; - if (repository == null || !repository.CurrentRemote.HasValue) + if (Repository == null || !Repository.CurrentRemote.HasValue) return false; var selected = Selection.activeObject; @@ -140,7 +141,7 @@ private static bool ContextMenu_CanUnlock() return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); var isLocked = locks.Any(x => repositoryPath == x.Path); return isLocked; @@ -153,15 +154,15 @@ private static void ContextMenu_Unlock() var selected = Selection.activeObject; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - repository + Repository .ReleaseLock(repositoryPath, false) .FinallyInUI((success, ex) => { if (success) { - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } else { @@ -185,7 +186,7 @@ private static bool ContextMenu_CanUnlockForce() { if (isBusy) return false; - if (repository == null || !repository.CurrentRemote.HasValue) + if (Repository == null || !Repository.CurrentRemote.HasValue) return false; var selected = Selection.activeObject; @@ -195,7 +196,7 @@ private static bool ContextMenu_CanUnlockForce() return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); var isLocked = locks.Any(x => repositoryPath == x.Path); return isLocked; @@ -208,15 +209,15 @@ private static void ContextMenu_UnlockForce() var selected = Selection.activeObject; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = EntryPoint.Environment.GetRepositoryPath(assetPath); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - repository + Repository .ReleaseLock(repositoryPath, false) .FinallyInUI((success, ex) => { if (success) { - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } else { @@ -247,7 +248,7 @@ private static void OnLocksUpdate() foreach (var lck in locks) { NPath repositoryPath = lck.Path; - NPath assetPath = EntryPoint.Environment.GetAssetPath(repositoryPath); + NPath assetPath = manager.Environment.GetAssetPath(repositoryPath); var g = AssetDatabase.AssetPathToGUID(assetPath); guidsLocks.Add(g); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs index 65ccbc884..d3afe1306 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UpdateCheck.cs @@ -48,7 +48,7 @@ public GUIPackage(Package package) } } - public class UpdateCheckWindow : EditorWindow + class UpdateCheckWindow : BaseWindow { public const string UpdateFeedUrl = #if DEBUG @@ -58,9 +58,9 @@ public class UpdateCheckWindow : EditorWindow #endif ; - public static void CheckForUpdates() + public static void CheckForUpdates(IApplicationManager manager) { - var download = new DownloadTask(TaskManager.Instance.Token, EntryPoint.Environment.FileSystem, UpdateFeedUrl, EntryPoint.Environment.UserCachePath) + var download = new DownloadTask(manager.TaskManager.Token, manager.Environment.FileSystem, UpdateFeedUrl, manager.Environment.UserCachePath) .Catch(ex => { LogHelper.Warning(@"Error downloading update check:{0} ""{1}""", UpdateFeedUrl, ex.GetExceptionMessageShort()); @@ -76,7 +76,7 @@ public static void CheckForUpdates() TheVersion current = TheVersion.Parse(ApplicationInfo.Version); TheVersion newVersion = package.Version; - var versionToSkip = EntryPoint.ApplicationManager.UserSettings.Get(Constants.SkipVersionKey); + var versionToSkip = manager.UserSettings.Get(Constants.SkipVersionKey); if (versionToSkip == newVersion) { LogHelper.Info("Skipping GitHub for Unity update v" + newVersion); @@ -89,9 +89,9 @@ public static void CheckForUpdates() return; } - TaskManager.Instance.RunInUI(() => + manager.TaskManager.RunInUI(() => { - NotifyOfNewUpdate(current, package); + NotifyOfNewUpdate(manager, current, package); }); } catch(Exception ex) @@ -103,10 +103,10 @@ public static void CheckForUpdates() download.Start(); } - private static void NotifyOfNewUpdate(TheVersion currentVersion, Package package) + private static void NotifyOfNewUpdate(IApplicationManager manager, TheVersion currentVersion, Package package) { var window = GetWindowWithRect(new Rect(100, 100, 580, 400), true, windowTitle); - window.Initialize(currentVersion, package); + window.Initialize(manager, currentVersion, package); window.Show(); } @@ -131,21 +131,23 @@ private static void NotifyOfNewUpdate(TheVersion currentVersion, Package package [SerializeField] private bool hasReleaseNotesUrl; [SerializeField] private bool hasMessage; - private void Initialize(TheVersion current, Package newPackage) + private void Initialize(IApplicationManager manager, TheVersion current, Package newPackage) { package = new GUIPackage(newPackage); currentVersion = current.ToString(); - if (guiLogo != null) - { - guiLogo = null; - Repaint(); - } + var requiresRedraw = guiLogo != null; + guiLogo = null; + this.InitializeWindow(manager, requiresRedraw); } - private void OnGUI() + public override void OnDataUpdate() { + base.OnDataUpdate(); LoadContents(); + } + public override void OnUI() + { GUILayout.BeginVertical(); GUILayout.Space(10); @@ -196,7 +198,7 @@ private void OnGUI() { var settings = EntryPoint.ApplicationManager.UserSettings; settings.Set(Constants.SkipVersionKey, package.Package.Version); - this.Close(); + Close(); } EditorGUILayout.EndHorizontal(); @@ -228,5 +230,6 @@ private void LoadContents() guiPackageReleaseNotes = new GUIContent(package.Package.ReleaseNotes); } + public override bool IsBusy { get { return false; } } } } From 4e7bcaca043dc6e17d0f541746fbf247c06642ce Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 23 May 2018 15:52:57 -0400 Subject: [PATCH 254/567] Functionality to capture git repository size in usage metrics --- .../Application/ApplicationManagerBase.cs | 13 +++++++ src/GitHub.Api/Git/GitClient.cs | 7 ++++ src/GitHub.Api/Git/GitCountObjects.cs | 23 +++++++++++ .../Git/Tasks/GitCountObjectsTask.cs | 22 +++++++++++ src/GitHub.Api/GitHub.Api.csproj | 3 ++ src/GitHub.Api/Metrics/IUsageTracker.cs | 1 + src/GitHub.Api/Metrics/UsageModel.cs | 1 + src/GitHub.Api/Metrics/UsageTracker.cs | 7 ++++ .../GitCountObjectsProcessor.cs | 24 ++++++++++++ .../TestUtils/Helpers/AssertExtensions.cs | 6 +++ .../UnitTests/IO/CountObjectProcessorTests.cs | 39 +++++++++++++++++++ src/tests/UnitTests/UnitTests.csproj | 1 + 12 files changed, 147 insertions(+) create mode 100644 src/GitHub.Api/Git/GitCountObjects.cs create mode 100644 src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs create mode 100644 src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs create mode 100644 src/tests/UnitTests/IO/CountObjectProcessorTests.cs diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 58d3f1eb9..91b5b7dbf 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -178,6 +178,7 @@ public void SetupGit(GitInstaller.GitInstallationState state) if (Environment.RepositoryPath.IsInitialized) { ConfigureMergeSettings(); + CaptureRepoSize(); GitClient.LfsInstall() .Catch(e => @@ -286,6 +287,18 @@ private void ConfigureMergeSettings() }).RunWithReturn(true); } + private void CaptureRepoSize() + { + GitClient.CountObjects() + .Then((success, gitObjects) => { + if (success) + { + UsageTracker.UpdateRepoSize(gitObjects.kilobytes); + } + }) + .Start(); + } + public void RestartRepository() { if (!Environment.RepositoryPath.IsInitialized) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 9570920a9..9162bb6c5 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -38,6 +38,7 @@ public interface IGitClient ITask> Log(BaseOutputListProcessor processor = null); ITask Version(IOutputProcessor processor = null); ITask LfsVersion(IOutputProcessor processor = null); + ITask CountObjects(IOutputProcessor processor = null); ITask SetConfigNameAndEmail(string username, string email); } @@ -98,6 +99,12 @@ public ITask LfsVersion(IOutputProcessor processor = nul .Configure(processManager); } + public ITask CountObjects(IOutputProcessor processor = null) + { + return new GitCountObjectsTask(cancellationToken, processor) + .Configure(processManager); + } + public ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null) { return new GitConfigGetTask(key, configSource, cancellationToken, processor) diff --git a/src/GitHub.Api/Git/GitCountObjects.cs b/src/GitHub.Api/Git/GitCountObjects.cs new file mode 100644 index 000000000..61ba720b7 --- /dev/null +++ b/src/GitHub.Api/Git/GitCountObjects.cs @@ -0,0 +1,23 @@ +using System; + +namespace GitHub.Unity +{ + [Serializable] + public struct GitCountObjects + { + public static GitCountObjects Default = new GitCountObjects(); + + public int objects; + public int kilobytes; + + public GitCountObjects(int objects, int kilobytes) + { + this.objects = objects; + this.kilobytes = kilobytes; + } + + public int Objects => objects; + + public int Kilobytes => kilobytes; + } +} \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs b/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs new file mode 100644 index 000000000..0ee157fe7 --- /dev/null +++ b/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs @@ -0,0 +1,22 @@ +using System.Threading; + +namespace GitHub.Unity +{ + class GitCountObjectsTask : ProcessTask + { + private const string TaskName = "git count-objects"; + + public GitCountObjectsTask(CancellationToken token, IOutputProcessor processor = null) + : base(token, processor ?? new GitCountObjectsProcessor()) + { + Name = TaskName; + } + + public override string ProcessArguments + { + get { return "count-objects"; } + } + public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } + public override string Message { get; set; } = "Counting git objects..."; + } +} \ No newline at end of file diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 30462a466..2576b8049 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -95,9 +95,11 @@ + + @@ -116,6 +118,7 @@ + diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index b3a8b9942..c004b1153 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -19,5 +19,6 @@ public interface IUsageTracker void IncrementUnityProjectViewContextLfsUnlock(); void IncrementPublishViewButtonPublish(); void IncrementApplicationMenuMenuItemCommandLine(); + void UpdateRepoSize(int kilobytes); } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 674b034b7..9906799ae 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -42,6 +42,7 @@ public class Measures public int UnityProjectViewContextLfsUnlock { get; set; } public int PublishViewButtonPublish { get; set; } public int ApplicationMenuMenuItemCommandLine { get; set; } + public int GitRepoSize { get; set; } } class UsageModel diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 40c113b30..4257197b7 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -236,6 +236,13 @@ public void IncrementApplicationMenuMenuItemCommandLine() usageLoader.Save(usage); } + public void UpdateRepoSize(int kilobytes) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).GitRepoSize = kilobytes; + usageLoader.Save(usage); + } + public bool Enabled { get diff --git a/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs b/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs new file mode 100644 index 000000000..4d544e877 --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs @@ -0,0 +1,24 @@ +namespace GitHub.Unity +{ + public class GitCountObjectsProcessor : BaseOutputProcessor + { + public override void LineReceived(string line) + { + if (line == null) + { + return; + } + + //2488 objects, 4237 kilobytes + + var proc = new LineParser(line); + + var objects = int.Parse(proc.ReadUntilWhitespace()); + proc.ReadUntil(','); + proc.SkipWhitespace(); + var kilobytes = int.Parse(proc.ReadUntilWhitespace()); + + RaiseOnEntry(new GitCountObjects(objects, kilobytes)); + } + } +} \ No newline at end of file diff --git a/src/tests/TestUtils/Helpers/AssertExtensions.cs b/src/tests/TestUtils/Helpers/AssertExtensions.cs index e02f70c79..6a5477b2b 100644 --- a/src/tests/TestUtils/Helpers/AssertExtensions.cs +++ b/src/tests/TestUtils/Helpers/AssertExtensions.cs @@ -114,5 +114,11 @@ public static void AssertNotEqual(this GitStatus gitStatus, GitStatus other) Action action = () => gitStatus.AssertEqual(other); action.ShouldThrow(); } + + public static void AssertEqual(this GitCountObjects gitStatus, GitCountObjects other) + { + gitStatus.Objects.Should().Be(other.Objects, "Objects should be equal"); + gitStatus.Kilobytes.Should().Be(other.Kilobytes, "KilobytesS should be equal"); + } } } diff --git a/src/tests/UnitTests/IO/CountObjectProcessorTests.cs b/src/tests/UnitTests/IO/CountObjectProcessorTests.cs new file mode 100644 index 000000000..98234090a --- /dev/null +++ b/src/tests/UnitTests/IO/CountObjectProcessorTests.cs @@ -0,0 +1,39 @@ +using TestUtils; +using System.Collections.Generic; +using NUnit.Framework; +using GitHub.Unity; + +namespace UnitTests +{ + [TestFixture] + class GitCountObjectProcessorTests : BaseOutputProcessorTests + { + + [Test] + public void ShouldParseGitCountOutput() + { + var output = new[] + { + "2488 objects, 4237 kilobytes", + null + }; + + AssertProcessOutput(output, new GitCountObjects(2488, 4237)); + } + + private void AssertProcessOutput(IEnumerable lines, GitCountObjects expected) + { + GitCountObjects? result = null; + var outputProcessor = new GitCountObjectsProcessor(); + outputProcessor.OnEntry += status => { result = status; }; + + foreach (var line in lines) + { + outputProcessor.LineReceived(line); + } + + Assert.IsTrue(result.HasValue); + result.Value.AssertEqual(expected); + } + } +} \ No newline at end of file diff --git a/src/tests/UnitTests/UnitTests.csproj b/src/tests/UnitTests/UnitTests.csproj index c8a26e1fe..7e1ab27ab 100644 --- a/src/tests/UnitTests/UnitTests.csproj +++ b/src/tests/UnitTests/UnitTests.csproj @@ -78,6 +78,7 @@ + From b548733a3c5c400a53965348b07f4d0160ca92f8 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 23 May 2018 18:12:16 -0400 Subject: [PATCH 255/567] First attempt to get windows disk usage --- .../Application/ApplicationManagerBase.cs | 32 +++++++++++++++++++ src/GitHub.Api/GitHub.Api.csproj | 1 + src/GitHub.Api/Metrics/IUsageTracker.cs | 1 + src/GitHub.Api/Metrics/UsageModel.cs | 1 + src/GitHub.Api/Metrics/UsageTracker.cs | 7 ++++ .../Platform/WindowsDiskUsageTask.cs | 22 +++++++++++++ 6 files changed, 64 insertions(+) create mode 100644 src/GitHub.Api/Platform/WindowsDiskUsageTask.cs diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 91b5b7dbf..88ec76662 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -297,6 +297,38 @@ private void CaptureRepoSize() } }) .Start(); + + var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); + if (gitLfsDataPath.Exists()) + { + if (Environment.IsWindows) + { + new WindowsDiskUsageTask(gitLfsDataPath, TaskManager.Token).Configure(ProcessManager).Then( + (b, list) => { + if (b) + { + try + { + var output = list[list.Count - 2]; + var proc = new LineParser(output); + proc.SkipWhitespace(); + proc.ReadUntilWhitespace(); + proc.ReadUntilWhitespace(); + proc.SkipWhitespace(); + + var sizeInBytes = int.Parse(proc.ReadUntilWhitespace().Replace(",", string.Empty)); + var sizeInKilobytes = sizeInBytes / 1024; + + UsageTracker.UpdateLfsDiskUsage(sizeInKilobytes); + } + catch (Exception e) + { + Logger.Error(e, "Error Calculating LFS Disk Usage"); + } + } + }).Start(); + } + } } public void RestartRepository() diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 2576b8049..e010a0333 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -135,6 +135,7 @@ + diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index c004b1153..a56bcce15 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -20,5 +20,6 @@ public interface IUsageTracker void IncrementPublishViewButtonPublish(); void IncrementApplicationMenuMenuItemCommandLine(); void UpdateRepoSize(int kilobytes); + void UpdateLfsDiskUsage(int kilobytes); } } diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 9906799ae..33b1da760 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -43,6 +43,7 @@ public class Measures public int PublishViewButtonPublish { get; set; } public int ApplicationMenuMenuItemCommandLine { get; set; } public int GitRepoSize { get; set; } + public int LfsDiskUsage { get; set; } } class UsageModel diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 4257197b7..665f76df4 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -243,6 +243,13 @@ public void UpdateRepoSize(int kilobytes) usageLoader.Save(usage); } + public void UpdateLfsDiskUsage(int kilobytes) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).LfsDiskUsage = kilobytes; + usageLoader.Save(usage); + } + public bool Enabled { get diff --git a/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs b/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs new file mode 100644 index 000000000..b4567b909 --- /dev/null +++ b/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading; + +namespace GitHub.Unity +{ + class WindowsDiskUsageTask : ProcessTask> + { + private readonly string arguments; + + public WindowsDiskUsageTask(NPath directory, CancellationToken token) + : base(token, new SimpleListOutputProcessor()) + { + Name = "cmd"; + arguments = "/c dir /a/s \"" + directory + "\""; + } + + public override string ProcessName { get { return Name; } } + public override string ProcessArguments { get { return arguments; } } + public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Getting directory size..."; + } +} \ No newline at end of file From 186048b88ad32bacdc754c83a2444d6e8bd1a9e3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 23 May 2018 19:15:06 -0400 Subject: [PATCH 256/567] Complete the calculation of LFS disk usage --- .../Application/ApplicationManagerBase.cs | 54 ++++++---------- src/GitHub.Api/GitHub.Api.csproj | 3 + .../LinuxDiskUsageOutputProcessor.cs | 29 +++++++++ .../WindowsDiskUsageOutputProcessor.cs | 43 +++++++++++++ src/GitHub.Api/Platform/LinuxDiskUsageTask.cs | 21 ++++++ .../Platform/WindowsDiskUsageTask.cs | 6 +- .../IO/LinuxDiskUsageOutputProcessorTests.cs | 59 +++++++++++++++++ .../WindowsDiskUsageOutputProcessorTests.cs | 64 +++++++++++++++++++ src/tests/UnitTests/UnitTests.csproj | 2 + 9 files changed, 244 insertions(+), 37 deletions(-) create mode 100644 src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs create mode 100644 src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs create mode 100644 src/GitHub.Api/Platform/LinuxDiskUsageTask.cs create mode 100644 src/tests/UnitTests/IO/LinuxDiskUsageOutputProcessorTests.cs create mode 100644 src/tests/UnitTests/IO/WindowsDiskUsageOutputProcessorTests.cs diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 88ec76662..a09bbe54d 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -289,46 +289,32 @@ private void ConfigureMergeSettings() private void CaptureRepoSize() { + IProcessTask diskUsageTask = null; + var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); + if (gitLfsDataPath.Exists()) + { + diskUsageTask = Environment.IsWindows + ? (IProcessTask)new WindowsDiskUsageTask(gitLfsDataPath, TaskManager.Token) + : new LinuxDiskUsageTask(gitLfsDataPath, TaskManager.Token); + + diskUsageTask.Configure(ProcessManager); + } + GitClient.CountObjects() - .Then((success, gitObjects) => { - if (success) + .Finally((b1, gitObjects) => { + if (b1) { UsageTracker.UpdateRepoSize(gitObjects.kilobytes); } + + diskUsageTask?.Then((b2, kilobytes) => { + if (b2) + { + UsageTracker.UpdateLfsDiskUsage(kilobytes); + } + }).Start(); }) .Start(); - - var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); - if (gitLfsDataPath.Exists()) - { - if (Environment.IsWindows) - { - new WindowsDiskUsageTask(gitLfsDataPath, TaskManager.Token).Configure(ProcessManager).Then( - (b, list) => { - if (b) - { - try - { - var output = list[list.Count - 2]; - var proc = new LineParser(output); - proc.SkipWhitespace(); - proc.ReadUntilWhitespace(); - proc.ReadUntilWhitespace(); - proc.SkipWhitespace(); - - var sizeInBytes = int.Parse(proc.ReadUntilWhitespace().Replace(",", string.Empty)); - var sizeInKilobytes = sizeInBytes / 1024; - - UsageTracker.UpdateLfsDiskUsage(sizeInKilobytes); - } - catch (Exception e) - { - Logger.Error(e, "Error Calculating LFS Disk Usage"); - } - } - }).Start(); - } - } } public void RestartRepository() diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index e010a0333..0a9a5a11a 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -120,8 +120,10 @@ + + @@ -135,6 +137,7 @@ + diff --git a/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs new file mode 100644 index 000000000..782d41bd9 --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs @@ -0,0 +1,29 @@ +using System; + +namespace GitHub.Unity +{ + public class LinuxDiskUsageOutputProcessor : BaseOutputProcessor + { + private string buffer; + + public override void LineReceived(string line) + { + if (line == null) + { + if (buffer == null) + { + throw new InvalidOperationException("Not enough input"); + } + + var proc = new LineParser(buffer); + var kilobytes = int.Parse(proc.ReadUntilWhitespace()); + + RaiseOnEntry(kilobytes); + } + else + { + buffer = line; + } + } + } +} \ No newline at end of file diff --git a/src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs new file mode 100644 index 000000000..761b903f5 --- /dev/null +++ b/src/GitHub.Api/OutputProcessors/WindowsDiskUsageOutputProcessor.cs @@ -0,0 +1,43 @@ +using System; + +namespace GitHub.Unity +{ + public class WindowsDiskUsageOutputProcessor : BaseOutputProcessor + { + private int index = -1; + private int lineCount = 0; + private string[] buffer = new string[2]; + + public override void LineReceived(string line) + { + lineCount++; + index = (index + 1) % 2; + + if (line == null) + { + if (lineCount <= 2) + { + throw new InvalidOperationException("Not enough input"); + } + + var output = buffer[index]; + + Logger.Trace("Processing: {0}", output); + + var proc = new LineParser(output); + proc.SkipWhitespace(); + proc.ReadUntilWhitespace(); + proc.ReadUntilWhitespace(); + proc.SkipWhitespace(); + + var bytes = int.Parse(proc.ReadUntilWhitespace().Replace(",", string.Empty)); + var kilobytes = bytes / 1024; + RaiseOnEntry(kilobytes); + } + else + { + buffer[index] = line; + } + } + } +} \ No newline at end of file diff --git a/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs b/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs new file mode 100644 index 000000000..5f8d07bad --- /dev/null +++ b/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs @@ -0,0 +1,21 @@ +using System.Threading; + +namespace GitHub.Unity +{ + class LinuxDiskUsageTask : ProcessTask + { + private readonly string arguments; + + public LinuxDiskUsageTask(NPath directory, CancellationToken token) + : base(token, new LinuxDiskUsageOutputProcessor()) + { + Name = "du"; + arguments = string.Format("-h \"{0}\"", directory); + } + + public override string ProcessName { get { return Name; } } + public override string ProcessArguments { get { return arguments; } } + public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } + public override string Message { get; set; } = "Getting directory size..."; + } +} \ No newline at end of file diff --git a/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs b/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs index b4567b909..75fd4ff32 100644 --- a/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs +++ b/src/GitHub.Api/Platform/WindowsDiskUsageTask.cs @@ -3,15 +3,15 @@ namespace GitHub.Unity { - class WindowsDiskUsageTask : ProcessTask> + class WindowsDiskUsageTask : ProcessTask { private readonly string arguments; public WindowsDiskUsageTask(NPath directory, CancellationToken token) - : base(token, new SimpleListOutputProcessor()) + : base(token, new WindowsDiskUsageOutputProcessor()) { Name = "cmd"; - arguments = "/c dir /a/s \"" + directory + "\""; + arguments = string.Format("/c dir /a/s \"{0}\"", directory); } public override string ProcessName { get { return Name; } } diff --git a/src/tests/UnitTests/IO/LinuxDiskUsageOutputProcessorTests.cs b/src/tests/UnitTests/IO/LinuxDiskUsageOutputProcessorTests.cs new file mode 100644 index 000000000..7a559fe35 --- /dev/null +++ b/src/tests/UnitTests/IO/LinuxDiskUsageOutputProcessorTests.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using GitHub.Unity; +using NUnit.Framework; + +namespace UnitTests +{ + [TestFixture] + class LinuxDiskUsageOutputProcessorTests : BaseOutputProcessorTests + { + [Test] + public void ParseOutput() + { + var output = new[] + { + "4 .git/lfs/objects/f7/0a", + "4 .git/lfs/objects/f7", + "44 .git/lfs/objects/f8/0c", + "1 .git/lfs/objects/f8/1e", + "45 .git/lfs/objects/f8", + "176 .git/lfs/objects/fa/7c", + "176 .git/lfs/objects/fa", + "7716 .git/lfs/objects/fb/8d", + "188 .git/lfs/objects/fb/ac", + "7904 .git/lfs/objects/fb", + "168 .git/lfs/objects/fd/39", + "168 .git/lfs/objects/fd", + "284 .git/lfs/objects/fe/50", + "284 .git/lfs/objects/fe", + "2556 .git/lfs/objects/ff/48", + "1 .git/lfs/objects/ff/c6", + "2557 .git/lfs/objects/ff", + "4 .git/lfs/objects/incomplete", + "0 .git/lfs/objects/logs", + "628417 .git/lfs/objects", + "0 .git/lfs/tmp/objects", + "64 .git/lfs/tmp", + "628481 .git/lfs", + null + }; + + AssertProcessOutput(output, 628481); + } + + private void AssertProcessOutput(IEnumerable lines, int expected) + { + int? result = null; + var outputProcessor = new LinuxDiskUsageOutputProcessor(); + outputProcessor.OnEntry += status => { result = status; }; + + foreach (var line in lines) + { + outputProcessor.LineReceived(line); + } + + Assert.IsTrue(result.HasValue); + Assert.AreEqual(expected, result.Value); + } + } +} \ No newline at end of file diff --git a/src/tests/UnitTests/IO/WindowsDiskUsageOutputProcessorTests.cs b/src/tests/UnitTests/IO/WindowsDiskUsageOutputProcessorTests.cs new file mode 100644 index 000000000..5e1a327e8 --- /dev/null +++ b/src/tests/UnitTests/IO/WindowsDiskUsageOutputProcessorTests.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using GitHub.Unity; +using NUnit.Framework; + +namespace UnitTests +{ + [TestFixture] + class WindowsDiskUsageOutputProcessorTests : BaseOutputProcessorTests + { + [Test] + public void WindowsDiskUsageOutput() + { + var output = new[] + { + "01/10/2018 09:11 AM 0 862394886", + "05 / 10 / 2018 01:17 PM 0 880468387", + "05 / 10 / 2018 02:12 PM 0 882539090", + "05 / 08 / 2018 10:56 AM 0 890139785", + "05 / 10 / 2018 02:12 PM 0 907039131", + "01 / 10 / 2018 09:11 AM 0 909343522", + "01 / 10 / 2018 09:11 AM 0 914279800", + "03 / 08 / 2018 12:50 PM 0 935882723", + "02 / 20 / 2018 10:40 AM 0 953135163", + "01 / 10 / 2018 09:11 AM 0 956375995", + "01 / 10 / 2018 09:11 AM 0 957028503", + "01 / 10 / 2018 09:11 AM 0 957454540", + "05 / 08 / 2018 10:56 AM 0 961987973", + "01 / 10 / 2018 09:11 AM 0 972291688", + "01 / 10 / 2018 09:11 AM 0 986253768", + "03 / 07 / 2018 06:33 PM 0 991012201", + "04 / 02 / 2018 02:39 PM objects", + "148 File(s) 0 bytes", + "", + @" Directory of C:\Users\Spade\Projects\GitHub\Unity\.git\lfs\tmp\objects", + "", + "04 / 02 / 2018 02:39 PM.", + "04 / 02 / 2018 02:39 PM..", + " 0 File(s) 0 bytes", + "", + " Total Files Listed:", + " 409 File(s) 643,058,481 bytes", + " 1325 Dir(s) 151,921,385,472 bytes free", + null + }; + + AssertProcessOutput(output, 627986); + } + + private void AssertProcessOutput(IEnumerable lines, int expected) + { + int? result = null; + var outputProcessor = new WindowsDiskUsageOutputProcessor(); + outputProcessor.OnEntry += status => { result = status; }; + + foreach (var line in lines) + { + outputProcessor.LineReceived(line); + } + + Assert.IsTrue(result.HasValue); + Assert.AreEqual(expected, result.Value); + } + } +} \ No newline at end of file diff --git a/src/tests/UnitTests/UnitTests.csproj b/src/tests/UnitTests/UnitTests.csproj index 7e1ab27ab..bff3c8d57 100644 --- a/src/tests/UnitTests/UnitTests.csproj +++ b/src/tests/UnitTests/UnitTests.csproj @@ -77,6 +77,7 @@ + @@ -92,6 +93,7 @@ + From 9a926c6ed97309c1cec950ed4f7a97a24c704e2e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 08:49:17 -0400 Subject: [PATCH 257/567] Missing true in force unlock operations --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 7b8e5bafe..c4ff8ad25 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -212,7 +212,7 @@ private static void ContextMenu_UnlockForce() NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); Repository - .ReleaseLock(repositoryPath, false) + .ReleaseLock(repositoryPath, true) .FinallyInUI((success, ex) => { if (success) From b5b03d683bbda919ab54836f5ec1374f9d0a61fa Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 09:35:54 -0400 Subject: [PATCH 258/567] Letting the operations to count git objects and get lfs disk usage race --- .../Application/ApplicationManagerBase.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index a09bbe54d..a1fc35041 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -289,32 +289,33 @@ private void ConfigureMergeSettings() private void CaptureRepoSize() { - IProcessTask diskUsageTask = null; + GitClient.CountObjects() + .Finally((success, gitObjects) => + { + if (success) + { + UsageTracker.UpdateRepoSize(gitObjects.kilobytes); + } + }) + .Start(); + var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); if (gitLfsDataPath.Exists()) { - diskUsageTask = Environment.IsWindows + var diskUsageTask = Environment.IsWindows ? (IProcessTask)new WindowsDiskUsageTask(gitLfsDataPath, TaskManager.Token) : new LinuxDiskUsageTask(gitLfsDataPath, TaskManager.Token); - diskUsageTask.Configure(ProcessManager); + diskUsageTask + .Configure(ProcessManager) + .Then((success, kilobytes) => + { + if (success) + { + UsageTracker.UpdateLfsDiskUsage(kilobytes); + } + }).Start(); } - - GitClient.CountObjects() - .Finally((b1, gitObjects) => { - if (b1) - { - UsageTracker.UpdateRepoSize(gitObjects.kilobytes); - } - - diskUsageTask?.Then((b2, kilobytes) => { - if (b2) - { - UsageTracker.UpdateLfsDiskUsage(kilobytes); - } - }).Start(); - }) - .Start(); } public void RestartRepository() From 701e5666b8b3a17fef75e48b52401ef1b2a2b7cd Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 09:33:03 -0400 Subject: [PATCH 259/567] Prepping to lock/unlock multiple files --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 136 +++++++++--------- 1 file changed, 64 insertions(+), 72 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index c4ff8ad25..2fef852b9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -74,7 +74,11 @@ private static bool ContextMenu_CanLock() if (Repository == null || !Repository.CurrentRemote.HasValue) return false; - var selected = Selection.activeObject; + return Selection.objects.Any(IsObjectUnlocked); + } + + private static bool IsObjectUnlocked(Object selected) + { if (selected == null) return false; if (locks == null) @@ -89,6 +93,7 @@ private static bool ContextMenu_CanLock() { status = entries.FirstOrDefault(x => repositoryPath == x.Path.ToNPath()).Status; } + return !alreadyLocked && status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; } @@ -98,32 +103,32 @@ private static void ContextMenu_Lock() isBusy = true; var selected = Selection.activeObject; + var unlockedObjects = Selection.objects.Where(IsObjectUnlocked).ToArray(); + LockObject(selected); + + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); + } + + private static void LockObject(Object selected) + { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - Repository - .RequestLock(repositoryPath) - .FinallyInUI((success, ex) => + Repository.RequestLock(repositoryPath).FinallyInUI((success, ex) => { + if (success) { - if (success) - { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); - } - else - { - var error = ex.Message; - if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; - EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, - error, - Localization.Ok); - } - - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }) - .Start(); + manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); + } + else + { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; + EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, error, Localization.Ok); + } + }).Start(); } [MenuItem(AssetsMenuReleaseLock, true, 1000)] @@ -134,7 +139,11 @@ private static bool ContextMenu_CanUnlock() if (Repository == null || !Repository.CurrentRemote.HasValue) return false; - var selected = Selection.activeObject; + return Selection.objects.Any(IsObjectLocked); + } + + private static bool IsObjectLocked(Object selected) + { if (selected == null) return false; if (locks == null || locks.Count == 0) @@ -153,32 +162,13 @@ private static void ContextMenu_Unlock() isBusy = true; var selected = Selection.activeObject; - NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); + var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); - Repository - .ReleaseLock(repositoryPath, false) - .FinallyInUI((success, ex) => - { - if (success) - { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); - } - else - { - var error = ex.Message; - if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; - EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, - error, - Localization.Ok); - } - - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }) - .Start(); + UnlockObject(selected, false); + + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); } [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] @@ -207,33 +197,35 @@ private static void ContextMenu_UnlockForce() { isBusy = true; var selected = Selection.activeObject; + var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); + UnlockObject(selected, true); + + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); + } + + private static void UnlockObject(Object selected, bool force) + { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - Repository - .ReleaseLock(repositoryPath, true) - .FinallyInUI((success, ex) => - { - if (success) - { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); - } - else - { - var error = ex.Message; - if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; - EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, - error, - Localization.Ok); - } - - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }) - .Start(); + Repository.ReleaseLock(repositoryPath, force) + .FinallyInUI((success, ex) => + { + if (success) + { + manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + } + else + { + var error = ex.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; + EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, error, Localization.Ok); + } + }).Start(); } private static void OnLocksUpdate() From abe70a37363041ca1e6c90042b484a95f1010f1c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 24 May 2018 15:58:30 +0200 Subject: [PATCH 260/567] A task that encapsulates multiple concurrent tasks --- src/GitHub.Api/Tasks/ActionTask.cs | 47 ++++++++++++++++++++++++++++++ src/GitHub.Api/Tasks/TaskBase.cs | 15 ++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index e2396089c..60a186673 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -5,6 +5,53 @@ namespace GitHub.Unity { + class TaskQueue : TaskBase + { + private TaskCompletionSource aggregateTask = new TaskCompletionSource(); + private readonly List queuedTasks = new List(); + private volatile bool isSuccessful = true; + private volatile Exception exception; + private int finishedTaskCount; + + public TaskQueue() : base() + { + Initialize(aggregateTask.Task); + } + + public ITask Queue(ITask task) + { + task.OnEnd += TaskFinished; + queuedTasks.Add(task); + return this; + } + + public override ITask Start() + { + foreach (var task in queuedTasks) + task.Start(); + return base.Start(); + } + + private void TaskFinished(ITask task, bool success, Exception ex) + { + var count = Interlocked.Increment(ref finishedTaskCount); + isSuccessful &= success; + if (!success) + exception = ex; + if (count == queuedTasks.Count) + { + if (isSuccessful) + { + aggregateTask.TrySetResult(true); + } + else + { + aggregateTask.TrySetException(ex); + } + } + } + } + class ActionTask : TaskBase { protected Action Callback { get; } diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index b243da3df..32427cb51 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -131,6 +131,16 @@ protected TaskBase(CancellationToken token) protected TaskBase(Task task) : this() + { + Initialize(task); + } + + protected TaskBase() + { + this.progress = new Progress(this); + } + + protected void Initialize(Task task) { Task = new Task(t => { @@ -158,11 +168,6 @@ protected TaskBase(Task task) }, task, Token, TaskCreationOptions.None); } - protected TaskBase() - { - this.progress = new Progress(this); - } - public virtual T Then(T nextTask, TaskRunOptions runOptions = TaskRunOptions.OnSuccess, bool taskIsTopOfChain = false) where T : ITask { From 5ea03ee97a37901fc7e0ba94ffc1993828da0da2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 11:04:02 -0400 Subject: [PATCH 261/567] Validating the GitLock cache whenever any cache object is refreshed --- src/GitHub.Api/Git/Repository.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 38ca15704..33a10b1a4 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -188,6 +188,9 @@ public void Refresh(CacheType cacheType) { var cache = cacheContainer.GetCache(cacheType); cache.InvalidateData(); + + // Ensuring that the GitLock cache is kept up to date + cacheContainer.GetCache(CacheType.GitLocks).ValidateData(); } private void CacheHasBeenInvalidated(CacheType cacheType) From 3f28029f064a361b4961daaf651340a5503ac978 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 11:35:39 -0400 Subject: [PATCH 262/567] Attaching the Task Queue --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 2fef852b9..632245554 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -101,34 +101,37 @@ private static bool IsObjectUnlocked(Object selected) private static void ContextMenu_Lock() { isBusy = true; - var selected = Selection.activeObject; var unlockedObjects = Selection.objects.Where(IsObjectUnlocked).ToArray(); - LockObject(selected); + var tasks = unlockedObjects.Select(LockObject).ToArray(); - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); + var taskQueue = new TaskQueue(); + foreach (var task in tasks) + { + taskQueue.Queue(task); + } + + taskQueue.FinallyInUI((success, exception) => + { + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); + }).Start(); } - private static void LockObject(Object selected) + private static ITask LockObject(Object selected) { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - Repository.RequestLock(repositoryPath).FinallyInUI((success, ex) => { - if (success) - { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); - } - else - { - var error = ex.Message; - if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; - EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, error, Localization.Ok); - } - }).Start(); + return Repository.RequestLock(repositoryPath) + .FinallyInUI((success, ex) => + { + if (success) + { + manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); + } + }); } [MenuItem(AssetsMenuReleaseLock, true, 1000)] @@ -160,15 +163,22 @@ private static bool IsObjectLocked(Object selected) private static void ContextMenu_Unlock() { isBusy = true; - var selected = Selection.activeObject; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); + var tasks = lockedObjects.Select(o => UnlockObject(o, false)).ToArray(); - UnlockObject(selected, false); + var taskQueue = new TaskQueue(); + foreach (var task in tasks) + { + taskQueue.Queue(task); + } - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); + taskQueue.FinallyInUI((success, exception) => + { + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); + }).Start(); } [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] @@ -196,36 +206,37 @@ private static bool ContextMenu_CanUnlockForce() private static void ContextMenu_UnlockForce() { isBusy = true; - var selected = Selection.activeObject; + var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); + var tasks = lockedObjects.Select(o => UnlockObject(o, true)).ToArray(); - UnlockObject(selected, true); + var taskQueue = new TaskQueue(); + foreach (var task in tasks) + { + taskQueue.Queue(task); + } - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); + taskQueue.FinallyInUI((success, exception) => + { + isBusy = false; + Selection.activeGameObject = null; + EditorApplication.RepaintProjectWindow(); + }).Start(); } - private static void UnlockObject(Object selected, bool force) + private static ITask UnlockObject(Object selected, bool force) { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - Repository.ReleaseLock(repositoryPath, force) + return Repository.ReleaseLock(repositoryPath, force) .FinallyInUI((success, ex) => { if (success) { manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); } - else - { - var error = ex.Message; - if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; - EditorUtility.DisplayDialog(Localization.ReleaseLockActionTitle, error, Localization.Ok); - } - }).Start(); + }); } private static void OnLocksUpdate() From d2a2eea47ebaad1cbe2ab85a4cc34f7db355868a Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 11:35:57 -0400 Subject: [PATCH 263/567] Trying to use Start directly --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 632245554..57675936b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -100,7 +100,7 @@ private static bool IsObjectUnlocked(Object selected) [MenuItem(AssetsMenuRequestLock)] private static void ContextMenu_Lock() { - isBusy = true; +// isBusy = true; var unlockedObjects = Selection.objects.Where(IsObjectUnlocked).ToArray(); var tasks = unlockedObjects.Select(LockObject).ToArray(); @@ -111,12 +111,14 @@ private static void ContextMenu_Lock() taskQueue.Queue(task); } - taskQueue.FinallyInUI((success, exception) => - { - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }).Start(); + taskQueue.Start(); + +// taskQueue.FinallyInUI((success, exception) => +// { +// isBusy = false; +// Selection.activeGameObject = null; +// EditorApplication.RepaintProjectWindow(); +// }).Start(); } private static ITask LockObject(Object selected) @@ -162,7 +164,7 @@ private static bool IsObjectLocked(Object selected) [MenuItem(AssetsMenuReleaseLock, false, 1000)] private static void ContextMenu_Unlock() { - isBusy = true; +// isBusy = true; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); var tasks = lockedObjects.Select(o => UnlockObject(o, false)).ToArray(); @@ -173,12 +175,14 @@ private static void ContextMenu_Unlock() taskQueue.Queue(task); } - taskQueue.FinallyInUI((success, exception) => - { - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }).Start(); + taskQueue.Start(); + +// taskQueue.FinallyInUI((success, exception) => +// { +// isBusy = false; +// Selection.activeGameObject = null; +// EditorApplication.RepaintProjectWindow(); +// }).Start(); } [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] @@ -205,7 +209,7 @@ private static bool ContextMenu_CanUnlockForce() [MenuItem(AssetsMenuReleaseLockForced, false, 1000)] private static void ContextMenu_UnlockForce() { - isBusy = true; +// isBusy = true; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); var tasks = lockedObjects.Select(o => UnlockObject(o, true)).ToArray(); @@ -216,12 +220,14 @@ private static void ContextMenu_UnlockForce() taskQueue.Queue(task); } - taskQueue.FinallyInUI((success, exception) => - { - isBusy = false; - Selection.activeGameObject = null; - EditorApplication.RepaintProjectWindow(); - }).Start(); + taskQueue.Start(); + +// taskQueue.FinallyInUI((success, exception) => +// { +// isBusy = false; +// Selection.activeGameObject = null; +// EditorApplication.RepaintProjectWindow(); +// }).Start(); } private static ITask UnlockObject(Object selected, bool force) From 7d819821857ec459eb293fcc26472f6aa04dcbfa Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 24 May 2018 17:56:58 +0200 Subject: [PATCH 264/567] Run is the one that always gets called when kicking off tasks --- src/GitHub.Api/Tasks/ActionTask.cs | 4 ++-- src/GitHub.Api/Tasks/TaskBase.cs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index 60a186673..0969fdde5 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -25,11 +25,11 @@ public ITask Queue(ITask task) return this; } - public override ITask Start() + protected override void Run() { foreach (var task in queuedTasks) task.Start(); - return base.Start(); + base.Run(); } private void TaskFinished(ITask task, bool success, Exception ex) diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index 32427cb51..1e54cf7e2 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -299,14 +299,14 @@ public ITask Progress(Action handler) return this; } - public virtual ITask Start() + public ITask Start() { var depends = GetTopMostTaskInCreatedState() ?? this; depends.Run(); return this; } - protected void Run() + protected virtual void Run() { if (Task.Status == TaskStatus.Created) { @@ -540,6 +540,11 @@ protected TaskBase(CancellationToken token) protected TaskBase(Task task) : base() + { + Initialize(task); + } + + protected void Initialize(Task task) { Task = new Task(t => { From c98f73c65cd0bc577100b547265da889990888ad Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 13:40:48 -0400 Subject: [PATCH 265/567] Handling bulk lock and unlock operations with TaskQueue --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 57675936b..51d4b17b0 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -100,7 +100,7 @@ private static bool IsObjectUnlocked(Object selected) [MenuItem(AssetsMenuRequestLock)] private static void ContextMenu_Lock() { -// isBusy = true; + isBusy = true; var unlockedObjects = Selection.objects.Where(IsObjectUnlocked).ToArray(); var tasks = unlockedObjects.Select(LockObject).ToArray(); @@ -111,14 +111,11 @@ private static void ContextMenu_Lock() taskQueue.Queue(task); } - taskQueue.Start(); - -// taskQueue.FinallyInUI((success, exception) => -// { -// isBusy = false; -// Selection.activeGameObject = null; -// EditorApplication.RepaintProjectWindow(); -// }).Start(); + taskQueue.FinallyInUI((success, exception) => + { + isBusy = false; + Selection.activeGameObject = null; + }).Start(); } private static ITask LockObject(Object selected) @@ -164,7 +161,7 @@ private static bool IsObjectLocked(Object selected) [MenuItem(AssetsMenuReleaseLock, false, 1000)] private static void ContextMenu_Unlock() { -// isBusy = true; + isBusy = true; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); var tasks = lockedObjects.Select(o => UnlockObject(o, false)).ToArray(); @@ -175,14 +172,11 @@ private static void ContextMenu_Unlock() taskQueue.Queue(task); } - taskQueue.Start(); - -// taskQueue.FinallyInUI((success, exception) => -// { -// isBusy = false; -// Selection.activeGameObject = null; -// EditorApplication.RepaintProjectWindow(); -// }).Start(); + taskQueue.FinallyInUI((success, exception) => + { + isBusy = false; + Selection.activeGameObject = null; + }).Start(); } [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] @@ -209,7 +203,7 @@ private static bool ContextMenu_CanUnlockForce() [MenuItem(AssetsMenuReleaseLockForced, false, 1000)] private static void ContextMenu_UnlockForce() { -// isBusy = true; + isBusy = true; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); var tasks = lockedObjects.Select(o => UnlockObject(o, true)).ToArray(); @@ -220,14 +214,11 @@ private static void ContextMenu_UnlockForce() taskQueue.Queue(task); } - taskQueue.Start(); - -// taskQueue.FinallyInUI((success, exception) => -// { -// isBusy = false; -// Selection.activeGameObject = null; -// EditorApplication.RepaintProjectWindow(); -// }).Start(); + taskQueue.FinallyInUI((success, exception) => + { + isBusy = false; + Selection.activeGameObject = null; + }).Start(); } private static ITask UnlockObject(Object selected, bool force) From b4b80eb4ac46e1a646f97d004b0bdafa124f223b Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 24 May 2018 21:57:39 +0200 Subject: [PATCH 266/567] Add header to the request if we have the token --- octorun/src/bin/app-usage.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/octorun/src/bin/app-usage.js b/octorun/src/bin/app-usage.js index 07bd3b91c..9f6811d15 100644 --- a/octorun/src/bin/app-usage.js +++ b/octorun/src/bin/app-usage.js @@ -1,5 +1,6 @@ var commander = require("commander"); var package = require('../../package.json') +var config = require("../configuration"); var endOfLine = require('os').EOL; var fs = require('fs'); var util = require('util'); @@ -45,6 +46,9 @@ if (fileContents && host) { 'Content-Type': 'application/json' } }; + if (config.token) { + options.headers['Authorization'] = 'token ' + config.token; + } var req = https.request(options, function (res) { var success = res.statusCode == 200; From 50a3d7c4d8330a4a99ebb57237bfb747932642ba Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 24 May 2018 22:05:41 +0200 Subject: [PATCH 267/567] Update octorun --- octorun/version | 2 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/octorun/version b/octorun/version index de08fdd40..b2dfd9b3b 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -9fcd9faa \ No newline at end of file +b4b80eb4ac \ No newline at end of file diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 5a4719cf4..6c332478b 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -85,7 +85,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "9fcd9faa"; + public const string PackageVersion = "b4b80eb4ac"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index b4b2b9310..8a661c7ca 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac6b09ebe88bb66f5aefdd262bd9cbf9819fd6a0b7f996acfa746dc33f4dbe74 -size 219653 +oid sha256:802c9a15337ce6692f8c4c215a131b755358972cb3a7e76139193855770ab1c8 +size 214371 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 0c673acef..d2a4cc5cd 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -e562a8ccf9ef1e1d00a2e9a72f8234cf \ No newline at end of file +0a49f36d2e8df01456f832c6968a6782 From e75b1c995067844547568415b2a3d942f774d143 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 24 May 2018 13:46:08 -0400 Subject: [PATCH 268/567] Renaming methods for clarity --- .../Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 51d4b17b0..34d95dcb5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -103,7 +103,7 @@ private static void ContextMenu_Lock() isBusy = true; var unlockedObjects = Selection.objects.Where(IsObjectUnlocked).ToArray(); - var tasks = unlockedObjects.Select(LockObject).ToArray(); + var tasks = unlockedObjects.Select(CreateLockObjectTask).ToArray(); var taskQueue = new TaskQueue(); foreach (var task in tasks) @@ -118,7 +118,7 @@ private static void ContextMenu_Lock() }).Start(); } - private static ITask LockObject(Object selected) + private static ITask CreateLockObjectTask(Object selected) { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); @@ -164,7 +164,7 @@ private static void ContextMenu_Unlock() isBusy = true; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); - var tasks = lockedObjects.Select(o => UnlockObject(o, false)).ToArray(); + var tasks = lockedObjects.Select(o => CreateUnlockObjectTask(o, false)).ToArray(); var taskQueue = new TaskQueue(); foreach (var task in tasks) @@ -206,7 +206,7 @@ private static void ContextMenu_UnlockForce() isBusy = true; var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); - var tasks = lockedObjects.Select(o => UnlockObject(o, true)).ToArray(); + var tasks = lockedObjects.Select(o => CreateUnlockObjectTask(o, true)).ToArray(); var taskQueue = new TaskQueue(); foreach (var task in tasks) @@ -221,7 +221,7 @@ private static void ContextMenu_UnlockForce() }).Start(); } - private static ITask UnlockObject(Object selected, bool force) + private static ITask CreateUnlockObjectTask(Object selected, bool force) { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); From 0aa40cba58aa45fe9833f2c3543a6da3a821486a Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 28 May 2018 12:50:46 +0200 Subject: [PATCH 269/567] Bump version to 1.0.0rc3 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 44c1a4644..9d03c1349 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -34,6 +34,6 @@ internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0rc2"; + internal const string Version = "1.0.0rc3"; } } From 23c7be0e1ce4a00c3657cc9e7a6c474fe831585a Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 28 May 2018 12:33:28 +0200 Subject: [PATCH 270/567] Clean up project window initialization checks --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index c4ff8ad25..b00df2e8d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -11,11 +11,12 @@ class ProjectWindowInterface : AssetPostprocessor private const string AssetsMenuRequestLock = "Assets/Request Lock"; private const string AssetsMenuReleaseLock = "Assets/Release Lock"; private const string AssetsMenuReleaseLockForced = "Assets/Release Lock (forced)"; - private static readonly List entries = new List(); + + private static List entries = new List(); private static List locks = new List(); + private static List guids = new List(); + private static List guidsLocks = new List(); - private static readonly List guids = new List(); - private static readonly List guidsLocks = new List(); private static IApplicationManager manager; private static bool isBusy = false; private static ILogging logger; @@ -23,6 +24,7 @@ class ProjectWindowInterface : AssetPostprocessor private static CacheUpdateEvent lastRepositoryStatusChangedEvent; private static CacheUpdateEvent lastLocksChangedEvent; private static IRepository Repository { get { return manager.Environment.Repository; } } + private static bool IsInitialized { get { return Repository != null && Repository.CurrentRemote.HasValue; } } public static void Initialize(IApplicationManager theManager) { @@ -31,7 +33,7 @@ public static void Initialize(IApplicationManager theManager) manager = theManager; - if (Repository != null) + if (IsInitialized) { Repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; Repository.LocksChanged += RepositoryOnLocksChanged; @@ -39,6 +41,19 @@ public static void Initialize(IApplicationManager theManager) } } + private static bool EnsureInitialized() + { + if (locks == null) + locks = new List(); + if (entries == null) + entries = new List(); + if (guids == null) + guids = new List(); + if (guidsLocks == null) + guidsLocks = new List(); + return IsInitialized; + } + private static void ValidateCachedData() { Repository.CheckAndRaiseEventsIfCacheNewer(CacheType.GitStatus, lastRepositoryStatusChangedEvent); @@ -69,16 +84,14 @@ private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) [MenuItem(AssetsMenuRequestLock, true)] private static bool ContextMenu_CanLock() { - if (isBusy) + if (!EnsureInitialized()) return false; - if (Repository == null || !Repository.CurrentRemote.HasValue) + if (isBusy) return false; var selected = Selection.activeObject; if (selected == null) return false; - if (locks == null) - return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); @@ -113,7 +126,7 @@ private static void ContextMenu_Lock() { var error = ex.Message; if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; + error = "Failed to lock: no permissions"; EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, error, Localization.Ok); @@ -129,22 +142,19 @@ private static void ContextMenu_Lock() [MenuItem(AssetsMenuReleaseLock, true, 1000)] private static bool ContextMenu_CanUnlock() { - if (isBusy) + if (!EnsureInitialized()) return false; - if (Repository == null || !Repository.CurrentRemote.HasValue) + if (isBusy) return false; var selected = Selection.activeObject; if (selected == null) return false; - if (locks == null || locks.Count == 0) - return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - var isLocked = locks.Any(x => repositoryPath == x.Path); - return isLocked; + return locks.Any(x => repositoryPath == x.Path); } [MenuItem(AssetsMenuReleaseLock, false, 1000)] @@ -184,22 +194,19 @@ private static void ContextMenu_Unlock() [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] private static bool ContextMenu_CanUnlockForce() { - if (isBusy) + if (!EnsureInitialized()) return false; - if (Repository == null || !Repository.CurrentRemote.HasValue) + if (isBusy) return false; var selected = Selection.activeObject; if (selected == null) return false; - if (locks == null || locks.Count == 0) - return false; NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - var isLocked = locks.Any(x => repositoryPath == x.Path); - return isLocked; + return locks.Any(x => repositoryPath == x.Path); } [MenuItem(AssetsMenuReleaseLockForced, false, 1000)] @@ -238,12 +245,6 @@ private static void ContextMenu_UnlockForce() private static void OnLocksUpdate() { - if (locks == null) - { - return; - } - locks = locks.ToList(); - guidsLocks.Clear(); foreach (var lck in locks) { @@ -272,6 +273,9 @@ private static void OnStatusUpdate() private static void OnProjectWindowItemGUI(string guid, Rect itemRect) { + if (!EnsureInitialized()) + return; + if (Event.current.type != EventType.Repaint || string.IsNullOrEmpty(guid)) { return; From 1d8ef5783f07620910c17746e0ffa23544837c0c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 29 May 2018 07:19:09 +0200 Subject: [PATCH 271/567] Null checks are important here --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index b00df2e8d..4cb27e686 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -23,7 +23,7 @@ class ProjectWindowInterface : AssetPostprocessor private static ILogging Logger { get { return logger = logger ?? LogHelper.GetLogger(); } } private static CacheUpdateEvent lastRepositoryStatusChangedEvent; private static CacheUpdateEvent lastLocksChangedEvent; - private static IRepository Repository { get { return manager.Environment.Repository; } } + private static IRepository Repository { get { return manager != null ? manager.Environment.Repository : null; } } private static bool IsInitialized { get { return Repository != null && Repository.CurrentRemote.HasValue; } } public static void Initialize(IApplicationManager theManager) From a1ed76589b13b26490b43cb2f82204095273bd40 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 29 May 2018 11:52:22 +0200 Subject: [PATCH 272/567] Adding the ability to see diffs of files and folders using the configured diff viewer --- common/properties.props | 1 + src/GitHub.Api/Cache/CacheInterfaces.cs | 1 + src/GitHub.Api/Cache/CachingClasses.cs | 1 + src/GitHub.Api/Git/GitClient.cs | 7 ++ src/GitHub.Api/Git/IRepository.cs | 1 + src/GitHub.Api/Git/Repository.cs | 4 +- src/GitHub.Api/Git/RepositoryManager.cs | 9 ++- src/GitHub.Api/IO/FileSystem.cs | 13 +++- src/GitHub.Api/IO/NiceIO.cs | 76 +++++++++++++------ src/GitHub.Api/Tasks/ActionTask.cs | 8 +- src/GitHub.Api/Tasks/BaseOutputProcessor.cs | 2 +- src/GitHub.Api/Tasks/ProcessTask.cs | 10 ++- src/GitHub.Api/Tasks/TaskBase.cs | 13 +++- .../Editor/GitHub.Unity/ApplicationCache.cs | 16 ++++ .../Editor/GitHub.Unity/GitHub.Unity.csproj | 6 +- .../Editor/GitHub.Unity/UI/ChangesView.cs | 74 ++++++++++++++++++ .../Assets/Editor/GitHub.Unity/UI/Window.cs | 4 +- src/tests/IntegrationTests/CachingClasses.cs | 10 +++ .../Events/IRepositoryManagerListener.cs | 2 +- 19 files changed, 211 insertions(+), 47 deletions(-) diff --git a/common/properties.props b/common/properties.props index 60074b739..18fb9aa60 100644 --- a/common/properties.props +++ b/common/properties.props @@ -4,6 +4,7 @@ Internal ENABLE_METRICS + ENABLE_MONO $(SolutionDir)script\lib\ $(SolutionDir)lib\ diff --git a/src/GitHub.Api/Cache/CacheInterfaces.cs b/src/GitHub.Api/Cache/CacheInterfaces.cs index 810db7774..35487451e 100644 --- a/src/GitHub.Api/Cache/CacheInterfaces.cs +++ b/src/GitHub.Api/Cache/CacheInterfaces.cs @@ -102,6 +102,7 @@ public interface IRepositoryInfoCacheData GitBranch? CurrentGitBranch { get; } ConfigRemote? CurrentConfigRemote { get; } ConfigBranch? CurrentConfigBranch { get; } + string CurrentHead { get; } } public interface IRepositoryInfoCache : IManagedCache, IRepositoryInfoCacheData, ICanUpdate diff --git a/src/GitHub.Api/Cache/CachingClasses.cs b/src/GitHub.Api/Cache/CachingClasses.cs index 0f02368f6..a93f8c586 100644 --- a/src/GitHub.Api/Cache/CachingClasses.cs +++ b/src/GitHub.Api/Cache/CachingClasses.cs @@ -14,5 +14,6 @@ sealed class RepositoryInfoCacheData : IRepositoryInfoCacheData public GitBranch? CurrentGitBranch { get; set; } public ConfigRemote? CurrentConfigRemote { get; set; } public ConfigBranch? CurrentConfigBranch { get; set; } + public string CurrentHead { get; set; } } } diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 9570920a9..5a8eabd3e 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -39,6 +39,7 @@ public interface IGitClient ITask Version(IOutputProcessor processor = null); ITask LfsVersion(IOutputProcessor processor = null); ITask SetConfigNameAndEmail(string username, string email); + ITask GetHead(IOutputProcessor processor = null); } class GitClient : IGitClient @@ -303,6 +304,12 @@ public ITask Unlock(NPath file, bool force, .Configure(processManager); } + public ITask GetHead(IOutputProcessor processor = null) + { + return new FirstNonNullLineProcessTask(cancellationToken, "rev-parse --short HEAD") { Name = "Getting current head..." } + .Configure(processManager); + } + protected static ILogging Logger { get; } = LogHelper.GetLogger(); } diff --git a/src/GitHub.Api/Git/IRepository.cs b/src/GitHub.Api/Git/IRepository.cs index 1aa80c41f..148015282 100644 --- a/src/GitHub.Api/Git/IRepository.cs +++ b/src/GitHub.Api/Git/IRepository.cs @@ -60,6 +60,7 @@ public interface IRepository : IEquatable, IDisposable, IBackedByCa string CurrentBranchName { get; } List CurrentLog { get; } bool IsBusy { get; } + string CurrentHead { get; } event Action LogChanged; event Action TrackingStatusChanged; diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 33a10b1a4..926afcf70 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -238,7 +238,7 @@ private void CacheHasBeenInvalidated(CacheType cacheType) } } - private void RepositoryManagerOnCurrentBranchUpdated(ConfigBranch? branch, ConfigRemote? remote) + private void RepositoryManagerOnCurrentBranchUpdated(ConfigBranch? branch, ConfigRemote? remote, string head) { taskManager.RunInUI(() => { @@ -247,6 +247,7 @@ private void RepositoryManagerOnCurrentBranchUpdated(ConfigBranch? branch, Confi data.CurrentGitBranch = branch.HasValue ? (GitBranch?)GetLocalGitBranch(branch.Value.name, branch.Value) : null; data.CurrentConfigRemote = remote; data.CurrentGitRemote = remote.HasValue ? (GitRemote?)GetGitRemote(remote.Value) : null; + data.CurrentHead = head; name = null; cloneUrl = null; cacheContainer.RepositoryInfoCache.UpdateData(data); @@ -347,6 +348,7 @@ public void Dispose() public GitRemote? CurrentRemote => cacheContainer.RepositoryInfoCache.CurrentGitRemote; public List CurrentLog => cacheContainer.GitLogCache.Log; public List CurrentLocks => cacheContainer.GitLocksCache.GitLocks; + public string CurrentHead => cacheContainer.RepositoryInfoCache.CurrentHead; public UriString CloneUrl { diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index 865a66d26..4a5ff1cee 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -9,7 +9,7 @@ namespace GitHub.Unity public interface IRepositoryManager : IDisposable { event Action IsBusyChanged; - event Action CurrentBranchUpdated; + event Action CurrentBranchUpdated; event Action GitStatusUpdated; event Action> GitLocksUpdated; event Action> GitLogUpdated; @@ -106,7 +106,7 @@ class RepositoryManager : IRepositoryManager private bool isBusy; - public event Action CurrentBranchUpdated; + public event Action CurrentBranchUpdated; public event Action IsBusyChanged; public event Action GitStatusUpdated; public event Action GitAheadBehindStatusUpdated; @@ -394,9 +394,10 @@ public ITask UpdateRepositoryInfo() ConfigBranch? branch; ConfigRemote? remote; GetCurrentBranchAndRemote(out branch, out remote); - CurrentBranchUpdated?.Invoke(branch, remote); + var currentHead = GitClient.GetHead().RunWithReturn(true); + CurrentBranchUpdated?.Invoke(branch, remote, currentHead); }) - { Message = "Updating repository info..." };; + { Message = "Updating repository info..." }; return HookupHandlers(task, false); } diff --git a/src/GitHub.Api/IO/FileSystem.cs b/src/GitHub.Api/IO/FileSystem.cs index 94676b58e..814c916d6 100644 --- a/src/GitHub.Api/IO/FileSystem.cs +++ b/src/GitHub.Api/IO/FileSystem.cs @@ -46,12 +46,14 @@ public interface IFileSystem void WriteLines(string path, string[] contents); char DirectorySeparatorChar { get; } + string GetProcessDirectory(); } public class FileSystem : IFileSystem { private string currentDirectory; + private string processDirectory; public FileSystem() { } @@ -62,7 +64,7 @@ public FileSystem() /// Current directory public FileSystem(string directory) { - currentDirectory = directory; + processDirectory = currentDirectory = directory; } public void SetCurrentDirectory(string directory) @@ -163,7 +165,7 @@ public IEnumerable GetFiles(string path, string pattern, SearchOption se yield break; #if ENABLE_MONO - if (NPath.IsLinux) + if (NPath.IsUnix) { try { @@ -177,7 +179,7 @@ public IEnumerable GetFiles(string path, string pattern, SearchOption se { var realdir = dir; #if ENABLE_MONO - if (NPath.IsLinux) + if (NPath.IsUnix) { try { @@ -242,6 +244,11 @@ public string GetCurrentDirectory() return Directory.GetCurrentDirectory(); } + public string GetProcessDirectory() + { + return Directory.GetCurrentDirectory(); + } + public void WriteAllText(string path, string contents) { File.WriteAllText(path, contents); diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index f481e5292..7ff61671f 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -227,6 +227,7 @@ public NPath ChangeExtension(string extension) newElements[newElements.Length - 1] = newElements[newElements.Length - 1].TrimEnd('.'); return new NPath(newElements, _isRelative, _driveLetter); } + #endregion construction #region inspection @@ -307,7 +308,7 @@ public bool Exists(NPath append) public bool DirectoryExists() { ThrowIfNotInitialized(); - return FileSystem.DirectoryExists(ToString()); + return FileSystem.DirectoryExists(ToProcessDirectory().ToString()); } public bool DirectoryExists(string append) @@ -323,13 +324,13 @@ public bool DirectoryExists(NPath append) ThrowIfNotInitialized(); if (!append.IsInitialized) return DirectoryExists(); - return FileSystem.DirectoryExists(Combine(append).ToString()); + return FileSystem.DirectoryExists(Combine(append).ToProcessDirectory().ToString()); } public bool FileExists() { ThrowIfNotInitialized(); - return FileSystem.FileExists(ToString()); + return FileSystem.FileExists(ToProcessDirectory().ToString()); } public bool FileExists(string append) @@ -345,7 +346,7 @@ public bool FileExists(NPath append) ThrowIfNotInitialized(); if (!append.IsInitialized) return FileExists(); - return FileSystem.FileExists(Combine(append).ToString()); + return FileSystem.FileExists(Combine(append).ToProcessDirectory().ToString()); } public string ExtensionWithDot @@ -534,7 +535,7 @@ public bool IsRoot public IEnumerable Files(string filter, bool recurse = false) { - return FileSystem.GetFiles(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); + return FileSystem.GetFiles(ToProcessDirectory().ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); } public IEnumerable Files(bool recurse = false) @@ -554,7 +555,7 @@ public IEnumerable Contents(bool recurse = false) public IEnumerable Directories(string filter, bool recurse = false) { - return FileSystem.GetDirectories(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); + return FileSystem.GetDirectories(ToProcessDirectory().ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); } public IEnumerable Directories(bool recurse = false) @@ -571,7 +572,7 @@ public NPath CreateFile() ThrowIfRelative(); ThrowIfRoot(); EnsureParentDirectoryExists(); - FileSystem.WriteAllBytes(ToString(), new byte[0]); + FileSystem.WriteAllBytes(ToProcessDirectory().ToString(), new byte[0]); return this; } @@ -596,7 +597,7 @@ public NPath CreateDirectory() if (IsRoot) throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString()); - FileSystem.DirectoryCreate(ToString()); + FileSystem.DirectoryCreate(ToProcessDirectory().ToString()); return this; } @@ -665,7 +666,7 @@ NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func absoluteDestination.EnsureParentDirectoryExists(); - FileSystem.FileCopy(ToString(), absoluteDestination.ToString(), true); + FileSystem.FileCopy(ToProcessDirectory().ToString(), absoluteDestination.ToString(), true); return absoluteDestination; } @@ -689,11 +690,11 @@ public void Delete(DeleteMode deleteMode = DeleteMode.Normal) throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString()); if (FileExists()) - FileSystem.FileDelete(ToString()); + FileSystem.FileDelete(ToProcessDirectory().ToString()); else if (DirectoryExists()) try { - FileSystem.DirectoryDelete(ToString(), true); + FileSystem.DirectoryDelete(ToProcessDirectory().ToString(), true); } catch (IOException) { @@ -789,52 +790,52 @@ public NPath Move(NPath dest) { dest.DeleteIfExists(); dest.EnsureParentDirectoryExists(); - FileSystem.FileMove(ToString(), dest.ToString()); + FileSystem.FileMove(ToProcessDirectory().ToString(), dest.ToProcessDirectory().ToString()); return dest; } if (DirectoryExists()) { - FileSystem.DirectoryMove(ToString(), dest.ToString()); + FileSystem.DirectoryMove(ToProcessDirectory().ToString(), dest.ToProcessDirectory().ToString()); return dest; } - throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString()); + throw new ArgumentException("Move() called on a path that doesn't exist: " + ToProcessDirectory().ToString()); } public NPath WriteAllText(string contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllText(ToString(), contents); + FileSystem.WriteAllText(ToProcessDirectory().ToString(), contents); return this; } public string ReadAllText() { ThrowIfNotInitialized(); - return FileSystem.ReadAllText(ToString()); + return FileSystem.ReadAllText(ToProcessDirectory().ToString()); } public NPath WriteAllText(string contents, Encoding encoding) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllText(ToString(), contents, encoding); + FileSystem.WriteAllText(ToProcessDirectory().ToString(), contents, encoding); return this; } public string ReadAllText(Encoding encoding) { ThrowIfNotInitialized(); - return FileSystem.ReadAllText(ToString(), encoding); + return FileSystem.ReadAllText(ToProcessDirectory().ToString(), encoding); } public NPath WriteLines(string[] contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteLines(ToString(), contents); + FileSystem.WriteLines(ToProcessDirectory().ToString(), contents); return this; } @@ -842,28 +843,28 @@ public NPath WriteAllLines(string[] contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllLines(ToString(), contents); + FileSystem.WriteAllLines(ToProcessDirectory().ToString(), contents); return this; } public string[] ReadAllLines() { ThrowIfNotInitialized(); - return FileSystem.ReadAllLines(ToString()); + return FileSystem.ReadAllLines(ToProcessDirectory().ToString()); } public NPath WriteAllBytes(byte[] contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllBytes(ToString(), contents); + FileSystem.WriteAllBytes(ToProcessDirectory().ToString(), contents); return this; } public byte[] ReadAllBytes() { ThrowIfNotInitialized(); - return FileSystem.ReadAllBytes(ToString()); + return FileSystem.ReadAllBytes(ToProcessDirectory().ToString()); } @@ -901,6 +902,14 @@ public static NPath CurrentDirectory } } + public static NPath ProcessDirectory + { + get + { + return new NPath(FileSystem.GetProcessDirectory()); + } + } + public static NPath HomeDirectory { get @@ -947,6 +956,13 @@ private static void ThrowIfNotInitialized(NPath path) path.ThrowIfNotInitialized(); } + public NPath ToProcessDirectory() + { + if (!IsRelative) + return this; + return MakeAbsolute().RelativeTo(NPath.ProcessDirectory); + } + public NPath EnsureDirectoryExists(string append = "") { ThrowIfNotInitialized(); @@ -1177,8 +1193,20 @@ public static NPath Resolve(this NPath path) public static string CalculateMD5(this NPath path) { - return NPath.FileSystem.CalculateFileMD5(path); + return NPath.FileSystem.CalculateFileMD5(path.ToProcessDirectory()); } + + public static NPath CreateTempDirectory(this NPath baseDir, string myprefix = "") + { + var random = new Random(); + while (true) + { + var candidate = baseDir.Combine(myprefix + "_" + random.Next()); + if (!candidate.Exists()) + return candidate.CreateDirectory(); + } + } + } public enum SlashMode diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index e2396089c..b2ad3a459 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -221,16 +221,16 @@ class FuncTask : TaskBase protected Func Callback { get; } protected Func CallbackWithException { get; } - public FuncTask(CancellationToken token, Func action) - : base(token) + public FuncTask(CancellationToken token, Func action, Func getPreviousResult = null) + : base(token, getPreviousResult) { Guard.ArgumentNotNull(action, "action"); this.Callback = action; Name = $"FuncTask<{typeof(T)}, {typeof(TResult)}>"; } - public FuncTask(CancellationToken token, Func action) - : base(token) + public FuncTask(CancellationToken token, Func action, Func getPreviousResult = null) + : base(token, getPreviousResult) { Guard.ArgumentNotNull(action, "action"); this.CallbackWithException = action; diff --git a/src/GitHub.Api/Tasks/BaseOutputProcessor.cs b/src/GitHub.Api/Tasks/BaseOutputProcessor.cs index b0b0342e5..51878efe4 100644 --- a/src/GitHub.Api/Tasks/BaseOutputProcessor.cs +++ b/src/GitHub.Api/Tasks/BaseOutputProcessor.cs @@ -110,7 +110,7 @@ protected override bool ProcessLine(string line, out string result) result = null; if (String.IsNullOrEmpty(line)) return false; - result = line; + result = line.Trim(); return true; } } diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index a5aa3c103..eb9c651bd 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -492,7 +492,7 @@ public override string ToString() class FirstNonNullLineProcessTask : ProcessTask { - private readonly NPath fullPathToExecutable; + private readonly NPath? fullPathToExecutable; private readonly string arguments; public FirstNonNullLineProcessTask(CancellationToken token, NPath fullPathToExecutable, string arguments) @@ -502,7 +502,13 @@ public FirstNonNullLineProcessTask(CancellationToken token, NPath fullPathToExec this.arguments = arguments; } - public override string ProcessName => fullPathToExecutable.FileName; + public FirstNonNullLineProcessTask(CancellationToken token, string arguments) + : base(token, new FirstNonNullLineOutputProcessor()) + { + this.arguments = arguments; + } + + public override string ProcessName => fullPathToExecutable?.FileName; public override string ProcessArguments => arguments; } diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index b243da3df..95427eb25 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -699,13 +699,20 @@ protected override void CallFinallyHandler() abstract class TaskBase : TaskBase { - public TaskBase(CancellationToken token) + public TaskBase(CancellationToken token, Func getPreviousResult = null) : base(token) { Task = new Task(() => { var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); - T prevResult = previousIsSuccessful && DependsOn != null && DependsOn is ITask ? ((ITask)DependsOn).Result : default(T); + // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task + // otherwise if there's a method to retrieve the value, call that + // otherwise use the PreviousResult property + T prevResult = PreviousResult; + if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) + prevResult = ((ITask)DependsOn).Result; + else if (getPreviousResult != null) + prevResult = getPreviousResult(); var ret = RunWithData(previousIsSuccessful, prevResult); tcs.SetResult(ret); return ret; @@ -722,6 +729,8 @@ protected virtual TResult RunWithData(bool success, T previousResult) base.Run(success); return default(TResult); } + + public T PreviousResult { get; set; } = default(T); } abstract class DataTaskBase : TaskBase, ITask diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs index a8d36bce3..b1de8018d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs @@ -447,6 +447,7 @@ sealed class RepositoryInfoCache : ManagedCacheBase, IRepos [SerializeField] private GitBranch currentGitBranch; [SerializeField] private ConfigBranch currentConfigBranch; [SerializeField] private ConfigRemote currentConfigRemote; + [SerializeField] private string currentHead; public RepositoryInfoCache() : base(CacheType.RepositoryInfo) { } @@ -480,6 +481,12 @@ public void UpdateData(IRepositoryInfoCacheData data) isUpdated = true; } + if (forcedInvalidation ||!String.Equals(currentHead, data.CurrentHead)) + { + currentHead = data.CurrentHead; + isUpdated = true; + } + SaveData(now, isUpdated); } @@ -519,6 +526,15 @@ public ConfigBranch? CurrentConfigBranch } } + public string CurrentHead + { + get + { + ValidateData(); + return currentHead; + } + } + public override TimeSpan DataTimeout { get { return TimeSpan.FromDays(1); } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index 1fa8438aa..f91279e51 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -19,7 +19,7 @@ true full false - DEBUG;TRACE + DEBUG;TRACE;$(BuildDefs) prompt 4 4 @@ -30,7 +30,7 @@ pdbonly true - TRACE + TRACE;$(BuildDefs) prompt 4 4 @@ -43,7 +43,7 @@ true full false - DEBUG;TRACE;DEVELOPER_BUILD + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) prompt 4 4 diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 0a94770eb..0051b3696 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -189,6 +189,34 @@ private GenericMenu CreateContextMenu(ChangesTreeNode node) { var genericMenu = new GenericMenu(); + genericMenu.AddItem(new GUIContent("Show Diff"), false, () => + { + ITask calculateDiff = null; + if (node.IsFolder) + { + calculateDiff = CalculateFolderDiff(node); + } + else + { + calculateDiff = CalculateFileDiff(node); + } + calculateDiff.FinallyInUI((s, ex, leftRight) => + { + if (s) + EditorUtility.InvokeDiffTool( + leftRight[0].IsInitialized ? leftRight[0].FileName : null, + leftRight[0].IsInitialized ? leftRight[0].MakeAbsolute().ToString() : null, + leftRight[1].IsInitialized ? leftRight[1].FileName : null, + leftRight[1].IsInitialized ? leftRight[1].MakeAbsolute().ToString() : null, + null, null); + else + throw ex; + }) + .Start(); + }); + + genericMenu.AddSeparator(""); + if (discardGuiContent == null) { discardGuiContent = new GUIContent("Discard"); @@ -215,6 +243,50 @@ private GenericMenu CreateContextMenu(ChangesTreeNode node) return genericMenu; } + private ITask CalculateFolderDiff(ChangesTreeNode node) + { + var rightFile = node.Path.ToNPath(); + var tmpDir = Manager.Environment.UnityProjectPath.Combine("Temp").CreateTempDirectory(); + var changedFiles = treeChanges.GetLeafNodes(node).Select(x => x.Path.ToNPath()).ToList(); + return new FuncTask, NPath[]>(TaskManager.Token, (s, files) => + { + var leftFolder = tmpDir.Combine("left", rightFile.FileName); + var rightFolder = tmpDir.Combine("right", rightFile.FileName); + foreach (var file in files) + { + var txt = new SimpleProcessTask(TaskManager.Token, "show HEAD:\"" + file.ToString(SlashMode.Forward) + "\"") + .Configure(Manager.ProcessManager, false) + .Catch(_ => true) + .RunWithReturn(true); + if (txt != null) + leftFolder.Combine(file.RelativeTo(rightFile)).WriteAllText(txt); + if (file.FileExists()) + rightFolder.Combine(file.RelativeTo(rightFile)).WriteAllText(file.ReadAllText()); + } + return new NPath[] { leftFolder, rightFolder }; + }, () => changedFiles) { Message = "Calculating diff..." }; + } + + private ITask CalculateFileDiff(ChangesTreeNode node) + { + var rightFile = node.Path.ToNPath(); + var tmpDir = Manager.Environment.UnityProjectPath.Combine("Temp", "ghu-diffs").EnsureDirectoryExists(); + var leftFile = tmpDir.Combine(rightFile.FileName + "_" + Repository.CurrentHead + rightFile.ExtensionWithDot); + return new SimpleProcessTask(TaskManager.Token, "show HEAD:\"" + rightFile.ToString(SlashMode.Forward) + "\"") + .Configure(Manager.ProcessManager, false) + .Catch(_ => true) + .Then((success, txt) => + { + if (success) + leftFile.WriteAllText(txt); + else + leftFile = NPath.Default; + if (!rightFile.FileExists()) + rightFile = NPath.Default; + return new NPath[] { leftFile, rightFile }; + }); + } + private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) { if (!lastStatusEntriesChangedEvent.Equals(cacheUpdateEvent)) @@ -344,6 +416,8 @@ private void DoCommitGUI() GUILayout.Space(Styles.CommitAreaPadding); // Disable committing when already committing or if we don't have all the data needed + //Debug.LogFormat("IsBusy:{0} string.IsNullOrEmpty(commitMessage): {1} treeChanges.GetCheckedFiles().Any(): {2}", + // IsBusy, string.IsNullOrEmpty(commitMessage), treeChanges.GetCheckedFiles().Any()); EditorGUI.BeginDisabledGroup(IsBusy || string.IsNullOrEmpty(commitMessage) || !treeChanges.GetCheckedFiles().Any()); { GUILayout.BeginHorizontal(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 34e2b5228..faaf926e5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -568,7 +568,7 @@ private void DoActionbarGUI() EditorUtility.DisplayDialog(Localization.PullConfirmTitle, String.Format(Localization.PullConfirmDescription, currentRemoteName), Localization.PullConfirmYes, - Localization.PullConfirmCancel) + Localization.Cancel) ) { Pull(); @@ -586,7 +586,7 @@ private void DoActionbarGUI() EditorUtility.DisplayDialog(Localization.PushConfirmTitle, String.Format(Localization.PushConfirmDescription, currentRemoteName), Localization.PushConfirmYes, - Localization.PushConfirmCancel) + Localization.Cancel) ) { Push(); diff --git a/src/tests/IntegrationTests/CachingClasses.cs b/src/tests/IntegrationTests/CachingClasses.cs index fcbec1a2c..1699e12b5 100644 --- a/src/tests/IntegrationTests/CachingClasses.cs +++ b/src/tests/IntegrationTests/CachingClasses.cs @@ -363,6 +363,7 @@ sealed class RepositoryInfoCache : ManagedCacheBase, IRepos private GitBranch currentGitBranch; private ConfigBranch currentConfigBranch; private ConfigRemote currentConfigRemote; + private string currentHead; public RepositoryInfoCache() : base(CacheType.RepositoryInfo) { } @@ -435,6 +436,15 @@ public ConfigBranch? CurrentConfigBranch } } + public string CurrentHead + { + get + { + ValidateData(); + return currentHead; + } + } + public override TimeSpan DataTimeout { get { return TimeSpan.FromDays(1); } } } diff --git a/src/tests/TestUtils/Events/IRepositoryManagerListener.cs b/src/tests/TestUtils/Events/IRepositoryManagerListener.cs index 240996b82..c862eec49 100644 --- a/src/tests/TestUtils/Events/IRepositoryManagerListener.cs +++ b/src/tests/TestUtils/Events/IRepositoryManagerListener.cs @@ -85,7 +85,7 @@ public static void AttachListener(this IRepositoryManagerListener listener, managerEvents?.isNotBusy.TrySetResult(true); }; - repositoryManager.CurrentBranchUpdated += (configBranch, configRemote) => { + repositoryManager.CurrentBranchUpdated += (configBranch, configRemote, head) => { logger?.Trace("CurrentBranchUpdated"); listener.CurrentBranchUpdated(configBranch, configRemote); managerEvents?.currentBranchUpdated.TrySetResult(true); From 4cc3169d5b25d3ac53aec4b44e5852845f196e3f Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 11 Apr 2018 14:29:25 +0200 Subject: [PATCH 273/567] Obey the DeleteMode flag when deleting files too --- src/GitHub.Api/IO/NiceIO.cs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index f481e5292..c4bf61c1a 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -688,20 +688,27 @@ public void Delete(DeleteMode deleteMode = DeleteMode.Normal) if (IsRoot) throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString()); - if (FileExists()) - FileSystem.FileDelete(ToString()); - else if (DirectoryExists()) - try + var isFile = FileExists(); + var isDir = DirectoryExists(); + if (!isFile && !isDir) + throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString()); + + try + { + if (isFile) { - FileSystem.DirectoryDelete(ToString(), true); + FileSystem.FileDelete(ToString()); } - catch (IOException) + else { - if (deleteMode == DeleteMode.Normal) - throw; + FileSystem.DirectoryDelete(ToString(), true); } - else - throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString()); + } + catch (IOException) + { + if (deleteMode == DeleteMode.Normal) + throw; + } } public void DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal) From 00a4512e0c94762d62e689813cc0415fb3369028 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 28 May 2018 20:57:18 +0200 Subject: [PATCH 274/567] Discard should always ask for confirmation --- src/GitHub.Api/Localization.Designer.cs | 83 ++++++++++--------- src/GitHub.Api/Localization.resx | 17 ++-- .../Editor/GitHub.Unity/UI/ChangesView.cs | 9 +- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 4 +- 4 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/GitHub.Api/Localization.Designer.cs b/src/GitHub.Api/Localization.Designer.cs index a8877e72e..ca67ee71e 100644 --- a/src/GitHub.Api/Localization.Designer.cs +++ b/src/GitHub.Api/Localization.Designer.cs @@ -68,7 +68,7 @@ public static string AccountButton { return ResourceManager.GetString("AccountButton", resourceCulture); } } - + /// /// Looks up a localized string similar to {0}. /// @@ -88,7 +88,7 @@ public static string BranchesTitle { } /// - /// Looks up a localized string similar to cancel. + /// Looks up a localized string similar to Cancel. /// public static string Cancel { get { @@ -159,6 +159,33 @@ public static string DescriptionLabel { } } + /// + /// Looks up a localized string similar to Are you sure you want to discard these changes?. + /// + public static string DiscardConfirmDescription { + get { + return ResourceManager.GetString("DiscardConfirmDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Discard Changes?. + /// + public static string DiscardConfirmTitle { + get { + return ResourceManager.GetString("DiscardConfirmTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Discard. + /// + public static string DiscardConfirmYes { + get { + return ResourceManager.GetString("DiscardConfirmYes", resourceCulture); + } + } + /// /// Looks up a localized string similar to Fetch Changes. /// @@ -392,7 +419,7 @@ public static string MessageBranchCreated { return ResourceManager.GetString("MessageBranchCreated", resourceCulture); } } - + /// /// Looks up a localized string similar to Deleted branch {0}. /// @@ -401,7 +428,7 @@ public static string MessageBranchDeleted { return ResourceManager.GetString("MessageBranchDeleted", resourceCulture); } } - + /// /// Looks up a localized string similar to Switched to branch {0}. /// @@ -410,7 +437,7 @@ public static string MessageBranchSwitched { return ResourceManager.GetString("MessageBranchSwitched", resourceCulture); } } - + /// /// Looks up a localized string similar to Commit failed. /// @@ -419,7 +446,7 @@ public static string MessageCommitFailed { return ResourceManager.GetString("MessageCommitFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Committed. /// @@ -428,7 +455,7 @@ public static string MessageCommitted { return ResourceManager.GetString("MessageCommitted", resourceCulture); } } - + /// /// Looks up a localized string similar to Committing. /// @@ -437,7 +464,7 @@ public static string MessageCommitting { return ResourceManager.GetString("MessageCommitting", resourceCulture); } } - + /// /// Looks up a localized string similar to Fetched. /// @@ -446,7 +473,7 @@ public static string MessageFetched { return ResourceManager.GetString("MessageFetched", resourceCulture); } } - + /// /// Looks up a localized string similar to Fetch failed. /// @@ -455,7 +482,7 @@ public static string MessageFetchFailed { return ResourceManager.GetString("MessageFetchFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Fetching. /// @@ -464,7 +491,7 @@ public static string MessageFetching { return ResourceManager.GetString("MessageFetching", resourceCulture); } } - + /// /// Looks up a localized string similar to Pulled. /// @@ -473,7 +500,7 @@ public static string MessagePulled { return ResourceManager.GetString("MessagePulled", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to pull. /// @@ -482,7 +509,7 @@ public static string MessagePullFailed { return ResourceManager.GetString("MessagePullFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Pulling. /// @@ -491,7 +518,7 @@ public static string MessagePulling { return ResourceManager.GetString("MessagePulling", resourceCulture); } } - + /// /// Looks up a localized string similar to Pushed. /// @@ -500,7 +527,7 @@ public static string MessagePushed { return ResourceManager.GetString("MessagePushed", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to push. /// @@ -509,7 +536,7 @@ public static string MessagePushFailed { return ResourceManager.GetString("MessagePushFailed", resourceCulture); } } - + /// /// Looks up a localized string similar to Pushing. /// @@ -518,7 +545,7 @@ public static string MessagePushing { return ResourceManager.GetString("MessagePushing", resourceCulture); } } - + /// /// Looks up a localized string similar to Refreshed. /// @@ -527,7 +554,7 @@ public static string MessageRefreshed { return ResourceManager.GetString("MessageRefreshed", resourceCulture); } } - + /// /// Looks up a localized string similar to Refreshing. /// @@ -536,7 +563,7 @@ public static string MessageRefreshing { return ResourceManager.GetString("MessageRefreshing", resourceCulture); } } - + /// /// Looks up a localized string similar to Your current project is not currently in an active git repository:. /// @@ -645,15 +672,6 @@ public static string PullButtonCount { } } - /// - /// Looks up a localized string similar to Cancel. - /// - public static string PullConfirmCancel { - get { - return ResourceManager.GetString("PullConfirmCancel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Would you like to pull changes from remote '{0}'?. /// @@ -726,15 +744,6 @@ public static string PushButtonCount { } } - /// - /// Looks up a localized string similar to Cancel. - /// - public static string PushConfirmCancel { - get { - return ResourceManager.GetString("PushConfirmCancel", resourceCulture); - } - } - /// /// Looks up a localized string similar to Would you like to push changes to remote '{0}'?. /// diff --git a/src/GitHub.Api/Localization.resx b/src/GitHub.Api/Localization.resx index bfb82e1f9..da57ad667 100644 --- a/src/GitHub.Api/Localization.resx +++ b/src/GitHub.Api/Localization.resx @@ -198,9 +198,6 @@ Pull - - Cancel - Push Changes? @@ -210,9 +207,6 @@ Push - - Cancel - Commit summary @@ -265,7 +259,7 @@ ok - cancel + Cancel Pull @@ -426,4 +420,13 @@ Refreshing + + Are you sure you want to discard these changes? + + + Discard Changes? + + + Discard + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 0a94770eb..600f8d134 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -194,7 +194,14 @@ private GenericMenu CreateContextMenu(ChangesTreeNode node) discardGuiContent = new GUIContent("Discard"); } - genericMenu.AddItem(discardGuiContent, false, () => { + genericMenu.AddItem(discardGuiContent, false, () => + { + if (!EditorUtility.DisplayDialog(Localization.DiscardConfirmTitle, + Localization.DiscardConfirmDescription, + Localization.DiscardConfirmYes, + Localization.Cancel)) + return; + GitStatusEntry[] discardEntries; if (node.isFolder) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 34e2b5228..faaf926e5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -568,7 +568,7 @@ private void DoActionbarGUI() EditorUtility.DisplayDialog(Localization.PullConfirmTitle, String.Format(Localization.PullConfirmDescription, currentRemoteName), Localization.PullConfirmYes, - Localization.PullConfirmCancel) + Localization.Cancel) ) { Pull(); @@ -586,7 +586,7 @@ private void DoActionbarGUI() EditorUtility.DisplayDialog(Localization.PushConfirmTitle, String.Format(Localization.PushConfirmDescription, currentRemoteName), Localization.PushConfirmYes, - Localization.PushConfirmCancel) + Localization.Cancel) ) { Push(); From 4a32215489ecc7b8ea2ffe449c0bd74f808771f5 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 28 May 2018 20:54:02 +0200 Subject: [PATCH 275/567] Usage tracker should never be null --- .../Application/ApplicationManagerBase.cs | 38 +++++++------------ src/GitHub.Api/Metrics/IMetricsService.cs | 2 +- src/GitHub.Api/Metrics/IUsageTracker.cs | 1 + src/GitHub.Api/Metrics/UsageTracker.cs | 38 ++++++++++++------- .../IntegrationTests/Metrics/MetricsTests.cs | 22 ++++++++--- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 58d3f1eb9..b9f16fc71 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -47,6 +47,16 @@ protected void Initialize() ApplicationConfiguration.WebTimeout = UserSettings.Get(Constants.WebTimeoutKey, ApplicationConfiguration.WebTimeout); Platform.Initialize(ProcessManager, TaskManager); progress.OnProgress += progressReporter.UpdateProgress; + UsageTracker = new UsageTracker(UserSettings, Environment, InstanceId.ToString()); + +#if ENABLE_METRICS + var metricsService = new MetricsService(ProcessManager, + TaskManager, + Environment.FileSystem, + Environment.NodeJsExecutablePath, + Environment.OctorunScriptPath); + UsageTracker.MetricsService = metricsService; +#endif } public void Run() @@ -59,8 +69,7 @@ public void Run() GitInstallationState state = new GitInstallationState(); try { - SetupMetrics(Environment.UnityVersion); - + SetupMetrics(); if (Environment.IsMac) { var getEnvPath = new SimpleProcessTask(TaskManager.Token, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") @@ -301,35 +310,14 @@ public void RestartRepository() Logger.Trace($"Got a repository? {(Environment.Repository != null ? Environment.Repository.LocalPath : "null")}"); } - protected void SetupMetrics(string unityVersion) + protected void SetupMetrics() { - string userId = null; - if (UserSettings.Exists(Constants.GuidKey)) - { - userId = UserSettings.Get(Constants.GuidKey); - } - - if (String.IsNullOrEmpty(userId)) - { - userId = Guid.NewGuid().ToString(); - UserSettings.Set(Constants.GuidKey, userId); - } - -#if ENABLE_METRICS - var metricsService = new MetricsService(ProcessManager, - TaskManager, - Environment.FileSystem, - Environment.NodeJsExecutablePath, - Environment.OctorunScriptPath); - - UsageTracker = new UsageTracker(metricsService, UserSettings, Environment, userId, unityVersion, InstanceId.ToString()); - if (firstRun) { UsageTracker.IncrementNumberOfStartups(); } -#endif } + protected abstract void InitializeUI(); protected abstract void InitializationComplete(); diff --git a/src/GitHub.Api/Metrics/IMetricsService.cs b/src/GitHub.Api/Metrics/IMetricsService.cs index 78f18bb7d..858e946f1 100644 --- a/src/GitHub.Api/Metrics/IMetricsService.cs +++ b/src/GitHub.Api/Metrics/IMetricsService.cs @@ -3,7 +3,7 @@ namespace GitHub.Unity { - interface IMetricsService + public interface IMetricsService { /// /// Posts the provided usage model. diff --git a/src/GitHub.Api/Metrics/IUsageTracker.cs b/src/GitHub.Api/Metrics/IUsageTracker.cs index b3a8b9942..64a8f4fe7 100644 --- a/src/GitHub.Api/Metrics/IUsageTracker.cs +++ b/src/GitHub.Api/Metrics/IUsageTracker.cs @@ -3,6 +3,7 @@ public interface IUsageTracker { bool Enabled { get; set; } + IMetricsService MetricsService { get; set; } void IncrementNumberOfStartups(); void IncrementChangesViewButtonCommit(); void IncrementHistoryViewToolbarFetch(); diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 40c113b30..68b5eb6bb 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -14,33 +14,43 @@ class UsageTracker : IUsageTracker private readonly ISettings userSettings; private readonly IUsageLoader usageLoader; - private readonly IMetricsService metricsService; private readonly string userId; private readonly string appVersion; private readonly string unityVersion; private readonly string instanceId; private Timer timer; - public UsageTracker(IMetricsService metricsService, ISettings userSettings, - IEnvironment environment, string userId, string unityVersion, string instanceId) - : this(metricsService, userSettings, - new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), - userId, unityVersion, instanceId) + public IMetricsService MetricsService { get; set; } + + public UsageTracker(ISettings userSettings, + IEnvironment environment, string instanceId) + : this(userSettings, + new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), + environment.UnityVersion, instanceId) { } - public UsageTracker(IMetricsService metricsService, ISettings userSettings, + public UsageTracker(ISettings userSettings, IUsageLoader usageLoader, - string userId, string unityVersion, string instanceId) + string unityVersion, string instanceId) { this.userSettings = userSettings; this.usageLoader = usageLoader; - this.metricsService = metricsService; - this.userId = userId; this.appVersion = ApplicationInfo.Version; this.unityVersion = unityVersion; this.instanceId = instanceId; + if (userSettings.Exists(Constants.GuidKey)) + { + userId = userSettings.Get(Constants.GuidKey); + } + + if (String.IsNullOrEmpty(userId)) + { + userId = Guid.NewGuid().ToString(); + userSettings.Set(Constants.GuidKey, userId); + } + Logger.Trace("userId:{0} instanceId:{1}", userId, instanceId); if (Enabled) RunTimer(3*60); @@ -61,14 +71,14 @@ private void RunTimer(int seconds) private async Task SendUsage() { - var usageStore = usageLoader.Load(userId); - - if (metricsService == null) + if (MetricsService == null) { Logger.Warning("No service, not sending usage"); return; } + var usageStore = usageLoader.Load(userId); + if (usageStore.LastUpdated.Date != DateTimeOffset.UtcNow.Date) { var currentTimeOffset = DateTimeOffset.UtcNow; @@ -90,7 +100,7 @@ private async Task SendUsage() try { - await metricsService.PostUsage(extractReports); + await MetricsService.PostUsage(extractReports); success = true; } catch (Exception ex) diff --git a/src/tests/IntegrationTests/Metrics/MetricsTests.cs b/src/tests/IntegrationTests/Metrics/MetricsTests.cs index 3902574fc..6ae28aba2 100644 --- a/src/tests/IntegrationTests/Metrics/MetricsTests.cs +++ b/src/tests/IntegrationTests/Metrics/MetricsTests.cs @@ -34,11 +34,14 @@ public void IncrementMetricsWorks(string measureName) var instanceId = Guid.NewGuid().ToString(); var usageLoader = Substitute.For(); var usageStore = new UsageStore(); + var settings = Substitute.For(); + settings.Exists(Arg.Is(Constants.GuidKey)).Returns(true); + settings.Get(Arg.Is(Constants.GuidKey)).Returns(userId); + usageStore.Model.Guid = userId; usageLoader.Load(Arg.Is(userId)).Returns(usageStore); - var usageTracker = new UsageTracker(Substitute.For(), Substitute.For(), - usageLoader, userId, unityVersion, instanceId); + var usageTracker = new UsageTracker(settings, usageLoader, unityVersion, instanceId); var currentUsage = usageStore.GetCurrentMeasures(appVersion, unityVersion, instanceId); var prop = currentUsage.GetType().GetProperty(measureName); @@ -59,14 +62,21 @@ public void LoadingWorks() var instanceId = Guid.NewGuid().ToString(); var usageStore = new UsageStore(); usageStore.Model.Guid = userId; - var usageTracker = new UsageTracker(Substitute.For(), Substitute.For(), - Environment, userId, unityVersion, instanceId); - usageTracker.IncrementNumberOfStartups(); var storePath = Environment.UserCachePath.Combine(Constants.UsageFile); + var usageLoader = new UsageLoader(storePath); + + var settings = Substitute.For(); + settings.Exists(Arg.Is(Constants.GuidKey)).Returns(true); + settings.Get(Arg.Is(Constants.GuidKey)).Returns(userId); + var usageTracker = new UsageTracker(settings, usageLoader, unityVersion, instanceId); + + usageTracker.IncrementNumberOfStartups(); + usageTracker.IncrementNumberOfStartups(); + Assert.IsTrue(storePath.FileExists()); var json = storePath.ReadAllText(Encoding.UTF8); var savedStore = json.FromJson(lowerCase: true); - Assert.AreEqual(1, savedStore.GetCurrentMeasures(appVersion, unityVersion, instanceId).NumberOfStartups); + Assert.AreEqual(2, savedStore.GetCurrentMeasures(appVersion, unityVersion, instanceId).NumberOfStartups); } } } From 645420f7d42e77ff7c5fd1b94cf50879b6fcd6f0 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 28 May 2018 12:50:29 +0200 Subject: [PATCH 276/567] Make sure we shutdown promptly --- src/GitHub.Api/Managers/Downloader.cs | 6 ++-- src/GitHub.Api/Tasks/ActionTask.cs | 36 ++++++++++++++++++----- src/GitHub.Api/Tasks/ProcessTask.cs | 1 + src/GitHub.Api/Tasks/TaskBase.cs | 42 ++++++++++++++++++++------- 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/GitHub.Api/Managers/Downloader.cs b/src/GitHub.Api/Managers/Downloader.cs index 81481fc43..55552d5a0 100644 --- a/src/GitHub.Api/Managers/Downloader.cs +++ b/src/GitHub.Api/Managers/Downloader.cs @@ -151,6 +151,9 @@ public static bool Download(ILogging logger, UriString url, else logger.Trace($"Downloading {url}"); + if (!onProgress(bytes, bytes * 2)) + return false; + using (var webResponse = (HttpWebResponse)webRequest.GetResponseWithoutException()) { var httpStatusCode = webResponse.StatusCode; @@ -158,8 +161,7 @@ public static bool Download(ILogging logger, UriString url, if (expectingResume && httpStatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) { - onProgress(bytes, bytes); - return true; + return !onProgress(bytes, bytes); } if (!(httpStatusCode == HttpStatusCode.OK || httpStatusCode == HttpStatusCode.PartialContent)) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index e2396089c..30dca1376 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -83,12 +83,24 @@ public ActionTask(CancellationToken token, Action action, Func getPr { Guard.ArgumentNotNull(action, "action"); this.Callback = action; - Task = new Task(() => Run(DependsOn?.Successful ?? true, + Task = new Task(() => + { + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task // otherwise if there's a method to retrieve the value, call that // otherwise use the PreviousResult property - (DependsOn?.Successful ?? false) ? ((ITask)DependsOn).Result : getPreviousResult != null ? getPreviousResult() : PreviousResult), - Token, TaskCreationOptions.None); + T prevResult = PreviousResult; + if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) + prevResult = ((ITask)DependsOn).Result; + else if (getPreviousResult != null) + prevResult = getPreviousResult(); + + Run(previousIsSuccessful, prevResult); + + }, Token, TaskCreationOptions.None); + Name = $"ActionTask<{typeof(T)}>"; } @@ -103,12 +115,23 @@ public ActionTask(CancellationToken token, Action action, Fu { Guard.ArgumentNotNull(action, "action"); this.CallbackWithException = action; - Task = new Task(() => Run(DependsOn?.Successful ?? true, + Task = new Task(() => + { + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task // otherwise if there's a method to retrieve the value, call that // otherwise use the PreviousResult property - (DependsOn?.Successful ?? false) ? ((ITask)DependsOn).Result : getPreviousResult != null ? getPreviousResult() : PreviousResult), - Token, TaskCreationOptions.None); + T prevResult = PreviousResult; + if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) + prevResult = ((ITask)DependsOn).Result; + else if (getPreviousResult != null) + prevResult = getPreviousResult(); + + Run(previousIsSuccessful, prevResult); + + }, Token, TaskCreationOptions.None); Name = $"ActionTask"; } @@ -409,5 +432,4 @@ protected override List RunWithData(bool success, T previousResult) return result; } } - } \ No newline at end of file diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index a5aa3c103..660b8508a 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -113,6 +113,7 @@ public void Run() { Logger.Trace($"Running '{Process.StartInfo.FileName} {taskName}'"); + token.ThrowIfCancellationRequested(); Process.Start(); if (Process.StartInfo.RedirectStandardInput) diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index b243da3df..f77c98666 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -122,6 +122,7 @@ protected TaskBase(CancellationToken token) Token = token; Task = new Task(() => { + Token.ThrowIfCancellationRequested(); var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); Run(previousIsSuccessful); }, @@ -134,6 +135,8 @@ protected TaskBase(Task task) { Task = new Task(t => { + Token.ThrowIfCancellationRequested(); + var scheduler = TaskManager.GetScheduler(Affinity); RaiseOnStart(); var tk = ((Task)t); @@ -142,6 +145,7 @@ protected TaskBase(Task task) if (tk.Status == TaskStatus.Created && !tk.IsCompleted && ((tk.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) { + Token.ThrowIfCancellationRequested(); tk.RunSynchronously(scheduler); } } @@ -150,6 +154,7 @@ protected TaskBase(Task task) Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; + Token.ThrowIfCancellationRequested(); } finally { @@ -278,7 +283,12 @@ public T Finally(T taskToContinueWith) /// internal void SetFaultHandler(TaskBase handler) { - Task.ContinueWith(t => handler.Start(t), Token, + Task.ContinueWith(t => + { + Token.ThrowIfCancellationRequested(); + handler.Start(t); + }, + Token, TaskContinuationOptions.OnlyOnFaulted, TaskManager.GetScheduler(handler.Affinity)); DependsOn?.SetFaultHandler(handler); @@ -356,9 +366,15 @@ protected void SetContinuation() protected void SetContinuation(TaskBase continuation, TaskContinuationOptions runOptions) { - Task.ContinueWith(_ => ((TaskBase)(object)continuation).Run(), Token, - runOptions, - TaskManager.GetScheduler(continuation.Affinity)); + Token.ThrowIfCancellationRequested(); + Task.ContinueWith(_ => + { + Token.ThrowIfCancellationRequested(); + ((TaskBase)(object)continuation).Run(); + }, + Token, + runOptions, + TaskManager.GetScheduler(continuation.Affinity)); } protected ITask SetDependsOn(ITask dependsOn) @@ -406,6 +422,7 @@ public virtual void Run(bool success) taskFailed = false; hasRun = false; exception = null; + Token.ThrowIfCancellationRequested(); } protected virtual void RaiseOnStart() @@ -526,6 +543,7 @@ protected TaskBase(CancellationToken token) { Task = new Task(() => { + Token.ThrowIfCancellationRequested(); var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); var ret = RunWithReturn(previousIsSuccessful); tcs.SetResult(ret); @@ -538,6 +556,8 @@ protected TaskBase(Task task) { Task = new Task(t => { + Token.ThrowIfCancellationRequested(); + TResult ret = default(TResult); RaiseOnStart(); var tk = ((Task)t); @@ -546,6 +566,7 @@ protected TaskBase(Task task) if (tk.Status == TaskStatus.Created && !tk.IsCompleted && ((tk.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) { + Token.ThrowIfCancellationRequested(); tk.RunSynchronously(); } ret = tk.Result; @@ -555,6 +576,7 @@ protected TaskBase(Task task) Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; + Token.ThrowIfCancellationRequested(); } finally { @@ -626,8 +648,7 @@ public ITask Finally(Action handler) public ITask Finally(Func continuation, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(continuation, "continuation"); - var ret = Then(new FuncTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); - return ret; + return Then(new FuncTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); } /// @@ -636,8 +657,7 @@ public ITask Finally(Func continuati public ITask Finally(Action continuation, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(continuation, "continuation"); - var ret = Then(new ActionTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); - return ret; + return Then(new ActionTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); } public new ITask Start() @@ -704,13 +724,13 @@ public TaskBase(CancellationToken token) { Task = new Task(() => { + Token.ThrowIfCancellationRequested(); var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); T prevResult = previousIsSuccessful && DependsOn != null && DependsOn is ITask ? ((ITask)DependsOn).Result : default(T); var ret = RunWithData(previousIsSuccessful, prevResult); tcs.SetResult(ret); return ret; - }, - Token, TaskCreationOptions.None); + }, Token, TaskCreationOptions.None); } public TaskBase(Task task) @@ -764,4 +784,4 @@ public enum TaskAffinity Exclusive, UI } -} \ No newline at end of file +} From aed20198b9f6cdb2727d5b5e982ca40180b50367 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 29 May 2018 16:25:39 -0400 Subject: [PATCH 277/567] Restoring error dialog messages --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 1f7557520..f05d451b5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -126,6 +126,16 @@ private static void ContextMenu_Lock() taskQueue.FinallyInUI((success, exception) => { + if (!success) + { + var error = exception.Message; + if (error.Contains("exit status 255")) + error = "Failed to lock: no permissions"; + EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, + error, + Localization.Ok); + } + isBusy = false; Selection.activeGameObject = null; }).Start(); @@ -184,6 +194,16 @@ private static void ContextMenu_Unlock() taskQueue.FinallyInUI((success, exception) => { + if (!success) + { + var error = exception.Message; + if (error.Contains("exit status 255")) + error = "Failed to unlock: no permissions"; + EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, + error, + Localization.Ok); + } + isBusy = false; Selection.activeGameObject = null; }).Start(); From e69d1488f5f53fed4bac13dbcd4f2f24126aae39 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 29 May 2018 16:29:37 -0400 Subject: [PATCH 278/567] Returning the correct exception --- src/GitHub.Api/Tasks/ActionTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index 94b5c3779..1847e0871 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -46,7 +46,7 @@ private void TaskFinished(ITask task, bool success, Exception ex) } else { - aggregateTask.TrySetException(ex); + aggregateTask.TrySetException(exception); } } } From 05fd289ba8619b8d368cb508d856fdc013838873 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 30 May 2018 09:14:12 -0400 Subject: [PATCH 279/567] Correcting TaskQueue race condition to capture success and exception Co-Authored-By: @Joen-UnLogick --- src/GitHub.Api/Tasks/ActionTask.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index 1847e0871..9f5691229 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -34,10 +34,12 @@ protected override void Run() private void TaskFinished(ITask task, bool success, Exception ex) { - var count = Interlocked.Increment(ref finishedTaskCount); - isSuccessful &= success; if (!success) + { + isSuccessful = false; exception = ex; + } + var count = Interlocked.Increment(ref finishedTaskCount); if (count == queuedTasks.Count) { if (isSuccessful) From 8cd070a414f6b39871bd238f53e683d4aca92c5c Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 31 May 2018 11:15:22 -0400 Subject: [PATCH 280/567] Removing gitter as a chat option --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e83795b86..236bdd910 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ You can reach the team right here by opening a [new issue](https://github.com/gi [![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true)](https://ci.appveyor.com/project/github-windows/unity) -[![Join the chat at https://gitter.im/github-for-unity/Unity](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/github-for-unity/Unity?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://discord.gg/5zH8hVx](https://img.shields.io/badge/discord-join%20chat-7289DA.svg)](https://discord.gg/5zH8hVx) [![GitHub for Unity live coding on Twitch](https://img.shields.io/badge/twitch-live%20coding-6441A4.svg)](https://www.twitch.tv/sh4na) From 9cdc8742dcf87221bd3eb727c734b4906a5b4e40 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 31 May 2018 17:30:29 +0200 Subject: [PATCH 281/567] Fix running TaskQueue and catching errors --- src/GitHub.Api/Git/RepositoryManager.cs | 17 +++++- src/GitHub.Api/Installer/UnzipTask.cs | 1 - src/GitHub.Api/Tasks/ActionTask.cs | 22 ++------ src/GitHub.Api/Tasks/DownloadTask.cs | 1 - src/GitHub.Api/Tasks/TaskBase.cs | 52 +++++++++++++------ .../GitHub.Unity/UI/ProjectWindowInterface.cs | 11 ++-- src/tests/TaskSystemIntegrationTests/Tests.cs | 7 +-- 7 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index 865a66d26..37664e2fd 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -460,7 +460,7 @@ private ITask HookupHandlers(ITask task, bool filesystemChangesExpected) } }; - task.Finally(success => + task.OnEnd += (_, __, ___) => { if (filesystemChangesExpected) { @@ -473,6 +473,21 @@ private ITask HookupHandlers(ITask task, bool filesystemChangesExpected) //Logger.Trace("Ended Operation - Clearing Busy Flag"); IsBusy = false; } + }; + task.Catch(_ => + { + if (filesystemChangesExpected) + { + //Logger.Trace("Ended Operation - Enable Watcher"); + watcher.Start(); + } + + if (isExclusive) + { + //Logger.Trace("Ended Operation - Clearing Busy Flag"); + IsBusy = false; + } + }); return task; } diff --git a/src/GitHub.Api/Installer/UnzipTask.cs b/src/GitHub.Api/Installer/UnzipTask.cs index 49a9d4590..54b897d28 100644 --- a/src/GitHub.Api/Installer/UnzipTask.cs +++ b/src/GitHub.Api/Installer/UnzipTask.cs @@ -38,7 +38,6 @@ public override NPath RunWithReturn(bool success) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index 9f5691229..3b24160cc 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -10,7 +10,7 @@ class TaskQueue : TaskBase private TaskCompletionSource aggregateTask = new TaskCompletionSource(); private readonly List queuedTasks = new List(); private volatile bool isSuccessful = true; - private volatile Exception exception; + private volatile Exception taskException; private int finishedTaskCount; public TaskQueue() : base() @@ -21,6 +21,7 @@ public TaskQueue() : base() public ITask Queue(ITask task) { task.OnEnd += TaskFinished; + task.Catch(e => TaskFinished(task, false, e)); queuedTasks.Add(task); return this; } @@ -37,7 +38,7 @@ private void TaskFinished(ITask task, bool success, Exception ex) if (!success) { isSuccessful = false; - exception = ex; + taskException = ex; } var count = Interlocked.Increment(ref finishedTaskCount); if (count == queuedTasks.Count) @@ -48,7 +49,7 @@ private void TaskFinished(ITask task, bool success, Exception ex) } else { - aggregateTask.TrySetException(exception); + aggregateTask.TrySetException(taskException.GetBaseException()); } } } @@ -105,7 +106,6 @@ public override void Run(bool success) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } @@ -207,7 +207,6 @@ protected virtual void Run(bool success, T previousResult) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } @@ -275,7 +274,6 @@ public override T RunWithReturn(bool success) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } @@ -336,7 +334,6 @@ protected override TResult RunWithData(bool success, T previousResult) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } @@ -402,16 +399,8 @@ public override List RunWithReturn(bool success) result = CallbackWithException(success, thrown); } } - catch (AggregateException ex) - { - var e = ex.GetBaseException(); - Errors = e.Message; - if (!RaiseFaultHandlers(e)) - throw e; - } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } @@ -469,7 +458,6 @@ protected override List RunWithData(bool success, T previousResult) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } @@ -481,4 +469,4 @@ protected override List RunWithData(bool success, T previousResult) return result; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/DownloadTask.cs b/src/GitHub.Api/Tasks/DownloadTask.cs index ca03f2a28..1d7da391bac 100644 --- a/src/GitHub.Api/Tasks/DownloadTask.cs +++ b/src/GitHub.Api/Tasks/DownloadTask.cs @@ -61,7 +61,6 @@ public override NPath RunWithReturn(bool success) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; } diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index ca3dccd05..1cbbbdc13 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -158,10 +158,11 @@ protected void Initialize(Task task) Token.ThrowIfCancellationRequested(); tk.RunSynchronously(scheduler); } + else + tk.Wait(); } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; Token.ThrowIfCancellationRequested(); @@ -245,11 +246,18 @@ public ITask Catch(Action handler) public ITask Catch(Func handler) { Guard.ArgumentNotNull(handler, "handler"); - catchHandler += handler; + CatchInternal(handler); DependsOn?.Catch(handler); return this; } + internal ITask CatchInternal(Func handler) + { + Guard.ArgumentNotNull(handler, "handler"); + catchHandler += handler; + return this; + } + /// /// Run a callback at the end of the task execution, on the same thread as the task that just finished, regardless of execution state /// This will always run on the same thread as the previous task @@ -268,7 +276,14 @@ public ITask Finally(Action handler) public ITask Finally(Action actionToContinueWith, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(actionToContinueWith, nameof(actionToContinueWith)); - return Finally(new ActionTask(Token, actionToContinueWith) { Affinity = affinity, Name = "Finally" }); + return Then(new ActionTask(Token, (s, ex) => + { + actionToContinueWith(s, ex); + if (!s) + throw ex; + }) + { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways) + .CatchInternal(_ => true); } /// @@ -438,13 +453,15 @@ protected virtual void RaiseOnStart() protected virtual bool RaiseFaultHandlers(Exception ex) { + exception = ex is AggregateException ? ex.GetBaseException() : ex; + Errors = exception.Message; taskFailed = true; - exception = ex; if (catchHandler == null) return false; + var args = new object[] { exception }; foreach (var handler in catchHandler.GetInvocationList()) { - if ((bool)handler.DynamicInvoke(new object[] { ex })) + if ((bool)handler.DynamicInvoke(args)) { exceptionWasHandled = true; break; @@ -497,15 +514,14 @@ protected virtual void CallFinallyHandler() protected Exception GetThrownException() { - if (DependsOn == null) - return null; - - if (DependsOn.Task.Status == TaskStatus.Faulted) + var depends = DependsOn; + while (depends != null) { - var ex = DependsOn.Task.Exception; - return ex?.InnerException ?? ex; + if (depends.taskFailed) + return depends.exception; + depends = depends.DependsOn; } - return DependsOn.GetThrownException(); + return null; } public void UpdateProgress(long value, long total, string message = null) @@ -583,7 +599,6 @@ protected void Initialize(Task task) } catch (Exception ex) { - Errors = ex.Message; if (!RaiseFaultHandlers(ex)) throw; Token.ThrowIfCancellationRequested(); @@ -635,7 +650,7 @@ public override T Then(T continuation, TaskRunOptions runOptions = TaskRunOpt public new ITask Catch(Func handler) { Guard.ArgumentNotNull(handler, "handler"); - catchHandler += handler; + CatchInternal(handler); DependsOn?.Catch(handler); return this; } @@ -667,7 +682,14 @@ public ITask Finally(Func continuati public ITask Finally(Action continuation, TaskAffinity affinity = TaskAffinity.Concurrent) { Guard.ArgumentNotNull(continuation, "continuation"); - return Then(new ActionTask(Token, continuation) { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways); + return Then(new ActionTask(Token, (s, ex, res) => + { + continuation(s, ex, res); + if (!s) + throw ex; + }) + { Affinity = affinity, Name = "Finally" }, TaskRunOptions.OnAlways) + .CatchInternal(_ => true); } public new ITask Start() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index f05d451b5..4e7ee4eda 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -146,14 +146,9 @@ private static ITask CreateLockObjectTask(Object selected) NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - return Repository.RequestLock(repositoryPath) - .FinallyInUI((success, ex) => - { - if (success) - { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); - } - }); + var task = Repository.RequestLock(repositoryPath); + task.OnEnd += (_, s, ___) => { if (s) manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); }; + return task; } [MenuItem(AssetsMenuReleaseLock, true, 1000)] diff --git a/src/tests/TaskSystemIntegrationTests/Tests.cs b/src/tests/TaskSystemIntegrationTests/Tests.cs index 41d27c292..bcec2317c 100644 --- a/src/tests/TaskSystemIntegrationTests/Tests.cs +++ b/src/tests/TaskSystemIntegrationTests/Tests.cs @@ -915,7 +915,7 @@ public async Task RunningDifferentTasksDependingOnPreviousResult() var callOrder = new List(); var taskEnd = new ActionTask(Token, () => callOrder.Add("chain completed")) { Name = "Chain Completed" }; - var final = taskEnd.Finally((_, __) => { }, TaskAffinity.Concurrent); + var final = taskEnd.Finally((_, __) => { }, TaskAffinity.Exclusive); var taskStart = new FuncTask(Token, _ => { @@ -942,12 +942,13 @@ public async Task RunningDifferentTasksDependingOnPreviousResult() await final.StartAndSwallowException(); - CollectionAssert.AreEqual(new string[] { + + Assert.AreEqual(new string[] { "chain start", "failing", "on failure", "chain completed" - }, callOrder); + }.Join(","), callOrder.Join(",")); } private T LogAndReturnResult(List callOrder, string msg, T result) From 1f3d2423e2bf86817e56e9a261e8ab79c7492895 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 31 May 2018 18:16:56 +0200 Subject: [PATCH 282/567] Fix MismatchLayout error in locks view --- src/GitHub.Api/Tasks/ProcessTask.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/LocksView.cs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index 660b8508a..b0a4a9127 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -544,4 +544,4 @@ public SimpleListProcessTask(CancellationToken token, NPath fullPathToExecutable public override string ProcessName => fullPathToExecutable; public override string ProcessArguments => arguments; } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 6ba86b54f..5b61a6f80 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -51,6 +51,7 @@ class LocksControl [NonSerialized] private GitLockEntry rightClickNextRenderEntry; [NonSerialized] private int controlId; [NonSerialized] private UnityEngine.Object lastActivatedObject; + [NonSerialized] private Dictionary visibleItems = new Dictionary(); [SerializeField] private Vector2 scroll; [SerializeField] private List gitLockEntries = new List(); @@ -106,15 +107,19 @@ public bool Render(Rect containingRect, Action singleClick = null, var endDisplay = scroll.y + containingRect.height; var rect = new Rect(containingRect.x, containingRect.y, containingRect.width, 0); - for (var index = 0; index < gitLockEntries.Count; index++) { var entry = gitLockEntries[index]; var entryRect = new Rect(rect.x, rect.y, rect.width, Styles.LocksEntryHeight); - var shouldRenderEntry = !(entryRect.y > endDisplay || entryRect.yMax < startDisplay); - if (shouldRenderEntry) + if (Event.current.type == EventType.Layout) + { + var shouldRenderEntry = !(entryRect.y > endDisplay || entryRect.yMax < startDisplay); + visibleItems[entry.GitLock.ID] = shouldRenderEntry; + } + + if (visibleItems[entry.GitLock.ID]) { entryRect = RenderEntry(entryRect, entry); } @@ -225,6 +230,7 @@ public void Load(List locks, List gitStatusEntries) var scrollIndex = (int)(scrollValue / Styles.LocksEntryHeight); assets.Clear(); + visibleItems.Clear(); gitLockEntries = locks.Select(gitLock => { @@ -244,6 +250,7 @@ public void Load(List locks, List gitStatusEntries) assets.Add(assetGuid, gitLockEntry); } + visibleItems.Add(gitLockEntry.GitLock.ID, false); return gitLockEntry; }).ToList(); From 69e13c8b7f66449b264e733ea1586199ce9759f5 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 31 May 2018 18:47:00 +0200 Subject: [PATCH 283/567] Fix the rest of the end handlers, cleanup code, and put the asset menus after a separator --- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 172 ++++++------------ 1 file changed, 58 insertions(+), 114 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 4e7ee4eda..15aa1d3d7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -1,8 +1,10 @@ +using System; using GitHub.Logging; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; +using Object = UnityEngine.Object; namespace GitHub.Unity { @@ -81,138 +83,96 @@ private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) } } - [MenuItem(AssetsMenuRequestLock, true)] + [MenuItem(AssetsMenuRequestLock, true, 10000)] private static bool ContextMenu_CanLock() { if (!EnsureInitialized()) return false; if (isBusy) return false; - return Selection.objects.Any(IsObjectUnlocked); } - private static bool IsObjectUnlocked(Object selected) + [MenuItem(AssetsMenuReleaseLock, true, 10001)] + private static bool ContextMenu_CanUnlock() { - if (selected == null) + if (!EnsureInitialized()) return false; - - NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - - var alreadyLocked = locks.Any(x => repositoryPath == x.Path); - GitFileStatus status = GitFileStatus.None; - if (entries != null) - { - status = entries.FirstOrDefault(x => repositoryPath == x.Path.ToNPath()).Status; - } - - return !alreadyLocked && status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; - } - - [MenuItem(AssetsMenuRequestLock)] - private static void ContextMenu_Lock() - { - isBusy = true; - - var unlockedObjects = Selection.objects.Where(IsObjectUnlocked).ToArray(); - var tasks = unlockedObjects.Select(CreateLockObjectTask).ToArray(); - - var taskQueue = new TaskQueue(); - foreach (var task in tasks) - { - taskQueue.Queue(task); - } - - taskQueue.FinallyInUI((success, exception) => - { - if (!success) - { - var error = exception.Message; - if (error.Contains("exit status 255")) - error = "Failed to lock: no permissions"; - EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, - error, - Localization.Ok); - } - - isBusy = false; - Selection.activeGameObject = null; - }).Start(); - } - - private static ITask CreateLockObjectTask(Object selected) - { - NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - - var task = Repository.RequestLock(repositoryPath); - task.OnEnd += (_, s, ___) => { if (s) manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); }; - return task; + if (isBusy) + return false; + return Selection.objects.Any(IsObjectLocked); } - [MenuItem(AssetsMenuReleaseLock, true, 1000)] - private static bool ContextMenu_CanUnlock() + [MenuItem(AssetsMenuReleaseLockForced, true, 10002)] + private static bool ContextMenu_CanUnlockForce() { if (!EnsureInitialized()) return false; if (isBusy) return false; - return Selection.objects.Any(IsObjectLocked); } - private static bool IsObjectLocked(Object selected) + [MenuItem(AssetsMenuRequestLock, false, 10000)] + private static void ContextMenu_Lock() { - if (selected == null) - return false; - - NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); - NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - - return locks.Any(x => repositoryPath == x.Path); + RunLockUnlock(IsObjectUnlocked, CreateLockObjectTask, Localization.RequestLockActionTitle, "Failed to lock: no permissions"); } - [MenuItem(AssetsMenuReleaseLock, false, 1000)] + [MenuItem(AssetsMenuReleaseLockForced, false, 10001)] private static void ContextMenu_Unlock() { - isBusy = true; + RunLockUnlock(IsObjectLocked, x => CreateUnlockObjectTask(x, false), Localization.ReleaseLockActionTitle, "Failed to unlock: no permissions"); + } - var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); - var tasks = lockedObjects.Select(o => CreateUnlockObjectTask(o, false)).ToArray(); + [MenuItem(AssetsMenuReleaseLockForced, false, 10002)] + private static void ContextMenu_UnlockForce() + { + RunLockUnlock(IsObjectLocked, x => CreateUnlockObjectTask(x, true), Localization.ReleaseLockActionTitle, "Failed to unlock: no permissions"); + } + private static void RunLockUnlock(Func selector, Func creator, string title, string errorMessage) + { + isBusy = true; var taskQueue = new TaskQueue(); - foreach (var task in tasks) + foreach (var lockedObject in Selection.objects.Where(selector)) { - taskQueue.Queue(task); + taskQueue.Queue(creator(lockedObject)); } - taskQueue.FinallyInUI((success, exception) => { if (!success) { var error = exception.Message; if (error.Contains("exit status 255")) - error = "Failed to unlock: no permissions"; - EditorUtility.DisplayDialog(Localization.RequestLockActionTitle, - error, - Localization.Ok); + error = errorMessage; + EditorUtility.DisplayDialog(title, error, Localization.Ok); } - isBusy = false; - Selection.activeGameObject = null; - }).Start(); + }); + taskQueue.Start(); } - [MenuItem(AssetsMenuReleaseLockForced, true, 1000)] - private static bool ContextMenu_CanUnlockForce() + private static bool IsObjectUnlocked(Object selected) { - if (!EnsureInitialized()) - return false; - if (isBusy) + if (selected == null) return false; - var selected = Selection.activeObject; + NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); + + var alreadyLocked = locks.Any(x => repositoryPath == x.Path); + GitFileStatus status = GitFileStatus.None; + if (entries != null) + { + status = entries.FirstOrDefault(x => repositoryPath == x.Path.ToNPath()).Status; + } + + return !alreadyLocked && status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; + } + + private static bool IsObjectLocked(Object selected) + { if (selected == null) return false; @@ -222,40 +182,24 @@ private static bool ContextMenu_CanUnlockForce() return locks.Any(x => repositoryPath == x.Path); } - [MenuItem(AssetsMenuReleaseLockForced, false, 1000)] - private static void ContextMenu_UnlockForce() + private static ITask CreateUnlockObjectTask(Object selected, bool force) { - isBusy = true; - - var lockedObjects = Selection.objects.Where(IsObjectLocked).ToArray(); - var tasks = lockedObjects.Select(o => CreateUnlockObjectTask(o, true)).ToArray(); - - var taskQueue = new TaskQueue(); - foreach (var task in tasks) - { - taskQueue.Queue(task); - } + NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); + NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - taskQueue.FinallyInUI((success, exception) => - { - isBusy = false; - Selection.activeGameObject = null; - }).Start(); + var task = Repository.ReleaseLock(repositoryPath, force); + task.OnEnd += (_, s, __) => { if (s) manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); }; + return task; } - private static ITask CreateUnlockObjectTask(Object selected, bool force) + private static ITask CreateLockObjectTask(Object selected) { NPath assetPath = AssetDatabase.GetAssetPath(selected.GetInstanceID()).ToNPath(); NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); - return Repository.ReleaseLock(repositoryPath, force) - .FinallyInUI((success, ex) => - { - if (success) - { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); - } - }); + var task = Repository.RequestLock(repositoryPath); + task.OnEnd += (_, s, ___) => { if (s) manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); }; + return task; } private static void OnLocksUpdate() From ea5312b6555745657e15977cd1b04222b2fd6f83 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 31 May 2018 18:56:46 +0200 Subject: [PATCH 284/567] Simplifying some code --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 15aa1d3d7..9a3d66574 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -162,13 +162,15 @@ private static bool IsObjectUnlocked(Object selected) NPath repositoryPath = manager.Environment.GetRepositoryPath(assetPath); var alreadyLocked = locks.Any(x => repositoryPath == x.Path); + if (alreadyLocked) + return false; + GitFileStatus status = GitFileStatus.None; if (entries != null) { status = entries.FirstOrDefault(x => repositoryPath == x.Path.ToNPath()).Status; } - - return !alreadyLocked && status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; + return status != GitFileStatus.Untracked && status != GitFileStatus.Ignored; } private static bool IsObjectLocked(Object selected) From 39f334eab7214de45d3e2ea59c6a0e95bc252eae Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 31 May 2018 19:29:04 +0200 Subject: [PATCH 285/567] Fix octorun version check sigh --- src/GitHub.Api/Installer/OctorunInstaller.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 6c332478b..902c3dab5 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -71,7 +71,7 @@ private bool IsOctorunExtracted() return false; } - var octorunVersion = installDetails.VersionFile.ReadAllText(); + var octorunVersion = installDetails.VersionFile.ReadAllText().Trim(); if (!OctorunInstallDetails.PackageVersion.Equals(octorunVersion)) { Logger.Warning("Current version {0} does not match expected {1}", octorunVersion, OctorunInstallDetails.PackageVersion); @@ -112,4 +112,4 @@ public OctorunInstallDetails(NPath baseDataPath) public NPath VersionFile => InstallationPath.Combine("version"); } } -} \ No newline at end of file +} From 5f7f6a1458d95d711d86c7334ae6fd62324279cc Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 31 May 2018 14:57:46 -0400 Subject: [PATCH 286/567] Adding locks to the UsageTracker --- script | 2 +- src/GitHub.Api/Metrics/IMetricsService.cs | 2 +- src/GitHub.Api/Metrics/UsageTracker.cs | 207 ++++++++++++++-------- 3 files changed, 132 insertions(+), 79 deletions(-) diff --git a/script b/script index 4a8d675da..bc1312a7b 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 4a8d675dafaea646a7925763412a831e8ed9abdb +Subproject commit bc1312a7b86de817a172316c3b0d1c8a1a61f17e diff --git a/src/GitHub.Api/Metrics/IMetricsService.cs b/src/GitHub.Api/Metrics/IMetricsService.cs index 78f18bb7d..231418624 100644 --- a/src/GitHub.Api/Metrics/IMetricsService.cs +++ b/src/GitHub.Api/Metrics/IMetricsService.cs @@ -8,6 +8,6 @@ interface IMetricsService /// /// Posts the provided usage model. /// - Task PostUsage(List model); + void PostUsage(List model); } } diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 40c113b30..f875aae40 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -12,6 +12,8 @@ class UsageTracker : IUsageTracker { private static ILogging Logger { get; } = LogHelper.GetLogger(); + private static object _lock = new object(); + private readonly ISettings userSettings; private readonly IUsageLoader usageLoader; private readonly IMetricsService metricsService; @@ -23,7 +25,7 @@ class UsageTracker : IUsageTracker public UsageTracker(IMetricsService metricsService, ISettings userSettings, IEnvironment environment, string userId, string unityVersion, string instanceId) - : this(metricsService, userSettings, + : this(metricsService, userSettings, new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), userId, unityVersion, instanceId) { @@ -43,23 +45,23 @@ public UsageTracker(IMetricsService metricsService, ISettings userSettings, Logger.Trace("userId:{0} instanceId:{1}", userId, instanceId); if (Enabled) - RunTimer(3*60); + RunTimer(3 * 60); } private void RunTimer(int seconds) { - timer = new Timer(async _ => + timer = new Timer(_ => { try { timer.Dispose(); - await SendUsage(); + SendUsage(); } - catch {} + catch { } }, null, seconds * 1000, Timeout.Infinite); } - private async Task SendUsage() + private void SendUsage() { var usageStore = usageLoader.Load(userId); @@ -69,13 +71,16 @@ private async Task SendUsage() return; } - if (usageStore.LastUpdated.Date != DateTimeOffset.UtcNow.Date) + var currentTimeOffset = DateTimeOffset.UtcNow; + if (usageStore.LastUpdated.Date == currentTimeOffset) { - var currentTimeOffset = DateTimeOffset.UtcNow; - var beforeDate = currentTimeOffset.Date; + return; + } + lock (_lock) + { var success = false; - var extractReports = usageStore.Model.SelectReports(beforeDate); + var extractReports = usageStore.Model.SelectReports(currentTimeOffset.Date); if (!extractReports.Any()) { Logger.Trace("No items to send"); @@ -90,7 +95,7 @@ private async Task SendUsage() try { - await metricsService.PostUsage(extractReports); + metricsService.PostUsage(extractReports); success = true; } catch (Exception ex) @@ -101,7 +106,7 @@ private async Task SendUsage() if (success) { - usageStore.Model.RemoveReports(beforeDate); + usageStore.Model.RemoveReports(currentTimeOffset.Date); usageStore.LastUpdated = currentTimeOffset; usageLoader.Save(usageStore); } @@ -110,130 +115,178 @@ private async Task SendUsage() public void IncrementNumberOfStartups() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .NumberOfStartups++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .NumberOfStartups++; + usageLoader.Save(usage); + } } public void IncrementProjectsInitialized() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .ProjectsInitialized++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .ProjectsInitialized++; + usageLoader.Save(usage); + } } public void IncrementChangesViewButtonCommit() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .ChangesViewButtonCommit++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .ChangesViewButtonCommit++; + usageLoader.Save(usage); + } } public void IncrementHistoryViewToolbarFetch() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .HistoryViewToolbarFetch++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .HistoryViewToolbarFetch++; + usageLoader.Save(usage); + } } public void IncrementHistoryViewToolbarPush() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .HistoryViewToolbarPush++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .HistoryViewToolbarPush++; + usageLoader.Save(usage); + } } public void IncrementHistoryViewToolbarPull() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .HistoryViewToolbarPull++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .HistoryViewToolbarPull++; + usageLoader.Save(usage); + } } public void IncrementBranchesViewButtonCreateBranch() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .BranchesViewButtonCreateBranch++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonCreateBranch++; + usageLoader.Save(usage); + } } public void IncrementBranchesViewButtonDeleteBranch() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .BranchesViewButtonDeleteBranch++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonDeleteBranch++; + usageLoader.Save(usage); + } } public void IncrementBranchesViewButtonCheckoutLocalBranch() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .BranchesViewButtonCheckoutLocalBranch++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonCheckoutLocalBranch++; + usageLoader.Save(usage); + } } public void IncrementBranchesViewButtonCheckoutRemoteBranch() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .BranchesViewButtonCheckoutRemoteBranch++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .BranchesViewButtonCheckoutRemoteBranch++; + usageLoader.Save(usage); + } } public void IncrementSettingsViewButtonLfsUnlock() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .SettingsViewButtonLfsUnlock++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .SettingsViewButtonLfsUnlock++; + usageLoader.Save(usage); + } } public void IncrementAuthenticationViewButtonAuthentication() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .AuthenticationViewButtonAuthentication++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .AuthenticationViewButtonAuthentication++; + usageLoader.Save(usage); + } } public void IncrementUnityProjectViewContextLfsLock() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .UnityProjectViewContextLfsLock++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .UnityProjectViewContextLfsLock++; + usageLoader.Save(usage); + } } public void IncrementUnityProjectViewContextLfsUnlock() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .UnityProjectViewContextLfsUnlock++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .UnityProjectViewContextLfsUnlock++; + usageLoader.Save(usage); + } } public void IncrementPublishViewButtonPublish() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .PublishViewButtonPublish++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .PublishViewButtonPublish++; + usageLoader.Save(usage); + } } public void IncrementApplicationMenuMenuItemCommandLine() { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) - .ApplicationMenuMenuItemCommandLine++; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId) + .ApplicationMenuMenuItemCommandLine++; + usageLoader.Save(usage); + } } public bool Enabled @@ -293,7 +346,7 @@ public UsageStore Load(string userId) { path.DeleteIfExists(); } - catch {} + catch { } } } From ff3fa10fe4c42f3d8d59b0307f49a93fc15bb1e0 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 31 May 2018 16:18:04 -0400 Subject: [PATCH 287/567] Fixing up the ConsoleLogAdapter --- src/GitHub.Logging/ConsoleLogAdapter.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/GitHub.Logging/ConsoleLogAdapter.cs b/src/GitHub.Logging/ConsoleLogAdapter.cs index bf8a5951c..11df4e981 100644 --- a/src/GitHub.Logging/ConsoleLogAdapter.cs +++ b/src/GitHub.Logging/ConsoleLogAdapter.cs @@ -5,41 +5,40 @@ namespace GitHub.Logging { public class ConsoleLogAdapter : LogAdapterBase { - private string GetMessage(string level, string context, string message) + private string GetMessage(string context, string message) { var time = DateTime.Now.ToString("HH:mm:ss.fff tt"); - var threadId = Thread.CurrentThread.ManagedThreadId; - return string.Format("{0} {1} [{2,2}] {3} {4}", time, level, threadId, context, message); + return $"{time} {context} {message}"; } public override void Info(string context, string message) { - Console.WriteLine(message); + WriteLine(context, message); } public override void Debug(string context, string message) { - WriteLine("DEBUG", context, message); + WriteLine(context, message); } public override void Trace(string context, string message) { - WriteLine("TRACE", context, message); + WriteLine(context, message); } public override void Warning(string context, string message) { - WriteLine("WARN", context, message); + WriteLine(context, message); } public override void Error(string context, string message) { - Console.Error.WriteLine(message); + WriteLine(context, message); } - private void WriteLine(string level, string context, string message) + private void WriteLine(string context, string message) { - Console.WriteLine(GetMessage(level, context, message)); + Console.WriteLine(GetMessage(context, message)); } } } From 9e7e08f6db0f43816208a15fe29f9e0393a455e1 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 31 May 2018 16:18:48 -0400 Subject: [PATCH 288/567] Adding functionality to the TestWebServer to handle usage reporting --- src/tests/TestWebServer/HttpServer.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/tests/TestWebServer/HttpServer.cs b/src/tests/TestWebServer/HttpServer.cs index 8181c5d11..554056b12 100644 --- a/src/tests/TestWebServer/HttpServer.cs +++ b/src/tests/TestWebServer/HttpServer.cs @@ -100,10 +100,21 @@ public void Abort() private void Process(HttpListenerContext context) { - Logger.Info($"Handling request"); + Logger.Info("Handling request {0}", context.Request.Url.AbsolutePath); + + if (context.Request.Url.AbsolutePath == "/api/usage/unity") + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + + var streamWriter = new StreamWriter(context.Response.OutputStream); + streamWriter.Write("Cool Unity usage bro!"); + streamWriter.Flush(); + + context.Response.Close(); + return; + } var filename = context.Request.Url.AbsolutePath; - Logger.Info($"{filename}"); filename = filename.TrimStart('/'); filename = filename.Replace('/', Path.DirectorySeparatorChar); filename = Path.Combine(rootDirectory, filename); @@ -111,6 +122,7 @@ private void Process(HttpListenerContext context) if (!File.Exists(filename)) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; + Logger.Info($"Path not found - Returning 404"); context.Response.Close(); return; } From df0703c467ef1f1e4c8088a8181f8e1a8d72876e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 31 May 2018 16:28:48 -0400 Subject: [PATCH 289/567] Update submodule --- script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script b/script index bc1312a7b..bab459e1e 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit bc1312a7b86de817a172316c3b0d1c8a1a61f17e +Subproject commit bab459e1eefbb6b9ec954c84b281099da1200e0d From 72721528f28a063929b75c8357d39d68946ab912 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 01:14:33 +0200 Subject: [PATCH 290/567] Don't trample an existing SynchronizationContext --- .../Application/ApplicationManagerBase.cs | 4 +- src/GitHub.Api/Threading/ThreadingHelper.cs | 109 ++---------------- 2 files changed, 12 insertions(+), 101 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index b9f16fc71..ba6a63bbe 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -28,10 +28,10 @@ public event Action OnProgress public ApplicationManagerBase(SynchronizationContext synchronizationContext, IEnvironment environment) { + UIScheduler = ThreadingHelper.GetUIScheduler(synchronizationContext); + SynchronizationContext = synchronizationContext; - SynchronizationContext.SetSynchronizationContext(SynchronizationContext); ThreadingHelper.SetUIThread(); - UIScheduler = TaskScheduler.FromCurrentSynchronizationContext(); ThreadingHelper.MainThreadScheduler = UIScheduler; Environment = environment; diff --git a/src/GitHub.Api/Threading/ThreadingHelper.cs b/src/GitHub.Api/Threading/ThreadingHelper.cs index 13dfd9aa4..295e636f3 100644 --- a/src/GitHub.Api/Threading/ThreadingHelper.cs +++ b/src/GitHub.Api/Threading/ThreadingHelper.cs @@ -1,6 +1,4 @@ -using System; -using System.Runtime.CompilerServices; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace GitHub.Unity @@ -19,102 +17,15 @@ public static void SetUIThread() public static bool InUIThread => InMainThread || Guard.InUnitTestRunner; - /// - /// Switch to the UI thread - /// Auto-disables switching when running in unit test mode - /// - /// - public static IAwaitable SwitchToMainThreadAsync() + public static TaskScheduler GetUIScheduler(SynchronizationContext synchronizationContext) { - return Guard.InUnitTestRunner ? - new AwaitableWrapper() : - new AwaitableWrapper(MainThreadScheduler); + // quickly swap out the sync context so we can leverage FromCurrentSynchronizationContext for our ui scheduler + var currentSyncContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(synchronizationContext); + var ret = TaskScheduler.FromCurrentSynchronizationContext(); + if (currentSyncContext != null) + SynchronizationContext.SetSynchronizationContext(currentSyncContext); + return ret; } - - - /// - /// Switch to a thread pool background thread if the current thread isn't one, otherwise does nothing - /// Auto-disables switching when running in unit test mode - /// - /// - /// - public static IAwaitable SwitchToThreadAsync(TaskScheduler scheduler = null) - { - return Guard.InUnitTestRunner ? - new AwaitableWrapper() : - new AwaitableWrapper(scheduler ?? TaskManager.Instance.ConcurrentScheduler); - } - - class AwaitableWrapper : IAwaitable - { - Func getAwaiter; - - public AwaitableWrapper() - { - getAwaiter = () => new AwaiterWrapper(); - } - - public AwaitableWrapper(TaskScheduler scheduler) - { - getAwaiter = () => new AwaiterWrapper(new TaskSchedulerAwaiter(scheduler)); - } - - public IAwaiter GetAwaiter() => getAwaiter(); - } - - class AwaiterWrapper : IAwaiter - { - Func isCompleted; - Action onCompleted; - Action getResult; - - public AwaiterWrapper() - { - isCompleted = () => true; - onCompleted = c => c(); - getResult = () => { }; - } - - public AwaiterWrapper(TaskSchedulerAwaiter awaiter) - { - isCompleted = () => awaiter.IsCompleted; - onCompleted = c => awaiter.OnCompleted(c); - getResult = () => awaiter.GetResult(); - } - - public bool IsCompleted => isCompleted(); - - public void OnCompleted(Action continuation) => onCompleted(continuation); - - public void GetResult() => getResult(); - } - - public struct TaskSchedulerAwaiter : INotifyCompletion - { - private readonly TaskScheduler scheduler; - - public bool IsCompleted - { - get - { - return (this.scheduler == TaskManager.Instance.UIScheduler && InUIThread) || (this.scheduler != TaskManager.Instance.UIScheduler && !InUIThread); - } - } - - public TaskSchedulerAwaiter(TaskScheduler scheduler) - { - this.scheduler = scheduler; - } - - public void OnCompleted(Action action) - { - Task.Factory.StartNew(action, TaskManager.Instance.Token, TaskCreationOptions.None, this.scheduler); - } - - public void GetResult() - { - } - } - } -} \ No newline at end of file +} From 704ac8e361cc1558550e989b6f98c3d1fd89c0d4 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 12:49:29 +0200 Subject: [PATCH 291/567] Thread information is important in the logs --- src/GitHub.Logging/ConsoleLogAdapter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Logging/ConsoleLogAdapter.cs b/src/GitHub.Logging/ConsoleLogAdapter.cs index 11df4e981..a03990d13 100644 --- a/src/GitHub.Logging/ConsoleLogAdapter.cs +++ b/src/GitHub.Logging/ConsoleLogAdapter.cs @@ -8,7 +8,8 @@ public class ConsoleLogAdapter : LogAdapterBase private string GetMessage(string context, string message) { var time = DateTime.Now.ToString("HH:mm:ss.fff tt"); - return $"{time} {context} {message}"; + var threadId = Thread.CurrentThread.ManagedThreadId; + return string.Format("{0} [{1,2}] {2} {3}", time, threadId, context, message); } public override void Info(string context, string message) From 12d701cf12408614d66fb484113c02cf4673555c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 14:17:36 +0200 Subject: [PATCH 292/567] Fix issues, add tests --- src/GitHub.Api/Metrics/UsageModel.cs | 6 +- src/GitHub.Api/Metrics/UsageTracker.cs | 59 +++++++++-------- src/tests/CommandLine/Program.cs | 64 +++++++++++++++++-- .../IntegrationTests/BaseIntegrationTest.cs | 1 + .../IntegrationTests/IntegrationTests.csproj | 4 ++ .../IntegrationTests/Metrics/MetricsTests.cs | 39 +++++++++++ src/tests/TaskSystemIntegrationTests/Tests.cs | 12 ++-- src/tests/TestWebServer/HttpServer.cs | 18 ++++-- src/tests/TestWebServer/TestWebServer.csproj | 4 ++ 9 files changed, 159 insertions(+), 48 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 674b034b7..35f04ab48 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -85,18 +85,18 @@ public Usage GetCurrentUsage(string appVersion, string unityVersion, string inst public List SelectReports(DateTime beforeDate) { - return Reports.Where(usage => usage.Dimensions.Date.Date != beforeDate.Date).ToList(); + return Reports.Where(usage => usage.Dimensions.Date.Date < beforeDate.Date).ToList(); } public void RemoveReports(DateTime beforeDate) { - Reports.RemoveAll(usage => usage.Dimensions.Date.Date != beforeDate.Date); + Reports.RemoveAll(usage => usage.Dimensions.Date.Date < beforeDate.Date); } } class UsageStore { - public DateTimeOffset LastUpdated { get; set; } = DateTimeOffset.Now; + public DateTimeOffset LastSubmissionDate { get; set; } = DateTimeOffset.Now; public UsageModel Model { get; set; } = new UsageModel(); public Measures GetCurrentMeasures(string appVersion, string unityVersion, string instanceId) diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 983a32684..c5200ec4b 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -1,9 +1,7 @@ using System; using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Threading; -using Timer = System.Threading.Timer; using GitHub.Logging; namespace GitHub.Unity @@ -60,7 +58,7 @@ public UsageTracker(ISettings userSettings, private void RunTimer(int seconds) { - timer = new Timer(_ => + timer = new System.Threading.Timer(_ => { try { @@ -79,45 +77,50 @@ private void SendUsage() return; } - var usageStore = usageLoader.Load(userId); + UsageStore usageStore = null; + lock (_lock) + { + usageStore = usageLoader.Load(userId); + } var currentTimeOffset = DateTimeOffset.UtcNow; - if (usageStore.LastUpdated.Date == currentTimeOffset) + if (usageStore.LastSubmissionDate.Date == currentTimeOffset.Date) { return; } - lock (_lock) + var success = false; + var extractReports = usageStore.Model.SelectReports(currentTimeOffset.Date); + if (!extractReports.Any()) + { + Logger.Trace("No items to send"); + } + else { - var success = false; - var extractReports = usageStore.Model.SelectReports(currentTimeOffset.Date); - if (!extractReports.Any()) + if (!Enabled) { - Logger.Trace("No items to send"); + Logger.Trace("Metrics disabled"); + return; } - else - { - if (!Enabled) - { - Logger.Trace("Metrics disabled"); - return; - } - try - { - MetricsService.PostUsage(extractReports); - success = true; - } - catch (Exception ex) - { - Logger.Warning(@"Error Sending Usage Exception Type:""{0}"" Message:""{1}""", ex.GetType().ToString(), ex.GetExceptionMessageShort()); - } + try + { + MetricsService.PostUsage(extractReports); + success = true; } + catch (Exception ex) + { + Logger.Warning(@"Error Sending Usage Exception Type:""{0}"" Message:""{1}""", ex.GetType().ToString(), ex.GetExceptionMessageShort()); + } + } - if (success) + if (success) + { + lock(_lock) { + usageStore = usageLoader.Load(userId); + usageStore.LastSubmissionDate = currentTimeOffset; usageStore.Model.RemoveReports(currentTimeOffset.Date); - usageStore.LastUpdated = currentTimeOffset; usageLoader.Save(usageStore); } } diff --git a/src/tests/CommandLine/Program.cs b/src/tests/CommandLine/Program.cs index c198254b8..a5e930ea7 100644 --- a/src/tests/CommandLine/Program.cs +++ b/src/tests/CommandLine/Program.cs @@ -6,6 +6,8 @@ using GitHub.Unity; using GitHub; using GitHub.Logging; +using System.Net; +using System.Text; namespace TestApp { @@ -39,6 +41,18 @@ static void RunWebServer(NPath path, int port) server.Stop(); } + static TestWebServer.HttpServer RunWebServer(int port) + { + var path = typeof(Program).Assembly.Location.ToNPath().Parent.Combine("files"); + var server = new TestWebServer.HttpServer(path, port); + var thread = new Thread(() => + { + server.Start(); + }); + thread.Start(); + return server; + } + static int Main(string[] args) { LogHelper.LogAdapter = new ConsoleLogAdapter(); @@ -61,7 +75,15 @@ static int Main(string[] args) string url = null; string readVersion = null; string msg = null; + string host = null; + bool runUsage = false; + var arguments = new List(args); + if (arguments.Contains("usage")) + { + runUsage = true; + arguments.RemoveRange(0, 2); + } p = p .Add("r=", (int v) => retCode = v) @@ -69,22 +91,54 @@ static int Main(string[] args) .Add("e=|error=", v => error = v) .Add("f=|file=", v => data = File.ReadAllText(v)) .Add("ef=|errorFile=", v => error = File.ReadAllText(v)) - .Add("s=|sleep=", (int v) => sleepms = v) + .Add("sleep=", (int v) => sleepms = v) .Add("i|input", v => readInputToEof = true) .Add("w|web", v => runWebServer = true) - .Add("port=", (int v) => webServerPort = v) + .Add("p|port=", "Port", (int v) => webServerPort = v) .Add("g|generateVersion", v => generateVersion = true) .Add("v=|version=", v => version = v) - .Add("p|gen-package", "Pass --version --url --path --md5 --rn --msg to generate a package", v => generatePackage = true) + .Add("gen-package", "Pass --version --url --path --md5 --rn --msg to generate a package", v => generatePackage = true) .Add("u=|url=", v => url = v) .Add("path=", v => path = v.ToNPath()) .Add("rn=", "Path to file with release notes", v => releaseNotes = v.ReadAllTextIfFileExists()) .Add("msg=", "Path to file with message for package", v => msg = v.ReadAllTextIfFileExists()) .Add("readVersion=", v => readVersion = v) .Add("o=|outfile=", v => outfile = v.ToNPath().MakeAbsolute()) - .Add("h|help", v => p.WriteOptionDescriptions(Console.Out)); + .Add("h=", "Host", v => host = v) + .Add("help", v => p.WriteOptionDescriptions(Console.Out)); - p.Parse(args); + var extra = p.Parse(arguments); + if (runUsage) + { + extra.Remove("usage"); + p.Parse(extra); + + path = extra[extra.Count - 1].ToNPath(); + var server = RunWebServer(webServerPort); + var webRequest = (HttpWebRequest)WebRequest.Create(new UriString("http://localhost:" + webServerPort + "/api/usage/unity")); + webRequest.Method = "POST"; + using (var sw = new StreamWriter(webRequest.GetRequestStream())) + { + foreach (var line in path.ReadAllLines()) + { + sw.WriteLine(line); + } + } + using (var webResponse = (HttpWebResponse)webRequest.GetResponseWithoutException()) + { + MemoryStream ms = new MemoryStream(); + var responseLength = webResponse.ContentLength; + using (var sr = new StreamWriter(ms)) + using (var responseStream = webResponse.GetResponseStream()) + { + Utils.Copy(responseStream, ms, responseLength); + } + Console.WriteLine(Encoding.ASCII.GetString(ms.ToArray())); + } + + server.Stop(); + return 0; + } if (generatePackage) { diff --git a/src/tests/IntegrationTests/BaseIntegrationTest.cs b/src/tests/IntegrationTests/BaseIntegrationTest.cs index 6436d4950..d100b9e4e 100644 --- a/src/tests/IntegrationTests/BaseIntegrationTest.cs +++ b/src/tests/IntegrationTests/BaseIntegrationTest.cs @@ -15,6 +15,7 @@ namespace IntegrationTests [Isolated] class BaseIntegrationTest { + protected NPath TestApp => System.Reflection.Assembly.GetExecutingAssembly().Location.ToNPath().Parent.Combine("CommandLine.exe"); public IRepositoryManager RepositoryManager { get; set; } protected IApplicationManager ApplicationManager { get; set; } protected ILogging Logger { get; set; } diff --git a/src/tests/IntegrationTests/IntegrationTests.csproj b/src/tests/IntegrationTests/IntegrationTests.csproj index 217732259..351a6d571 100644 --- a/src/tests/IntegrationTests/IntegrationTests.csproj +++ b/src/tests/IntegrationTests/IntegrationTests.csproj @@ -95,6 +95,10 @@ {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging + + {08b87d2a-8cf1-4211-b7aa-5209f00f72f8} + CommandLine + {66a1d219-f61d-4ae4-9bd7-aaeb97276fff} TestUtils diff --git a/src/tests/IntegrationTests/Metrics/MetricsTests.cs b/src/tests/IntegrationTests/Metrics/MetricsTests.cs index 6ae28aba2..4f20328fb 100644 --- a/src/tests/IntegrationTests/Metrics/MetricsTests.cs +++ b/src/tests/IntegrationTests/Metrics/MetricsTests.cs @@ -78,5 +78,44 @@ public void LoadingWorks() var savedStore = json.FromJson(lowerCase: true); Assert.AreEqual(2, savedStore.GetCurrentMeasures(appVersion, unityVersion, instanceId).NumberOfStartups); } + + [Test] + public void SubmissionWorks() + { + InitializeEnvironment(TestBasePath, false, false); + InitializePlatform(TestBasePath, false, "SubmissionWorks"); + var userId = Guid.NewGuid().ToString(); + var appVersion = ApplicationInfo.Version; + var unityVersion = "2017.3f1"; + var instanceId = Guid.NewGuid().ToString(); + var usageStore = new UsageStore(); + usageStore.Model.Guid = userId; + var storePath = Environment.UserCachePath.Combine(Constants.UsageFile); + var usageLoader = new UsageLoader(storePath); + + var settings = Substitute.For(); + settings.Exists(Arg.Is(Constants.GuidKey)).Returns(true); + settings.Get(Arg.Is(Constants.GuidKey)).Returns(userId); + var usageTracker = new UsageTracker(settings, usageLoader, unityVersion, instanceId); + + usageTracker.IncrementNumberOfStartups(); + usageTracker.IncrementNumberOfStartups(); + + var json = storePath.ReadAllText(Encoding.UTF8); + var savedStore = json.FromJson(lowerCase: true); + var current = savedStore.Model.GetCurrentUsage(appVersion, unityVersion, instanceId); + var yesterday = DateTimeOffset.UtcNow.AddDays(-1); + current.Dimensions.Date = yesterday; + savedStore.LastSubmissionDate = yesterday; + storePath.WriteAllText(savedStore.ToJson(lowerCase: true)); + settings.Get(Arg.Is(Constants.MetricsKey), Arg.Any()).Returns(true); + + var metricsService = new MetricsService(ProcessManager, TaskManager, Environment.FileSystem, TestApp, TestApp); + usageTracker.MetricsService = metricsService; + var method = usageTracker.GetType().GetMethod("SendUsage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + method.Invoke(usageTracker, null); + json = storePath.ReadAllText(Encoding.UTF8); + savedStore = json.FromJson(lowerCase: true); + } } } diff --git a/src/tests/TaskSystemIntegrationTests/Tests.cs b/src/tests/TaskSystemIntegrationTests/Tests.cs index 41d27c292..23377c5a5 100644 --- a/src/tests/TaskSystemIntegrationTests/Tests.cs +++ b/src/tests/TaskSystemIntegrationTests/Tests.cs @@ -30,7 +30,7 @@ public BaseTest() protected IProcessManager ProcessManager { get; set; } protected NPath TestBasePath { get; private set; } protected CancellationToken Token => TaskManager.Token; - protected NPath TestApp => System.Reflection.Assembly.GetExecutingAssembly().Location.ToNPath().Combine("CommandLine.exe"); + protected NPath TestApp => System.Reflection.Assembly.GetExecutingAssembly().Location.ToNPath().Parent.Combine("CommandLine.exe"); [TestFixtureSetUp] public void OneTimeSetup() @@ -115,7 +115,7 @@ public async Task ProcessReadsFromStandardInput() var expectedOutput = "Hello"; - var procTask = new FirstNonNullLineProcessTask(Token, TestApp, @"-s 100 -i") + var procTask = new FirstNonNullLineProcessTask(Token, TestApp, @"--sleep 100 -i") .Configure(ProcessManager, true); procTask.OnStartProcess += proc => @@ -142,13 +142,13 @@ public async Task ProcessOnStartOnEndTaskOrder() string process1Value = null; string process2Value = null; - var process1Task = new FirstNonNullLineProcessTask(Token, TestApp, @"-s 100 -d process1") + var process1Task = new FirstNonNullLineProcessTask(Token, TestApp, @"--sleep 100 -d process1") .Configure(ProcessManager, true).Then((b, s) => { process1Value = s; values.Add(s); }); - var process2Task = new FirstNonNullLineProcessTask(Token, TestApp, @"-s 100 -d process2") + var process2Task = new FirstNonNullLineProcessTask(Token, TestApp, @"---sleep 100 -d process2") .Configure(ProcessManager, true).Then((b, s) => { process2Value = s; values.Add(s); @@ -181,7 +181,7 @@ public async Task ProcessReturningErrorThrowsException() var output = new List(); var expectedOutput = new List { "one name" }; - var task = new FirstNonNullLineProcessTask(Token, TestApp, @"-s 100 -d ""one name""").Configure(ProcessManager) + var task = new FirstNonNullLineProcessTask(Token, TestApp, @"--sleep 100 -d ""one name""").Configure(ProcessManager) .Catch(ex => thrown = ex) .Then((s, d) => output.Add(d)) .Then(new FirstNonNullLineProcessTask(Token, TestApp, @"-e kaboom -r -1").Configure(ProcessManager)) @@ -207,7 +207,7 @@ public async Task NestedProcessShouldChainCorrectly() await new ActionTask(Token, _ => { results.Add("BeforeProcess"); }) - .Then(new FirstNonNullLineProcessTask(Token, TestApp, @"-s 1000 -d ""ok""") + .Then(new FirstNonNullLineProcessTask(Token, TestApp, @"--sleep 1000 -d ""ok""") .Configure(ProcessManager) .Then(new FuncTask(Token, (b, i) => { results.Add("ProcessOutput"); diff --git a/src/tests/TestWebServer/HttpServer.cs b/src/tests/TestWebServer/HttpServer.cs index 554056b12..8f4765abb 100644 --- a/src/tests/TestWebServer/HttpServer.cs +++ b/src/tests/TestWebServer/HttpServer.cs @@ -1,10 +1,12 @@ using GitHub.Logging; +using GitHub.Unity; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; namespace TestWebServer @@ -19,7 +21,8 @@ public class HttpServer { ".png", "image/png" }, { ".txt", "text/plain" }, { ".md5", "text/plain" }, - { ".zip", "application/zip" } + { ".zip", "application/zip" }, + { ".json", "application/json" }, }; private readonly HttpListener listener; private readonly string rootDirectory; @@ -81,7 +84,6 @@ public void Start() } catch (Exception ex) { - Logger.Error(ex); break; } } @@ -104,12 +106,16 @@ private void Process(HttpListenerContext context) if (context.Request.Url.AbsolutePath == "/api/usage/unity") { + var json = new { result = "Cool unity usage" }.ToJson(); context.Response.StatusCode = (int)HttpStatusCode.OK; - - var streamWriter = new StreamWriter(context.Response.OutputStream); - streamWriter.Write("Cool Unity usage bro!"); - streamWriter.Flush(); + context.Response.ContentLength64 = json.Length; + string mime; + context.Response.ContentType = mimeTypeMappings.TryGetValue(".json", out mime) + ? mime + : "application/octet-stream"; + Utils.Copy(new MemoryStream(Encoding.UTF8.GetBytes(json)), context.Response.OutputStream, json.Length); + context.Response.OutputStream.Flush(); context.Response.Close(); return; } diff --git a/src/tests/TestWebServer/TestWebServer.csproj b/src/tests/TestWebServer/TestWebServer.csproj index 81119e24f..c8d6cffad 100644 --- a/src/tests/TestWebServer/TestWebServer.csproj +++ b/src/tests/TestWebServer/TestWebServer.csproj @@ -83,6 +83,10 @@ + + {B389ADAF-62CC-486E-85B4-2D8B078DF763} + GitHub.Api + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging From 28d854fc9128ba4a409dd2e83daf8b5926a320a4 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 14:22:56 +0200 Subject: [PATCH 293/567] Simplify code and trace messages a tad --- src/GitHub.Api/Metrics/UsageTracker.cs | 49 ++++++++++++-------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index c5200ec4b..7d637a0fd 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -73,7 +73,13 @@ private void SendUsage() { if (MetricsService == null) { - Logger.Warning("No service, not sending usage"); + Logger.Warning("Metrics disabled: no service"); + return; + } + + if (!Enabled) + { + Logger.Trace("Metrics disabled"); return; } @@ -86,43 +92,34 @@ private void SendUsage() var currentTimeOffset = DateTimeOffset.UtcNow; if (usageStore.LastSubmissionDate.Date == currentTimeOffset.Date) { + Logger.Trace("Already sent today"); return; } - var success = false; var extractReports = usageStore.Model.SelectReports(currentTimeOffset.Date); if (!extractReports.Any()) { Logger.Trace("No items to send"); + return; } - else - { - if (!Enabled) - { - Logger.Trace("Metrics disabled"); - return; - } - try - { - MetricsService.PostUsage(extractReports); - success = true; - } - catch (Exception ex) - { - Logger.Warning(@"Error Sending Usage Exception Type:""{0}"" Message:""{1}""", ex.GetType().ToString(), ex.GetExceptionMessageShort()); - } + try + { + MetricsService.PostUsage(extractReports); + } + catch (Exception ex) + { + Logger.Warning(@"Error sending usage:""{0}"" Message:""{1}""", ex.GetType(), ex.GetExceptionMessageShort()); + return; } - if (success) + // if we're here, success! + lock(_lock) { - lock(_lock) - { - usageStore = usageLoader.Load(userId); - usageStore.LastSubmissionDate = currentTimeOffset; - usageStore.Model.RemoveReports(currentTimeOffset.Date); - usageLoader.Save(usageStore); - } + usageStore = usageLoader.Load(userId); + usageStore.LastSubmissionDate = currentTimeOffset; + usageStore.Model.RemoveReports(currentTimeOffset.Date); + usageLoader.Save(usageStore); } } From 3e4a2b003428ac7c1fe1d204b05ffeb65f53421f Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 14:55:53 +0200 Subject: [PATCH 294/567] Make the repo dir -> project dir mapping in IO operations more transparent Every time we do an IO operation, we need to convert any relative paths we have to be based off of the real working directory. That's easier to do with an NPath object than with a string. This introduces a wrapper object that does the conversion and passes the call onto IFileSystem, so that NPath itself doesn't know about it, so it's easier to audit all calls and not miss any conversions. Context: NPath/FileSystem are working under the assumption that the working directory of the process is the repo root. This is a lie - Unity sets the working dir to the project path when it loads a project. We lie so it's easier to spawn our processes from the root of the git repository and to show all changes, even though they're outside the Unity project path. --- src/GitHub.Api/IO/FileSystem.cs | 15 +- src/GitHub.Api/IO/NiceIO.cs | 169 +++++++++++++++--- src/tests/IntegrationTests/CachingClasses.cs | 6 + .../UnitTests/IO/GitEnvironmentTestsBase.cs | 30 +--- 4 files changed, 153 insertions(+), 67 deletions(-) diff --git a/src/GitHub.Api/IO/FileSystem.cs b/src/GitHub.Api/IO/FileSystem.cs index 814c916d6..e217f9eff 100644 --- a/src/GitHub.Api/IO/FileSystem.cs +++ b/src/GitHub.Api/IO/FileSystem.cs @@ -23,13 +23,11 @@ public interface IFileSystem IEnumerable GetDirectories(string path); IEnumerable GetDirectories(string path, string pattern); IEnumerable GetDirectories(string path, string pattern, SearchOption searchOption); - string GetDirectoryName(string path); string GetFileNameWithoutExtension(string fileName); IEnumerable GetFiles(string path); IEnumerable GetFiles(string path, string pattern); IEnumerable GetFiles(string path, string pattern, SearchOption searchOption); string GetFullPath(string path); - string GetParentDirectory(string path); string GetRandomFileName(); string GetTempPath(); Stream OpenRead(string path); @@ -53,7 +51,6 @@ public interface IFileSystem public class FileSystem : IFileSystem { private string currentDirectory; - private string processDirectory; public FileSystem() { } @@ -64,7 +61,7 @@ public FileSystem() /// Current directory public FileSystem(string directory) { - processDirectory = currentDirectory = directory; + currentDirectory = directory; } public void SetCurrentDirectory(string directory) @@ -104,11 +101,6 @@ public string GetFullPath(string path) return Path.GetFullPath(path); } - public string GetDirectoryName(string path) - { - return Path.GetDirectoryName(path); - } - public bool DirectoryExists(string path) { return Directory.Exists(path); @@ -120,11 +112,6 @@ public bool ExistingPathIsDirectory(string path) return (attr & FileAttributes.Directory) == FileAttributes.Directory; } - public string GetParentDirectory(string path) - { - return Directory.GetParent(path).FullName; - } - public IEnumerable GetDirectories(string path, string pattern) { return Directory.GetDirectories(path, pattern); diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index 3f172d492..3b6a098d4 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -308,7 +308,7 @@ public bool Exists(NPath append) public bool DirectoryExists() { ThrowIfNotInitialized(); - return FileSystem.DirectoryExists(ToProcessDirectory().ToString()); + return FSWrapper.DirectoryExists(this); } public bool DirectoryExists(string append) @@ -324,13 +324,13 @@ public bool DirectoryExists(NPath append) ThrowIfNotInitialized(); if (!append.IsInitialized) return DirectoryExists(); - return FileSystem.DirectoryExists(Combine(append).ToProcessDirectory().ToString()); + return FSWrapper.DirectoryExists(Combine(append)); } public bool FileExists() { ThrowIfNotInitialized(); - return FileSystem.FileExists(ToProcessDirectory().ToString()); + return FSWrapper.FileExists(this); } public bool FileExists(string append) @@ -346,7 +346,7 @@ public bool FileExists(NPath append) ThrowIfNotInitialized(); if (!append.IsInitialized) return FileExists(); - return FileSystem.FileExists(Combine(append).ToProcessDirectory().ToString()); + return FSWrapper.FileExists(Combine(append)); } public string ExtensionWithDot @@ -535,7 +535,7 @@ public bool IsRoot public IEnumerable Files(string filter, bool recurse = false) { - return FileSystem.GetFiles(ToProcessDirectory().ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); + return FSWrapper.GetFiles(this, filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); } public IEnumerable Files(bool recurse = false) @@ -555,7 +555,7 @@ public IEnumerable Contents(bool recurse = false) public IEnumerable Directories(string filter, bool recurse = false) { - return FileSystem.GetDirectories(ToProcessDirectory().ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); + return FSWrapper.GetDirectories(this, filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s)); } public IEnumerable Directories(bool recurse = false) @@ -572,7 +572,7 @@ public NPath CreateFile() ThrowIfRelative(); ThrowIfRoot(); EnsureParentDirectoryExists(); - FileSystem.WriteAllBytes(ToProcessDirectory().ToString(), new byte[0]); + FSWrapper.WriteAllBytes(this, new byte[0]); return this; } @@ -597,7 +597,7 @@ public NPath CreateDirectory() if (IsRoot) throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString()); - FileSystem.DirectoryCreate(ToProcessDirectory().ToString()); + FSWrapper.DirectoryCreate(this); return this; } @@ -666,7 +666,7 @@ NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func absoluteDestination.EnsureParentDirectoryExists(); - FileSystem.FileCopy(ToProcessDirectory().ToString(), absoluteDestination.ToString(), true); + FSWrapper.FileCopy(this, absoluteDestination, true); return absoluteDestination; } @@ -698,11 +698,11 @@ public void Delete(DeleteMode deleteMode = DeleteMode.Normal) { if (isFile) { - FileSystem.FileDelete(ToProcessDirectory().ToString()); + FSWrapper.FileDelete(this); } else { - FileSystem.DirectoryDelete(ToProcessDirectory().ToString(), true); + FSWrapper.DirectoryDelete(this, true); } } catch (IOException) @@ -797,13 +797,13 @@ public NPath Move(NPath dest) { dest.DeleteIfExists(); dest.EnsureParentDirectoryExists(); - FileSystem.FileMove(ToProcessDirectory().ToString(), dest.ToProcessDirectory().ToString()); + FSWrapper.FileMove(this, dest); return dest; } if (DirectoryExists()) { - FileSystem.DirectoryMove(ToProcessDirectory().ToString(), dest.ToProcessDirectory().ToString()); + FSWrapper.DirectoryMove(this, dest); return dest; } @@ -814,35 +814,35 @@ public NPath WriteAllText(string contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllText(ToProcessDirectory().ToString(), contents); + FSWrapper.WriteAllText(this, contents); return this; } public string ReadAllText() { ThrowIfNotInitialized(); - return FileSystem.ReadAllText(ToProcessDirectory().ToString()); + return FSWrapper.ReadAllText(this); } public NPath WriteAllText(string contents, Encoding encoding) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllText(ToProcessDirectory().ToString(), contents, encoding); + FSWrapper.WriteAllText(this, contents, encoding); return this; } public string ReadAllText(Encoding encoding) { ThrowIfNotInitialized(); - return FileSystem.ReadAllText(ToProcessDirectory().ToString(), encoding); + return FSWrapper.ReadAllText(this, encoding); } public NPath WriteLines(string[] contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteLines(ToProcessDirectory().ToString(), contents); + FSWrapper.WriteLines(this, contents); return this; } @@ -850,28 +850,28 @@ public NPath WriteAllLines(string[] contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllLines(ToProcessDirectory().ToString(), contents); + FSWrapper.WriteAllLines(this, contents); return this; } public string[] ReadAllLines() { ThrowIfNotInitialized(); - return FileSystem.ReadAllLines(ToProcessDirectory().ToString()); + return FSWrapper.ReadAllLines(this); } public NPath WriteAllBytes(byte[] contents) { ThrowIfNotInitialized(); EnsureParentDirectoryExists(); - FileSystem.WriteAllBytes(ToProcessDirectory().ToString(), contents); + FSWrapper.WriteAllBytes(this, contents); return this; } public byte[] ReadAllBytes() { ThrowIfNotInitialized(); - return FileSystem.ReadAllBytes(ToProcessDirectory().ToString()); + return FSWrapper.ReadAllBytes(this); } @@ -1100,18 +1100,21 @@ public static IFileSystem FileSystem { if (_fileSystem == null) #if UNITY_4 || UNITY_5 || UNITY_5_3_OR_NEWER - _fileSystem = new FileSystem(UnityEngine.Application.dataPath); + FileSystem = new FileSystem(UnityEngine.Application.dataPath); #else - _fileSystem = new FileSystem(Directory.GetCurrentDirectory()); + FileSystem = new FileSystem(Directory.GetCurrentDirectory()); #endif return _fileSystem; } set { _fileSystem = value; + FSWrapper = new FSWrapper(value); } } + private static FSWrapper FSWrapper { get; set; } + private static bool? _isUnix; internal static bool IsUnix { @@ -1228,4 +1231,122 @@ public enum DeleteMode Normal, Soft } + + + class FSWrapper + { + private readonly IFileSystem fileSystem; + + public FSWrapper(IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + } + + public void DirectoryCreate(NPath path) + { + fileSystem.DirectoryCreate(path.ToProcessDirectory().ToString()); + } + + public void DirectoryDelete(NPath path, bool recursive) + { + fileSystem.DirectoryDelete(path.ToProcessDirectory().ToString(), recursive); + } + + public bool DirectoryExists(NPath path) + { + return fileSystem.DirectoryExists(path.ToProcessDirectory().ToString()); + } + public void DirectoryMove(NPath from, NPath to) + { + fileSystem.DirectoryMove(from.ToProcessDirectory().ToString(), to.ToProcessDirectory().ToString()); + } + public bool ExistingPathIsDirectory(NPath path) + { + return fileSystem.ExistingPathIsDirectory(path.ToProcessDirectory().ToString()); + } + public void FileCopy(NPath from, NPath to, bool overwrite) + { + fileSystem.FileCopy(from.ToProcessDirectory().ToString(), to.ToProcessDirectory().ToString(), overwrite); + } + public void FileDelete(NPath path) + { + fileSystem.FileDelete(path.ToProcessDirectory().ToString()); + } + public bool FileExists(NPath path) + { + return fileSystem.FileExists(path.ToProcessDirectory().ToString()); + } + public void FileMove(NPath from, NPath to) + { + fileSystem.FileMove(from.ToProcessDirectory().ToString(), to.ToProcessDirectory().ToString()); + } + public IEnumerable GetDirectories(NPath path) + { + return fileSystem.GetDirectories(path.ToProcessDirectory().ToString()); + } + public IEnumerable GetDirectories(NPath path, string pattern) + { + return fileSystem.GetDirectories(path.ToProcessDirectory().ToString(), pattern); + } + public IEnumerable GetDirectories(NPath path, string pattern, SearchOption searchOption) + { + return fileSystem.GetDirectories(path.ToProcessDirectory().ToString(), pattern, searchOption); + } + public IEnumerable GetFiles(NPath path) + { + return fileSystem.GetFiles(path.ToProcessDirectory().ToString()); + } + public IEnumerable GetFiles(NPath path, string pattern) + { + return fileSystem.GetFiles(path.ToProcessDirectory().ToString(), pattern); + } + public IEnumerable GetFiles(NPath path, string pattern, SearchOption searchOption) + { + return fileSystem.GetFiles(path.ToProcessDirectory().ToString(), pattern, searchOption); + } + public Stream OpenRead(NPath path) + { + return fileSystem.OpenRead(path.ToProcessDirectory().ToString()); + } + public Stream OpenWrite(NPath path, FileMode mode) + { + return fileSystem.OpenWrite(path.ToProcessDirectory().ToString(), mode); + } + public byte[] ReadAllBytes(NPath path) + { + return fileSystem.ReadAllBytes(path.ToProcessDirectory().ToString()); + } + public string[] ReadAllLines(NPath path) + { + return fileSystem.ReadAllLines(path.ToProcessDirectory().ToString()); + } + public string ReadAllText(NPath path) + { + return fileSystem.ReadAllText(path.ToProcessDirectory().ToString()); + } + public string ReadAllText(NPath path, Encoding encoding) + { + return fileSystem.ReadAllText(path.ToProcessDirectory().ToString(), encoding); + } + public void WriteAllBytes(NPath path, byte[] bytes) + { + fileSystem.WriteAllBytes(path.ToProcessDirectory().ToString(), bytes); + } + public void WriteAllLines(NPath path, string[] contents) + { + fileSystem.WriteAllLines(path.ToProcessDirectory().ToString(), contents); + } + public void WriteAllText(NPath path, string contents) + { + fileSystem.WriteAllText(path.ToProcessDirectory().ToString(), contents); + } + public void WriteAllText(NPath path, string contents, Encoding encoding) + { + fileSystem.WriteAllText(path.ToProcessDirectory().ToString(), contents, encoding); + } + public void WriteLines(NPath path, string[] contents) + { + fileSystem.WriteLines(path.ToProcessDirectory().ToString(), contents); + } + } } diff --git a/src/tests/IntegrationTests/CachingClasses.cs b/src/tests/IntegrationTests/CachingClasses.cs index 1699e12b5..b359118c7 100644 --- a/src/tests/IntegrationTests/CachingClasses.cs +++ b/src/tests/IntegrationTests/CachingClasses.cs @@ -397,6 +397,12 @@ public void UpdateData(IRepositoryInfoCacheData data) isUpdated = true; } + if (currentHead != data.CurrentHead) + { + currentHead = data.CurrentHead; + isUpdated = true; + } + SaveData(now, isUpdated); } diff --git a/src/tests/UnitTests/IO/GitEnvironmentTestsBase.cs b/src/tests/UnitTests/IO/GitEnvironmentTestsBase.cs index b288e5320..fca50112e 100644 --- a/src/tests/UnitTests/IO/GitEnvironmentTestsBase.cs +++ b/src/tests/UnitTests/IO/GitEnvironmentTestsBase.cs @@ -15,37 +15,9 @@ protected object BuildFindRootFileSystem() filesystem.DirectorySeparatorChar.Returns('\\'); - filesystem - .GetDirectoryName(Args.String) - .Returns(info => Path.GetDirectoryName((string) info[0])); - filesystem.Combine(Args.String, Args.String) .Returns(info => Path.Combine((string) info[0], (string) info[1])); - filesystem.GetParentDirectory(Args.String) - .Returns(info => - { - switch ((string) info[0]) - { - case @"c:\Source\file.txt": - return @"c:\Source"; - - case @"c:\Documents\file.txt": - return @"c:\Documents"; - - case @"c:\Source": - case @"c:\Documents": - case @"c:\file.txt": - return @"c:"; - - case @"c:": - return null; - - default: - throw new ArgumentException(); - } - }); - filesystem.DirectoryExists(Args.String) .Returns(info => { @@ -75,4 +47,4 @@ protected object BuildFindRootFileSystem() return filesystem; } } -} \ No newline at end of file +} From c4397957a787e20e310903b54bfaf1221ffa5915 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 00:39:54 +0200 Subject: [PATCH 295/567] Refactor how we run tasks synchronously so that TPL tasks aren't broken Our task system uses TPL tasks (System.Threading.Tasks.Task) under the hood (every ITask really is a TPL task whose body is the callback or Run/RunWithReturn/RunWithData method override), so when we run one of our ITask, what we're really doing is scheduling the underlying TPL task to be executed. If we want to run the ITask synchronously, it's straightforward to just execute the Run/RunWithReturn/RunWithData method that does the actual work. Because of this, we also have an overload to pass in an existing TPL task into an ITask, so that we can also run async/await task types in our threading system (so that we can control the thread these tasks are going to be run in, given that async/await has no threading control model). The problem with allowing a TPL task to be set directly into an ITask is that the TPL task wasn't set up in a way that exposed a Run/RunWithReturn/RunWithData method that could be called synchronously.This PR moves things around so that instead of calling directly the Run/RunWithReturn/RunWithData methods, there is one RunSynchronously method that does the right thing for all task types, regardless of how they're initialized. This has the added benefit that the exact same method (`RunSynchronously`) is executed independent of who's calling it - if the ITask is running on the scheduler, `RunSynchronously` is the task body that the scheduler will execute - if the user wants to run it in thread, they can call it directly. --- .../Application/ApplicationManagerBase.cs | 22 +- src/GitHub.Api/Authentication/LoginManager.cs | 2 +- src/GitHub.Api/Installer/GitInstaller.cs | 16 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 4 +- src/GitHub.Api/Installer/UnzipTask.cs | 11 +- src/GitHub.Api/Managers/Downloader.cs | 124 ++---- src/GitHub.Api/Primitives/Package.cs | 4 +- src/GitHub.Api/Tasks/ActionTask.cs | 372 ++++++++++++------ src/GitHub.Api/Tasks/DownloadTask.cs | 13 +- src/GitHub.Api/Tasks/ProcessTask.cs | 42 +- src/GitHub.Api/Tasks/TaskBase.cs | 287 ++++++-------- src/GitHub.Api/Tasks/TaskExtensions.cs | 4 +- .../IntegrationTests/BaseIntegrationTest.cs | 4 +- .../Download/DownloadTaskTests.cs | 22 +- .../IntegrationTests/Git/GitClientTests.cs | 4 +- .../Installer/GitInstallerTests.cs | 10 +- src/tests/TaskSystemIntegrationTests/Tests.cs | 125 +++++- 17 files changed, 566 insertions(+), 500 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index b9f16fc71..2539afd76 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -16,7 +16,7 @@ abstract class ApplicationManagerBase : IApplicationManager private Progress progress = new Progress(TaskBase.Default); protected bool isBusy; private bool firstRun; - protected bool FirstRun { get { return firstRun; } set { firstRun = value; } } + protected bool FirstRun { get { return firstRun; } set { firstRun = value; } } private Guid instanceId; protected Guid InstanceId { get { return instanceId; } set { instanceId = value; } } @@ -75,7 +75,7 @@ public void Run() var getEnvPath = new SimpleProcessTask(TaskManager.Token, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") .Configure(ProcessManager, dontSetupGit: true) .Catch(e => true); // make sure this doesn't throw if the task fails - var path = getEnvPath.RunWithReturn(true); + var path = getEnvPath.RunSynchronously(); if (getEnvPath.Successful) { Logger.Trace("Existing Environment Path Original:{0} Updated:{1}", Environment.Path, path); @@ -194,7 +194,7 @@ public void SetupGit(GitInstaller.GitInstallationState state) Logger.Error(e, "Error running lfs install"); return true; }) - .RunWithReturn(true); + .RunSynchronously(); } if (Environment.IsWindows) @@ -204,7 +204,7 @@ public void SetupGit(GitInstaller.GitInstallationState state) { Logger.Error(e, "Error getting the credential helper"); return true; - }).RunWithReturn(true); + }).RunSynchronously(); if (string.IsNullOrEmpty(credentialHelper)) { @@ -215,7 +215,7 @@ public void SetupGit(GitInstaller.GitInstallationState state) Logger.Error(e, "Error setting the credential helper"); return true; }) - .RunWithReturn(true); + .RunSynchronously(); } } } @@ -238,21 +238,21 @@ public void InitializeRepository() var filesForInitialCommit = new List { gitignore, gitAttrs, assetsGitignore }; - GitClient.Init().RunWithReturn(true); + GitClient.Init().RunSynchronously(); progress.UpdateProgress(10, 100, "Initializing..."); ConfigureMergeSettings(); progress.UpdateProgress(20, 100, "Initializing..."); - GitClient.LfsInstall().RunWithReturn(true); + GitClient.LfsInstall().RunSynchronously(); progress.UpdateProgress(30, 100, "Initializing..."); AssemblyResources.ToFile(ResourceType.Generic, ".gitignore", targetPath, Environment); AssemblyResources.ToFile(ResourceType.Generic, ".gitattributes", targetPath, Environment); assetsGitignore.CreateFile(); - GitClient.Add(filesForInitialCommit).RunWithReturn(true); + GitClient.Add(filesForInitialCommit).RunSynchronously(); progress.UpdateProgress(60, 100, "Initializing..."); - GitClient.Commit("Initial commit", null).RunWithReturn(true); + GitClient.Commit("Initial commit", null).RunSynchronously(); progress.UpdateProgress(70, 100, "Initializing..."); Environment.InitializeRepository(); UsageTracker.IncrementProjectsInitialized(); @@ -287,12 +287,12 @@ private void ConfigureMergeSettings() GitClient.SetConfig("merge.unityyamlmerge.cmd", yamlMergeCommand, GitConfigSource.Local).Catch(e => { Logger.Error(e, "Error setting merge.unityyamlmerge.cmd"); return true; - }).RunWithReturn(true); + }).RunSynchronously(); GitClient.SetConfig("merge.unityyamlmerge.trustExitCode", "false", GitConfigSource.Local).Catch(e => { Logger.Error(e, "Error setting merge.unityyamlmerge.trustExitCode"); return true; - }).RunWithReturn(true); + }).RunSynchronously(); } public void RestartRepository() diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index f36fc8072..d59bbcd5d 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -138,7 +138,7 @@ public ITask Logout(UriString hostAddress) { Guard.ArgumentNotNull(hostAddress, nameof(hostAddress)); - return new ActionTask(keychain.Clear(hostAddress, true)) { Message = "Signing out" }.Start(); + return new TPLTask(keychain.Clear(hostAddress, true)) { Message = "Signing out" }.Start(); } private async Task TryLogin( diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index a3768bf60..32a158504 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -106,7 +106,7 @@ private GitInstallationState FindGit(GitInstallationState state) var gitPath = new FindExecTask("git", cancellationToken) .Configure(processManager, dontSetupGit: true) .Catch(e => true) - .RunWithReturn(true); + .RunSynchronously(); state.GitExecutablePath = gitPath; state = ValidateGitVersion(state); if (state.GitIsValid) @@ -122,7 +122,7 @@ private GitInstallationState FindGitLfs(GitInstallationState state) var gitLfsPath = new FindExecTask("git-lfs", cancellationToken) .Configure(processManager, dontSetupGit: true) .Catch(e => true) - .RunWithReturn(true); + .RunSynchronously(); state.GitLfsExecutablePath = gitLfsPath; state = ValidateGitLfsVersion(state); if (state.GitLfsIsValid) @@ -159,7 +159,7 @@ public GitInstallationState ValidateGitVersion(GitInstallationState state) var version = new GitVersionTask(cancellationToken) .Configure(processManager, state.GitExecutablePath, dontSetupGit: true) .Catch(e => true) - .RunWithReturn(true); + .RunSynchronously(); state.GitIsValid = version >= Constants.MinimumGitVersion; state.GitVersion = version; return state; @@ -175,7 +175,7 @@ public GitInstallationState ValidateGitLfsVersion(GitInstallationState state) var version = new ProcessTask(cancellationToken, "version", new LfsVersionOutputProcessor()) .Configure(processManager, state.GitLfsExecutablePath, dontSetupGit: true) .Catch(e => true) - .RunWithReturn(true); + .RunSynchronously(); state.GitLfsIsValid = version >= Constants.MinimumGitLfsVersion; state.GitLfsVersion = version; return state; @@ -244,7 +244,7 @@ private GitInstallationState GetZipsIfNeeded(GitInstallationState state) if (state.GitZipExists && state.GitLfsZipExists) return state; - var downloader = new Downloader(); + var downloader = new Downloader(environment.FileSystem); downloader.Catch(e => { LogHelper.Trace(e, "Failed to download"); @@ -255,7 +255,7 @@ private GitInstallationState GetZipsIfNeeded(GitInstallationState state) downloader.QueueDownload(state.GitPackage.Uri, installDetails.ZipPath); if (!state.GitLfsZipExists && !state.GitLfsIsValid && state.GitLfsPackage != null) downloader.QueueDownload(state.GitLfsPackage.Uri, installDetails.ZipPath); - downloader.RunWithReturn(true); + downloader.RunSynchronously(); state.GitZipExists = installDetails.GitZipPath.FileExists(); state.GitLfsZipExists = installDetails.GitLfsZipPath.FileExists(); @@ -295,7 +295,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) return true; }); unzipTask.Progress(p => Progress.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Message)); - var path = unzipTask.RunWithReturn(true); + var path = unzipTask.RunSynchronously(); var target = state.GitInstallationPath; if (unzipTask.Successful) { @@ -320,7 +320,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) return true; }); unzipTask.Progress(p => Progress.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Message)); - var path = unzipTask.RunWithReturn(true); + var path = unzipTask.RunSynchronously(); var target = state.GitLfsInstallationPath; if (unzipTask.Successful) { diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 6c332478b..0d783b8b6 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -38,7 +38,7 @@ public NPath SetupOctorunIfNeeded() tempZipExtractPath, sharpZipLibHelper, fileSystem) .Catch(e => { Logger.Error(e, "Error extracting octorun"); return true; }); - var extractPath = unzipTask.RunWithReturn(true); + var extractPath = unzipTask.RunSynchronously(); if (unzipTask.Successful) path = MoveOctorun(extractPath.Combine("octorun")); return path; @@ -112,4 +112,4 @@ public OctorunInstallDetails(NPath baseDataPath) public NPath VersionFile => InstallationPath.Combine("version"); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Installer/UnzipTask.cs b/src/GitHub.Api/Installer/UnzipTask.cs index 54b897d28..6be81e1f9 100644 --- a/src/GitHub.Api/Installer/UnzipTask.cs +++ b/src/GitHub.Api/Installer/UnzipTask.cs @@ -26,12 +26,9 @@ protected NPath BaseRun(bool success) return base.RunWithReturn(success); } - public override NPath RunWithReturn(bool success) + protected override NPath RunWithReturn(bool success) { var ret = BaseRun(success); - - RaiseOnStart(); - try { ret = RunUnzip(success); @@ -39,11 +36,7 @@ public override NPath RunWithReturn(bool success) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(ret); + throw exception; } return ret; } diff --git a/src/GitHub.Api/Managers/Downloader.cs b/src/GitHub.Api/Managers/Downloader.cs index 55552d5a0..0a643ccc0 100644 --- a/src/GitHub.Api/Managers/Downloader.cs +++ b/src/GitHub.Api/Managers/Downloader.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; using System.IO; using System.Net; -using System.Threading; -using System.Threading.Tasks; using GitHub.Logging; -using System.Linq; namespace GitHub.Unity { @@ -20,111 +16,39 @@ public DownloadData(UriString url, NPath file) } } - class Downloader : FuncListTask + class Downloader : TaskQueue { - public event Action DownloadStart; - public event Action DownloadComplete; - public event Action DownloadFailed; + public event Action OnDownloadStart; + public event Action OnDownloadComplete; + public event Action OnDownloadFailed; - private readonly List downloaders = new List(); - - public override string Message { get; set; } = "Downloading..."; - - public Downloader() : base(TaskManager.Instance.Token, RunDownloaders) + private readonly IFileSystem fileSystem; + public Downloader(IFileSystem fileSystem) + : base(t => + { + var dt = t as DownloadTask; + var destinationFile = dt.TargetDirectory.Combine(dt.Url.Filename); + return new DownloadData(dt.Url, destinationFile); + }) { + this.fileSystem = fileSystem; Name = "Downloader"; + Message = "Downloading..."; } public void QueueDownload(UriString url, NPath targetDirectory) { - var downloaderTask = new DownloaderTask(); - downloaderTask.QueueDownload(url, targetDirectory); - downloaders.Add(downloaderTask); - } - - private static List RunDownloaders(bool success, FuncListTask source) - { - Downloader self = (Downloader)source; - List result = null; - var listOfTasks = new List>(); - foreach (var downloader in self.downloaders) - { - downloader.DownloadStart += self.DownloadStart; - downloader.DownloadComplete += self.DownloadComplete; - downloader.DownloadFailed += self.DownloadFailed; - listOfTasks.Add(downloader.Run()); - } - var res = TaskEx.WhenAll(listOfTasks).Result; - if (res != null) - result = new List(res); - return result; - } - - class DownloaderTask - { - public event Action DownloadStart; - public event Action DownloadComplete; - public event Action DownloadFailed; - - private readonly List> queuedTasks = new List>(); - private readonly TaskCompletionSource aggregateDownloads = new TaskCompletionSource(); - private readonly IFileSystem fs; - private readonly CancellationToken cancellationToken; - - private volatile bool isSuccessful = true; - private volatile Exception exception; - private DownloadData result; - - public DownloaderTask() - { - fs = NPath.FileSystem; - cancellationToken = TaskManager.Instance.Token; - DownloadComplete += d => aggregateDownloads.TrySetResult(d); - DownloadFailed += (_, e) => aggregateDownloads.TrySetException(e); - } - - public Task Run() - { - foreach (var task in queuedTasks) - task.Start(); - if (queuedTasks.Count == 0) - DownloadComplete(result); - return aggregateDownloads.Task; - } - - public Task QueueDownload(UriString url, NPath targetDirectory) + var download = new DownloadTask(Token, fileSystem, url, targetDirectory); + download.OnStart += t => OnDownloadStart?.Invoke(((DownloadTask)t).Url); + download.OnEnd += (t, res, s, ex) => { - var destinationFile = targetDirectory.Combine(url.Filename); - result = new DownloadData(url, destinationFile); - - Action, NPath, bool, Exception> verifyDownload = (t, res, success, ex) => - { - isSuccessful &= success; - if (!success) - exception = ex; - if (!isSuccessful) - { - DownloadFailed(result, exception); - } - else - { - DownloadComplete(result); - } - }; - - var fileDownload = DownloadFile(url, targetDirectory, result, verifyDownload); - fileDownload.OnStart += _ => DownloadStart?.Invoke(result); - queuedTasks.Add(fileDownload); - return aggregateDownloads.Task; - } - - private ITask DownloadFile(UriString url, NPath targetDirectory, DownloadData res, Action, NPath, bool, Exception> verifyDownload) - { - var download = new DownloadTask(cancellationToken, fs, url, targetDirectory) - .Catch(e => { DownloadFailed(res, e); return true; }); - download.OnEnd += verifyDownload; - return download; - } + if (s) + OnDownloadComplete?.Invoke(((DownloadTask)t).Url, res); + else + OnDownloadFailed?.Invoke(((DownloadTask)t).Url, ex); + }; + // queue after hooking up events so OnDownload* gets called first + Queue(download); } public static bool Download(ILogging logger, UriString url, diff --git a/src/GitHub.Api/Primitives/Package.cs b/src/GitHub.Api/Primitives/Package.cs index 7b90aea47..3cd3eb73c 100644 --- a/src/GitHub.Api/Primitives/Package.cs +++ b/src/GitHub.Api/Primitives/Package.cs @@ -41,7 +41,7 @@ public static Package Load(IEnvironment environment, UriString packageFeed) LogHelper.Warning(@"Error downloading package feed:{0} ""{1}"" Message:""{2}""", packageFeed, ex.GetType().ToString(), ex.GetExceptionMessageShort()); return true; }) - .RunWithReturn(true); + .RunSynchronously(); if (feed.IsInitialized) environment.UserSettings.Set(key, now); @@ -67,4 +67,4 @@ public static Package Load(IEnvironment environment, UriString packageFeed) return package; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index 3b24160cc..22872fe35 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; namespace GitHub.Unity { - class TaskQueue : TaskBase + class TaskQueue : TPLTask { private TaskCompletionSource aggregateTask = new TaskCompletionSource(); private readonly List queuedTasks = new List(); - private volatile bool isSuccessful = true; - private volatile Exception taskException; private int finishedTaskCount; public TaskQueue() : base() @@ -20,38 +20,231 @@ public TaskQueue() : base() public ITask Queue(ITask task) { - task.OnEnd += TaskFinished; - task.Catch(e => TaskFinished(task, false, e)); + // if this task fails, both OnEnd and Catch will be called + // if a task before this one on the chain fails, only Catch will be called + // so avoid calling TaskFinished twice by ignoring failed OnEnd calls + task.OnEnd += InvokeFinishOnlyOnSuccess; + task.Catch(e => TaskFinished(false, e)); queuedTasks.Add(task); return this; } - protected override void Run() + public override void RunSynchronously() { foreach (var task in queuedTasks) task.Start(); - base.Run(); + base.RunSynchronously(); } - private void TaskFinished(ITask task, bool success, Exception ex) + protected override void Schedule() { - if (!success) + foreach (var task in queuedTasks) + task.Start(); + base.Schedule(); + } + + private void InvokeFinishOnlyOnSuccess(ITask task, bool success, Exception ex) + { + if (success) + TaskFinished(true, null); + } + + private void TaskFinished(bool success, Exception ex) + { + var count = Interlocked.Increment(ref finishedTaskCount); + if (count == queuedTasks.Count) + { + var exceptions = queuedTasks.Where(x => !x.Successful).Select(x => x.Exception).ToArray(); + var isSuccessful = exceptions.Length == 0; + + if (isSuccessful) + { + aggregateTask.TrySetResult(true); + } + else + { + aggregateTask.TrySetException(new AggregateException(exceptions)); + } + } + } + } + + class TaskQueue : TPLTask> + { + private TaskCompletionSource> aggregateTask = new TaskCompletionSource>(); + private readonly List> queuedTasks = new List>(); + private int finishedTaskCount; + private Func, TResult> resultConverter; + + /// + /// If is not assignable to , you must pass a + /// method to convert between the two. Implicit conversions don't count (so even though NPath has an implicit + /// conversion to string, you still need to pass in a converter) + /// + /// + public TaskQueue(Func, TResult> resultConverter = null) : base() + { + // this excludes implicit operators - that requires using reflection to figure out if + // the types are convertible, and I'd rather not do that + if (resultConverter == null && !typeof(TResult).IsAssignableFrom(typeof(TTaskResult))) { - isSuccessful = false; - taskException = ex; + throw new ArgumentNullException(nameof(resultConverter), + String.Format(CultureInfo.InvariantCulture, "Cannot cast {0} to {1} and no {2} method was passed in to do the conversion", typeof(TTaskResult), typeof(TResult), nameof(resultConverter))); } + this.resultConverter = resultConverter; + Initialize(aggregateTask.Task); + } + + /// + /// Queues an ITask for running, and when the task is done, is called + /// to convert the result of the task to something else + /// + /// + /// + /// + public ITask Queue(ITask task) + { + // if this task fails, both OnEnd and Catch will be called + // if a task before this one on the chain fails, only Catch will be called + // so avoid calling TaskFinished twice by ignoring failed OnEnd calls + task.OnEnd += InvokeFinishOnlyOnSuccess; + task.Catch(e => TaskFinished(default(TTaskResult), false, e)); + queuedTasks.Add(task); + return task; + } + + public override List RunSynchronously() + { + foreach (var task in queuedTasks) + task.Start(); + return base.RunSynchronously(); + } + + protected override void Schedule() + { + foreach (var task in queuedTasks) + task.Start(); + base.Schedule(); + } + + private void InvokeFinishOnlyOnSuccess(ITask task, TTaskResult result, bool success, Exception ex) + { + if (success) + TaskFinished(result, true, null); + } + + private void TaskFinished(TTaskResult result, bool success, Exception ex) + { var count = Interlocked.Increment(ref finishedTaskCount); if (count == queuedTasks.Count) { + var exceptions = queuedTasks.Where(x => !x.Successful).Select(x => x.Exception).ToArray(); + var isSuccessful = exceptions.Length == 0; + if (isSuccessful) { - aggregateTask.TrySetResult(true); + List results; + if (resultConverter != null) + results = queuedTasks.Select(x => resultConverter(x)).ToList(); + else + results = queuedTasks.Select(x => (TResult)(object)x.Result).ToList(); + aggregateTask.TrySetResult(results); } else { - aggregateTask.TrySetException(taskException.GetBaseException()); + aggregateTask.TrySetException(new AggregateException(exceptions)); + } + } + } + } + + class TPLTask : TaskBase + { + private Task task; + + protected TPLTask() : base() + {} + + public TPLTask(Task task) + : base() + { + Initialize(task); + } + + protected void Initialize(Task theTask) + { + this.task = theTask; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); + } + + protected override void Run(bool success) + { + base.Run(success); + + Token.ThrowIfCancellationRequested(); + try + { + if (task.Status == TaskStatus.Created && !task.IsCompleted && + ((task.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) + { + var scheduler = TaskManager.GetScheduler(Affinity); + Token.ThrowIfCancellationRequested(); + task.RunSynchronously(scheduler); + } + else + task.Wait(); + } + catch (Exception ex) + { + if (!RaiseFaultHandlers(ex)) + throw exception; + Token.ThrowIfCancellationRequested(); + } + } + } + + class TPLTask : TaskBase + { + private Task task; + + protected TPLTask() : base() + { } + + public TPLTask(Task task) + : base() + { + Initialize(task); + } + + protected void Initialize(Task theTask) + { + this.task = theTask; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); + } + + protected override T RunWithReturn(bool success) + { + var ret = base.RunWithReturn(success); + + Token.ThrowIfCancellationRequested(); + try + { + if (task.Status == TaskStatus.Created && !task.IsCompleted && + ((task.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) + { + var scheduler = TaskManager.GetScheduler(Affinity); + Token.ThrowIfCancellationRequested(); + task.RunSynchronously(scheduler); } + ret = task.Result; } + catch (Exception ex) + { + if (!RaiseFaultHandlers(ex)) + throw exception; + Token.ThrowIfCancellationRequested(); + } + return ret; } } @@ -84,17 +277,9 @@ public ActionTask(CancellationToken token, Action action) Name = "ActionTask"; } - public ActionTask(Task task) - : base(task) - { - Name = "ActionTask(Task)"; - } - - public override void Run(bool success) + protected override void Run(bool success) { base.Run(success); - - RaiseOnStart(); try { Callback?.Invoke(success); @@ -107,17 +292,15 @@ public override void Run(bool success) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(); + throw exception; } } } class ActionTask : TaskBase { + private readonly Func getPreviousResult; + protected Action Callback { get; } protected Action CallbackWithException { get; } @@ -132,24 +315,8 @@ public ActionTask(CancellationToken token, Action action, Func getPr { Guard.ArgumentNotNull(action, "action"); this.Callback = action; - Task = new Task(() => - { - Token.ThrowIfCancellationRequested(); - var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); - - // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task - // otherwise if there's a method to retrieve the value, call that - // otherwise use the PreviousResult property - T prevResult = PreviousResult; - if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) - prevResult = ((ITask)DependsOn).Result; - else if (getPreviousResult != null) - prevResult = getPreviousResult(); - - Run(previousIsSuccessful, prevResult); - - }, Token, TaskCreationOptions.None); - + this.getPreviousResult = getPreviousResult; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); Name = $"ActionTask<{typeof(T)}>"; } @@ -164,38 +331,39 @@ public ActionTask(CancellationToken token, Action action, Fu { Guard.ArgumentNotNull(action, "action"); this.CallbackWithException = action; - Task = new Task(() => - { - Token.ThrowIfCancellationRequested(); - var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); - - // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task - // otherwise if there's a method to retrieve the value, call that - // otherwise use the PreviousResult property - T prevResult = PreviousResult; - if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) - prevResult = ((ITask)DependsOn).Result; - else if (getPreviousResult != null) - prevResult = getPreviousResult(); - - Run(previousIsSuccessful, prevResult); - - }, Token, TaskCreationOptions.None); + this.getPreviousResult = getPreviousResult; + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); Name = $"ActionTask"; } - public ActionTask(Task task) - : base(task) + public override void RunSynchronously() { - Name = $"ActionTask<{typeof(T)}>(Task)"; + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + + // if this task depends on another task and the dependent task was successful, use the value of that other task as input to this task + // otherwise if there's a method to retrieve the value, call that + // otherwise use the PreviousResult property + T prevResult = PreviousResult; + if (previousIsSuccessful && DependsOn != null && DependsOn is ITask) + prevResult = ((ITask)DependsOn).Result; + else if (getPreviousResult != null) + prevResult = getPreviousResult(); + + try + { + Run(previousIsSuccessful, prevResult); + } + finally + { + RaiseOnEnd(); + } } protected virtual void Run(bool success, T previousResult) { base.Run(success); - - RaiseOnStart(); - try { Callback?.Invoke(success, previousResult); @@ -208,11 +376,7 @@ protected virtual void Run(bool success, T previousResult) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(); + throw exception; } } @@ -248,18 +412,9 @@ public FuncTask(CancellationToken token, Func action) Name = $"FuncTask"; } - public FuncTask(Task task) - : base(task) - { - Name = $"FuncTask<{typeof(T)}>(Task)"; - } - - public override T RunWithReturn(bool success) + protected override T RunWithReturn(bool success) { T result = base.RunWithReturn(success); - - RaiseOnStart(); - try { if (Callback != null) @@ -275,13 +430,8 @@ public override T RunWithReturn(bool success) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(result); + throw exception; } - return result; } } @@ -307,19 +457,9 @@ public FuncTask(CancellationToken token, Func actio Name = $"FuncTask<{typeof(T)}, Exception, {typeof(TResult)}>"; } - - public FuncTask(Task task) - : base(task) - { - Name = $"FuncTask<{typeof(T)}, {typeof(TResult)}>(Task)"; - } - protected override TResult RunWithData(bool success, T previousResult) { var result = base.RunWithData(success, previousResult); - - RaiseOnStart(); - try { if (Callback != null) @@ -335,13 +475,8 @@ protected override TResult RunWithData(bool success, T previousResult) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; - } - finally - { - RaiseOnEnd(result); + throw exception; } - return result; } } @@ -373,16 +508,9 @@ public FuncListTask(CancellationToken token, Func, List this.CallbackWithSelf = action; } - public FuncListTask(Task> task) - : base(task) - { } - - public override List RunWithReturn(bool success) + protected override List RunWithReturn(bool success) { var result = base.RunWithReturn(success); - - RaiseOnStart(); - try { if (Callback != null) @@ -402,14 +530,12 @@ public override List RunWithReturn(bool success) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; + throw exception; } finally { if (result == null) result = new List(); - - RaiseOnEnd(result); } return result; } @@ -434,16 +560,9 @@ public FuncListTask(CancellationToken token, Func> task) - : base(task) - { } - protected override List RunWithData(bool success, T previousResult) { var result = base.RunWithData(success, previousResult); - - RaiseOnStart(); - try { if (Callback != null) @@ -459,13 +578,8 @@ protected override List RunWithData(bool success, T previousResult) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; + throw exception; } - finally - { - RaiseOnEnd(result); - } - return result; } } diff --git a/src/GitHub.Api/Tasks/DownloadTask.cs b/src/GitHub.Api/Tasks/DownloadTask.cs index 1d7da391bac..d7c173c55 100644 --- a/src/GitHub.Api/Tasks/DownloadTask.cs +++ b/src/GitHub.Api/Tasks/DownloadTask.cs @@ -49,12 +49,9 @@ protected string BaseRunWithReturn(bool success) return base.RunWithReturn(success); } - public override NPath RunWithReturn(bool success) + protected override NPath RunWithReturn(bool success) { var result = base.RunWithReturn(success); - - RaiseOnStart(); - try { result = RunDownload(success); @@ -62,13 +59,8 @@ public override NPath RunWithReturn(bool success) catch (Exception ex) { if (!RaiseFaultHandlers(ex)) - throw; + throw exception; } - finally - { - RaiseOnEnd(result); - } - return result; } @@ -76,7 +68,6 @@ public override NPath RunWithReturn(bool success) /// The actual functionality to download with optional hash verification /// subclasses that wish to return the contents of the downloaded file /// or do something else with it can override this instead of RunWithReturn. - /// If you do, you must call RaiseOnStart()/RaiseOnEnd() /// /// /// diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index b0a4a9127..3cbe33538 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -279,12 +279,6 @@ public void Configure(Process existingProcess) Name = ProcessArguments; } - protected override void RaiseOnStart() - { - base.RaiseOnStart(); - OnStartProcess?.Invoke(this); - } - protected override void RaiseOnEnd() { base.RaiseOnEnd(); @@ -295,12 +289,12 @@ protected virtual void ConfigureOutputProcessor() { } - public override T RunWithReturn(bool success) + protected override T RunWithReturn(bool success) { var result = base.RunWithReturn(success); wrapper = new ProcessWrapper(Name, Process, outputProcessor, - RaiseOnStart, + () => OnStartProcess?.Invoke(this), () => { try @@ -322,15 +316,8 @@ public override T RunWithReturn(bool success) thrownException = new ProcessException(thrownException.GetExceptionMessage(), ex); } - try - { - if (thrownException != null && !RaiseFaultHandlers(thrownException)) - throw thrownException; - } - finally - { - RaiseOnEnd(result); - } + if (thrownException != null && !RaiseFaultHandlers(thrownException)) + throw thrownException; }, (ex, error) => { @@ -410,12 +397,6 @@ public virtual void Configure(ProcessStartInfo psi, IOutputProcessor> ProcessName = psi.FileName; } - protected override void RaiseOnStart() - { - base.RaiseOnStart(); - OnStartProcess?.Invoke(this); - } - protected override void RaiseOnEnd() { base.RaiseOnEnd(); @@ -431,12 +412,12 @@ protected virtual void ConfigureOutputProcessor() outputProcessor.OnEntry += x => RaiseOnData(x); } - public override List RunWithReturn(bool success) + protected override List RunWithReturn(bool success) { var result = base.RunWithReturn(success); wrapper = new ProcessWrapper(Name, Process, outputProcessor, - RaiseOnStart, + () => OnStartProcess?.Invoke(this), () => { try @@ -457,15 +438,8 @@ public override List RunWithReturn(bool success) thrownException = new ProcessException(thrownException.GetExceptionMessage(), ex); } - try - { - if (thrownException != null && !RaiseFaultHandlers(thrownException)) - throw thrownException; - } - finally - { - RaiseOnEnd(result); - } + if (thrownException != null && !RaiseFaultHandlers(thrownException)) + throw thrownException; }, (ex, error) => { diff --git a/src/GitHub.Api/Tasks/TaskBase.cs b/src/GitHub.Api/Tasks/TaskBase.cs index 1cbbbdc13..6b75fc3c7 100644 --- a/src/GitHub.Api/Tasks/TaskBase.cs +++ b/src/GitHub.Api/Tasks/TaskBase.cs @@ -32,7 +32,19 @@ public interface ITask : IAsyncResult T Finally(T taskToContinueWith) where T : ITask; ITask Start(); ITask Start(TaskScheduler scheduler); + void RunSynchronously(); + ITask Progress(Action progressHandler); + void UpdateProgress(long value, long total, string message = null); + + ITask GetTopOfChain(bool onlyCreated = true); + ITask GetEndOfChain(); + + /// + /// + /// true if any task on the chain is marked as exclusive + bool IsChainExclusive(); + bool Successful { get; } string Errors { get; } @@ -43,17 +55,8 @@ public interface ITask : IAsyncResult TaskBase DependsOn { get; } event Action OnStart; event Action OnEnd; - ITask GetTopOfChain(); - - /// - /// - /// true if any task on the chain is marked as exclusive - bool IsChainExclusive(); - - void UpdateProgress(long value, long total, string message = null); - ITask GetEndOfChain(); - void Run(bool success); string Message { get; } + Exception Exception { get; } } public interface ITask : ITask @@ -74,12 +77,13 @@ public interface ITask : ITask ITask Finally(Action continuation, TaskAffinity affinity = TaskAffinity.Concurrent); new ITask Start(); new ITask Start(TaskScheduler scheduler); + new TResult RunSynchronously(); new ITask Progress(Action progressHandler); + TResult Result { get; } new Task Task { get; } new event Action> OnStart; new event Action, TResult, bool, Exception> OnEnd; - TResult RunWithReturn(bool success); } interface ITask : ITask @@ -120,20 +124,7 @@ protected TaskBase(CancellationToken token) Guard.ArgumentNotNull(token, "token"); Token = token; - Task = new Task(() => - { - Token.ThrowIfCancellationRequested(); - var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); - Run(previousIsSuccessful); - }, - Token, - TaskCreationOptions.None); - } - - protected TaskBase(Task task) - : this() - { - Initialize(task); + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); } protected TaskBase() @@ -141,39 +132,6 @@ protected TaskBase() this.progress = new Progress(this); } - protected void Initialize(Task task) - { - Task = new Task(t => - { - Token.ThrowIfCancellationRequested(); - - var scheduler = TaskManager.GetScheduler(Affinity); - RaiseOnStart(); - var tk = ((Task)t); - try - { - if (tk.Status == TaskStatus.Created && !tk.IsCompleted && - ((tk.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) - { - Token.ThrowIfCancellationRequested(); - tk.RunSynchronously(scheduler); - } - else - tk.Wait(); - } - catch (Exception ex) - { - if (!RaiseFaultHandlers(ex)) - throw; - Token.ThrowIfCancellationRequested(); - } - finally - { - RaiseOnEnd(); - } - }, task, Token, TaskCreationOptions.None); - } - public virtual T Then(T nextTask, TaskRunOptions runOptions = TaskRunOptions.OnSuccess, bool taskIsTopOfChain = false) where T : ITask { @@ -326,12 +284,27 @@ public ITask Progress(Action handler) public ITask Start() { - var depends = GetTopMostTaskInCreatedState() ?? this; - depends.Run(); + var depends = GetTopMostStartableTask(); + depends?.Schedule(); return this; } - protected virtual void Run() + public virtual void RunSynchronously() + { + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + try + { + Run(previousIsSuccessful); + } + finally + { + RaiseOnEnd(); + } + } + + protected virtual void Schedule() { if (Task.Status == TaskStatus.Created) { @@ -361,9 +334,18 @@ public virtual ITask Start(TaskScheduler scheduler) return this; } - public ITask GetTopOfChain() + public ITask GetTopOfChain(bool onlyCreated = true) + { + return GetTopMostTask(null, onlyCreated, false); + } + + public ITask GetEndOfChain() { - return GetTopMostTaskInCreatedState() ?? this; + if (continuationOnSuccess != null) + return continuationOnSuccess.GetEndOfChain(); + else if (continuationOnAlways != null) + return continuationOnAlways.GetEndOfChain(); + return this; } /// @@ -390,7 +372,7 @@ protected void SetContinuation(TaskBase continuation, TaskContinuationOptions ru Task.ContinueWith(_ => { Token.ThrowIfCancellationRequested(); - ((TaskBase)(object)continuation).Run(); + ((TaskBase)(object)continuation).Schedule(); }, Token, runOptions, @@ -403,41 +385,41 @@ protected ITask SetDependsOn(ITask dependsOn) return this; } - protected TaskBase GetTopMostTaskInCreatedState() + /// + /// Returns the first startable task on the chain. If the chain has been started + /// already, returns null + /// + protected TaskBase GetTopMostStartableTask() { - var depends = DependsOn; - if (depends == null) - return null; - return depends.GetTopMostTask(null, true); + return GetTopMostTask(null, true, true); } - protected TaskBase GetTopMostTask() + protected TaskBase GetTopMostCreatedTask() { - var depends = DependsOn; - if (depends == null) - return null; - return depends.GetTopMostTask(null, false); + return GetTopMostTask(null, true, false); } - public ITask GetEndOfChain() + protected TaskBase GetTopMostTask() { - if (continuationOnSuccess != null) - return continuationOnSuccess.GetEndOfChain(); - else if (continuationOnAlways != null) - return continuationOnAlways.GetEndOfChain(); - return this; + return GetTopMostTask(null, false, false); } - protected TaskBase GetTopMostTask(TaskBase ret, bool onlyCreatedState) + protected TaskBase GetTopMostTask(TaskBase ret, bool onlyCreated, bool onlyUnstartedChain) { - ret = (!onlyCreatedState || Task.Status == TaskStatus.Created ? this : ret); + ret = (!onlyCreated || Task.Status == TaskStatus.Created ? this : ret); var depends = DependsOn; if (depends == null) + { + // if we're at the top of the chain and the chain has already been started + // and we only care about unstarted chains, return null + if (onlyUnstartedChain && Task.Status != TaskStatus.Created) + return null; return ret; - return depends.GetTopMostTask(ret, onlyCreatedState); + } + return depends.GetTopMostTask(ret, onlyCreated, onlyUnstartedChain); } - public virtual void Run(bool success) + protected virtual void Run(bool success) { taskFailed = false; hasRun = false; @@ -448,12 +430,19 @@ public virtual void Run(bool success) protected virtual void RaiseOnStart() { UpdateProgress(0, 100); + RaiseOnStartInternal(); + } + + protected void RaiseOnStartInternal() + { OnStart?.Invoke(this); } protected virtual bool RaiseFaultHandlers(Exception ex) { - exception = ex is AggregateException ? ex.GetBaseException() : ex; + exception = ex; + if (exception is AggregateException) + exception = exception.GetBaseException() ?? exception; Errors = exception.Message; taskFailed = true; if (catchHandler == null) @@ -473,12 +462,18 @@ protected virtual bool RaiseFaultHandlers(Exception ex) protected virtual void RaiseOnEnd() { - OnEnd?.Invoke(this, !taskFailed, exception); - SetupContinuations(); hasRun = true; + RaiseOnEndInternal(); + SetupContinuations(); UpdateProgress(100, 100); } + protected void RaiseOnEndInternal() + { + OnEnd?.Invoke(this, !taskFailed, exception); + } + + protected void SetupContinuations() { if (!taskFailed || exceptionWasHandled) @@ -536,6 +531,8 @@ public override string ToString() public virtual bool Successful { get { return hasRun && !taskFailed; } } public bool IsCompleted { get { return hasRun; } } + public Exception Exception => exception ?? GetThrownException(); + public string Errors { get; protected set; } public Task Task { get; protected set; } public WaitHandle AsyncWaitHandle { get { return (Task as IAsyncResult).AsyncWaitHandle; } } @@ -552,63 +549,21 @@ public override string ToString() abstract class TaskBase : TaskBase, ITask { - protected TaskCompletionSource tcs = new TaskCompletionSource(); private event Action finallyHandler; public new event Action> OnStart; public new event Action, TResult, bool, Exception> OnEnd; private TResult result; - protected TaskBase(CancellationToken token) - : base(token) - { - Task = new Task(() => - { - Token.ThrowIfCancellationRequested(); - var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); - var ret = RunWithReturn(previousIsSuccessful); - tcs.SetResult(ret); - return ret; - }, Token, TaskCreationOptions.None); - } - - protected TaskBase(Task task) + protected TaskBase() : base() { - Initialize(task); } - protected void Initialize(Task task) + protected TaskBase(CancellationToken token) + : base(token) { - Task = new Task(t => - { - Token.ThrowIfCancellationRequested(); - - TResult ret = default(TResult); - RaiseOnStart(); - var tk = ((Task)t); - try - { - if (tk.Status == TaskStatus.Created && !tk.IsCompleted && - ((tk.CreationOptions & (TaskCreationOptions)512) == TaskCreationOptions.None)) - { - Token.ThrowIfCancellationRequested(); - tk.RunSynchronously(); - } - ret = tk.Result; - } - catch (Exception ex) - { - if (!RaiseFaultHandlers(ex)) - throw; - Token.ThrowIfCancellationRequested(); - } - finally - { - RaiseOnEnd(); - } - return ret; - }, task, Token, TaskCreationOptions.None); + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); } public override T Then(T continuation, TaskRunOptions runOptions = TaskRunOptions.OnSuccess, bool taskIsTopOfChain = false) @@ -713,25 +668,44 @@ public ITask Finally(Action continuation, TaskAffinity return this; } - public virtual TResult RunWithReturn(bool success) + protected virtual TResult RunWithReturn(bool success) { base.Run(success); return result; } + public new virtual TResult RunSynchronously() + { + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + TResult ret = default(TResult); + try + { + ret = RunWithReturn(previousIsSuccessful); + } + finally + { + RaiseOnEnd(ret); + } + return ret; + } + + protected override void RaiseOnStart() { UpdateProgress(0, 100); OnStart?.Invoke(this); - base.RaiseOnStart(); + RaiseOnStartInternal(); } protected virtual void RaiseOnEnd(TResult data) { this.result = data; + hasRun = true; OnEnd?.Invoke(this, result, !taskFailed, exception); + RaiseOnEndInternal(); SetupContinuations(); - hasRun = true; UpdateProgress(100, 100); } @@ -746,7 +720,7 @@ protected override void CallFinallyHandler() get { return base.Task as Task; } set { base.Task = value; } } - public TResult Result { get { return Task.Result; } } + public TResult Result { get { return result; } } } abstract class TaskBase : TaskBase @@ -754,20 +728,27 @@ abstract class TaskBase : TaskBase public TaskBase(CancellationToken token) : base(token) { - Task = new Task(() => - { - Token.ThrowIfCancellationRequested(); - var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); - T prevResult = previousIsSuccessful && DependsOn != null && DependsOn is ITask ? ((ITask)DependsOn).Result : default(T); - var ret = RunWithData(previousIsSuccessful, prevResult); - tcs.SetResult(ret); - return ret; - }, Token, TaskCreationOptions.None); + Task = new Task(RunSynchronously, Token, TaskCreationOptions.None); } - public TaskBase(Task task) - : base(task) - { } + public override TResult RunSynchronously() + { + RaiseOnStart(); + Token.ThrowIfCancellationRequested(); + var previousIsSuccessful = previousSuccess.HasValue ? previousSuccess.Value : (DependsOn?.Successful ?? true); + T prevResult = previousIsSuccessful && DependsOn != null && DependsOn is ITask ? ((ITask)DependsOn).Result : default(T); + + TResult ret = default(TResult); + try + { + ret = RunWithData(previousIsSuccessful, prevResult); + } + finally + { + RaiseOnEnd(ret); + } + return ret; + } protected virtual TResult RunWithData(bool success, T previousResult) { @@ -782,10 +763,6 @@ public DataTaskBase(CancellationToken token) : base(token) {} - public DataTaskBase(Task task) - : base(task) - {} - public event Action OnData; protected void RaiseOnData(TData data) { @@ -799,10 +776,6 @@ public DataTaskBase(CancellationToken token) : base(token) {} - public DataTaskBase(Task task) - : base(task) - {} - public event Action OnData; protected void RaiseOnData(TData data) { diff --git a/src/GitHub.Api/Tasks/TaskExtensions.cs b/src/GitHub.Api/Tasks/TaskExtensions.cs index 265033576..57e17170d 100644 --- a/src/GitHub.Api/Tasks/TaskExtensions.cs +++ b/src/GitHub.Api/Tasks/TaskExtensions.cs @@ -68,7 +68,7 @@ public static ITask Then(this ITask task, Func public static ITask Then(this ITask task, Task continuation, TaskAffinity affinity = TaskAffinity.Concurrent, TaskRunOptions runOptions = TaskRunOptions.OnSuccess) { - var cont = new FuncTask(continuation) { Affinity = affinity, Name = $"ThenAsync<{typeof(T)}>" }; + var cont = new TPLTask(continuation) { Affinity = affinity, Name = $"ThenAsync<{typeof(T)}>" }; return task.Then(cont, runOptions); } @@ -133,4 +133,4 @@ public static Task StartAsAsync(this ITask task) return tcs.Task; } } -} \ No newline at end of file +} diff --git a/src/tests/IntegrationTests/BaseIntegrationTest.cs b/src/tests/IntegrationTests/BaseIntegrationTest.cs index 6436d4950..24ab03c85 100644 --- a/src/tests/IntegrationTests/BaseIntegrationTest.cs +++ b/src/tests/IntegrationTests/BaseIntegrationTest.cs @@ -147,7 +147,7 @@ protected void SetupGit(NPath pathToSetupGitInto, string testName) var extractPath = tempZipExtractPath.Combine("git").CreateDirectory(); var path = new UnzipTask(TaskManager.Token, installDetails.GitZipPath, extractPath, null, Environment.FileSystem) .Catch(e => true) - .RunWithReturn(true); + .RunSynchronously(); var source = path; installDetails.GitInstallationPath.EnsureParentDirectoryExists(); source.Move(installDetails.GitInstallationPath); @@ -155,7 +155,7 @@ protected void SetupGit(NPath pathToSetupGitInto, string testName) extractPath = tempZipExtractPath.Combine("git-lfs").CreateDirectory(); path = new UnzipTask(TaskManager.Token, installDetails.GitLfsZipPath, extractPath, null, Environment.FileSystem) .Catch(e => true) - .RunWithReturn(true); + .RunSynchronously(); installDetails.GitLfsInstallationPath.EnsureParentDirectoryExists(); path.Move(installDetails.GitLfsInstallationPath); } diff --git a/src/tests/IntegrationTests/Download/DownloadTaskTests.cs b/src/tests/IntegrationTests/Download/DownloadTaskTests.cs index ecb1c44ca..c773f3d1c 100644 --- a/src/tests/IntegrationTests/Download/DownloadTaskTests.cs +++ b/src/tests/IntegrationTests/Download/DownloadTaskTests.cs @@ -29,7 +29,7 @@ public async Task DownloadAndVerificationWorks() var package = Package.Load(Environment, new UriString($"http://localhost:{server.Port}/unity/git/windows/git-lfs.json")); - var downloader = new Downloader(); + var downloader = new Downloader(Environment.FileSystem); downloader.QueueDownload(package.Uri, TestBasePath); StartTrackTime(watch, logger, package.Url); @@ -54,7 +54,7 @@ public async Task DownloadingNonExistingFileThrows() var package = new Package { Url = $"http://localhost:{server.Port}/nope" }; - var downloader = new Downloader(); + var downloader = new Downloader(Environment.FileSystem); StartTrackTime(watch, logger, package.Url); downloader.QueueDownload(package.Uri, TestBasePath); @@ -76,7 +76,7 @@ public async Task FailsIfVerificationFails() var package = new Package { Url = gitPackage.Url, Md5 = gitLfsPackage.Md5 }; - var downloader = new Downloader(); + var downloader = new Downloader(Environment.FileSystem); downloader.QueueDownload(package.Uri, TestBasePath); StartTrackTime(watch, logger, package.Url); @@ -100,7 +100,7 @@ public async Task ResumingWorks() var fileSystem = NPath.FileSystem; var package = Package.Load(Environment, new UriString($"http://localhost:{server.Port}/unity/git/windows/git-lfs.json")); - var downloader = new Downloader(); + var downloader = new Downloader(fileSystem); StartTrackTime(watch, logger, package.Url); downloader.QueueDownload(package.Uri, TestBasePath); @@ -117,7 +117,7 @@ public async Task ResumingWorks() fileSystem.FileDelete(downloadData.File); fileSystem.WriteAllBytes(downloadData + ".partial", cutDownloadPathBytes); - downloader = new Downloader(); + downloader = new Downloader(fileSystem); StartTrackTime(watch, logger, "resuming download"); downloader.QueueDownload(package.Uri, TestBasePath); task = await TaskEx.WhenAny(downloader.Start().Task, TaskEx.Delay(Timeout)); @@ -140,7 +140,7 @@ public async Task SucceedIfEverythingIsAlreadyDownloaded() var fileSystem = NPath.FileSystem; var package = Package.Load(Environment, new UriString($"http://localhost:{server.Port}/unity/git/windows/git-lfs.json")); - var downloader = new Downloader(); + var downloader = new Downloader(fileSystem); StartTrackTime(watch, logger, package.Url); downloader.QueueDownload(package.Uri, TestBasePath); @@ -150,7 +150,7 @@ public async Task SucceedIfEverythingIsAlreadyDownloaded() var downloadData = await downloader.Task; var downloadPath = downloadData.FirstOrDefault().File; - downloader = new Downloader(); + downloader = new Downloader(fileSystem); StartTrackTime(watch, logger, "downloading again"); downloader.QueueDownload(package.Uri, TestBasePath); task = await TaskEx.WhenAny(downloader.Start().Task, TaskEx.Delay(Timeout)); @@ -175,12 +175,12 @@ public async Task DownloadsRunSideBySide() var events = new List(); - var downloader = new Downloader(); + var downloader = new Downloader(Environment.FileSystem); downloader.QueueDownload(package2.Uri, TestBasePath); downloader.QueueDownload(package1.Uri, TestBasePath); - downloader.DownloadStart += d => events.Add("start " + d.Url.Filename); - downloader.DownloadComplete += d => events.Add("end " + d.Url.Filename); - downloader.DownloadFailed += (d, _) => events.Add("failed " + d.Url.Filename); + downloader.OnDownloadStart += url => events.Add("start " + url.Filename); + downloader.OnDownloadComplete += (url, file) => events.Add("end " + url.Filename); + downloader.OnDownloadFailed += (url, ex) => events.Add("failed " + url.Filename); server.Delay = 1; StartTrackTime(watch, logger); diff --git a/src/tests/IntegrationTests/Git/GitClientTests.cs b/src/tests/IntegrationTests/Git/GitClientTests.cs index dacfb01bf..cda2b3f30 100644 --- a/src/tests/IntegrationTests/Git/GitClientTests.cs +++ b/src/tests/IntegrationTests/Git/GitClientTests.cs @@ -25,7 +25,7 @@ public void ShouldGetGitVersion() InitializePlatformAndEnvironment(TestRepoMasterCleanSynchronized); - var result = GitClient.Version().RunWithReturn(true); + var result = GitClient.Version().RunSynchronously(); var expected = TheVersion.Parse("2.17.0"); result.Major.Should().Be(expected.Major); result.Minor.Should().Be(expected.Minor); @@ -40,7 +40,7 @@ public void ShouldGetGitLfsVersion() InitializePlatformAndEnvironment(TestRepoMasterCleanSynchronized); - var result = GitClient.LfsVersion().RunWithReturn(true); + var result = GitClient.LfsVersion().RunSynchronously(); var expected = TheVersion.Parse("2.4.0"); result.Should().Be(expected); } diff --git a/src/tests/IntegrationTests/Installer/GitInstallerTests.cs b/src/tests/IntegrationTests/Installer/GitInstallerTests.cs index 3b215d7fd..8ecefaf9d 100644 --- a/src/tests/IntegrationTests/Installer/GitInstallerTests.cs +++ b/src/tests/IntegrationTests/Installer/GitInstallerTests.cs @@ -25,7 +25,7 @@ public override void OnSetup() public override void TestFixtureSetUp() { base.TestFixtureSetUp(); - server = new TestWebServer.HttpServer(SolutionDirectory.Combine("files")); + server = new TestWebServer.HttpServer(SolutionDirectory.Combine("files"), 50000); Task.Factory.StartNew(server.Start); ApplicationConfiguration.WebTimeout = 10000; } @@ -174,9 +174,9 @@ public void GitLfsIsInstalledIfMissing() var gitLfsInstallationPath = TestBasePath.Combine("GitInstall").Combine(GitInstaller.GitInstallDetails.GitLfsDirectory); var gitZipUri = new UriString($"http://localhost:{server.Port}/unity/git/windows/git-slim.zip"); - var downloader = new Downloader(); + var downloader = new Downloader(Environment.FileSystem); downloader.QueueDownload(gitZipUri, tempZipExtractPath); - downloader.RunWithReturn(true); + downloader.RunSynchronously(); var gitExtractPath = tempZipExtractPath.Combine("git").CreateDirectory(); ZipHelper.Instance.Extract(tempZipExtractPath.Combine(gitZipUri.Filename), gitExtractPath, TaskManager.Token, null); @@ -209,9 +209,9 @@ public void GitLfsIsInstalledIfMissingWithCustomGitPath() var customGitInstall = TestBasePath.Combine("CustomGitInstall").Combine(GitInstaller.GitInstallDetails.GitDirectory); var gitZipUri = new UriString($"http://localhost:{server.Port}/unity/git/windows/git-slim.zip"); - var downloader = new Downloader(); + var downloader = new Downloader(Environment.FileSystem); downloader.QueueDownload(gitZipUri, tempZipExtractPath); - downloader.RunWithReturn(true); + downloader.RunSynchronously(); var gitExtractPath = tempZipExtractPath.Combine("git").CreateDirectory(); ZipHelper.Instance.Extract(tempZipExtractPath.Combine(gitZipUri.Filename), gitExtractPath, TaskManager.Token, null); diff --git a/src/tests/TaskSystemIntegrationTests/Tests.cs b/src/tests/TaskSystemIntegrationTests/Tests.cs index bcec2317c..160c22290 100644 --- a/src/tests/TaskSystemIntegrationTests/Tests.cs +++ b/src/tests/TaskSystemIntegrationTests/Tests.cs @@ -770,7 +770,7 @@ public async Task CanWrapATask() { var runOrder = new List(); var task = new Task(() => runOrder.Add($"ran")); - var act = new ActionTask(task) { Affinity = TaskAffinity.Exclusive }; + var act = new TPLTask(task) { Affinity = TaskAffinity.Exclusive }; await act.Start().Task; CollectionAssert.AreEqual(new string[] { $"ran" }, runOrder); } @@ -816,19 +816,14 @@ public TestActionTask(CancellationToken token, Action action) : base(token, action) {} - public TaskBase Test_GetTopMostTask() + public TaskBase Test_GetFirstStartableTask() { - return base.GetTopMostTask(); - } - - public TaskBase Test_GetTopMostTaskInCreatedState() - { - return base.GetTopMostTaskInCreatedState(); + return base.GetTopMostStartableTask(); } } [Test] - public async Task GetTopMostTaskInCreatedState() + public async Task GetTopOfChain_ReturnsTopMostInCreatedState() { var task1 = new ActionTask(Token, () => { }); await task1.StartAwait(); @@ -837,20 +832,45 @@ public async Task GetTopMostTaskInCreatedState() task1.Then(task2).Then(task3); - var top = task3.Test_GetTopMostTaskInCreatedState(); + var top = task3.GetTopOfChain(); Assert.AreSame(task2, top); } [Test] - public void GetTopMostTask() + public void GetTopOfChain_ReturnsTopTaskWhenNotStarted() { - var task1 = new ActionTask(TaskEx.FromResult(true)); + var task1 = new TPLTask(TaskEx.FromResult(true)); var task2 = new TestActionTask(Token, _ => { }); var task3 = new TestActionTask(Token, _ => { }); task1.Then(task2).Then(task3); - var top = task3.Test_GetTopMostTask(); + var top = task3.GetTopOfChain(); + Assert.AreSame(task1, top); + } + + public async Task GetFirstStartableTask_ReturnsNullWhenItsAlreadyStarted() + { + var task1 = new ActionTask(Token, () => { }); + await task1.StartAwait(); + var task2 = new TestActionTask(Token, _ => { }); + var task3 = new TestActionTask(Token, _ => { }); + + task1.Then(task2).Then(task3); + + var top = task3.Test_GetFirstStartableTask(); + Assert.AreSame(task2, top); + } + + public void GetFirstStartableTask_ReturnsTopTaskWhenNotStarted() + { + var task1 = new ActionTask(Token, () => { }); + var task2 = new TestActionTask(Token, _ => { }); + var task3 = new TestActionTask(Token, _ => { }); + + task1.Then(task2).Then(task3); + + var top = task3.Test_GetFirstStartableTask(); Assert.AreSame(task1, top); } @@ -860,7 +880,7 @@ public async Task MergingTwoChainsWorks() var callOrder = new List(); var dependsOrder = new List(); - var innerChainTask1 = new ActionTask(TaskEx.FromResult(LogAndReturnResult(callOrder, "chain2 completed1", true))); + var innerChainTask1 = new TPLTask(TaskEx.FromResult(LogAndReturnResult(callOrder, "chain2 completed1", true))); var innerChainTask2 = innerChainTask1.Then(_ => { callOrder.Add("chain2 FuncTask"); @@ -958,6 +978,83 @@ private T LogAndReturnResult(List callOrder, string msg, T result) } } + [TestFixture] + class TaskQueueTests : BaseTest + { + [Test] + public void ConvertsTaskResultsCorrectly() + { + var vals = new string[] { "2.1", Math.PI.ToString(), "1" }; + var expected = new double[] { 2.1, Math.PI, 1.0 }; + var queue = new TaskQueue(task => Double.Parse(task.Result)); + vals.All(s => { queue.Queue(new TPLTask(TaskEx.FromResult(s))); return true; }); + var ret = queue.RunSynchronously(); + Assert.AreEqual(expected.Join(","), ret.Join(",")); + } + + [Test] + public void ThrowsIfCannotConvert() + { + Assert.Throws(() => new TaskQueue()); + // NPath has an implicit operator to string, but we cannot verify this without using + // reflection, so a converter is required + Assert.Throws(() => new TaskQueue()); + } + + [Test] + public void DoesNotThrowIfItCanConvert() + { + Assert.DoesNotThrow(() => new TaskQueue()); + } + + [Test] + public void FailingTasksThrowCorrectlyEvenIfFinallyIsPresent() + { + var queue = new TaskQueue(); + var task = new ActionTask(Token, () => throw new Exception()) + .Finally((s, e) => { }); + queue.Queue(task); + Assert.Throws(() => queue.RunSynchronously()); + } + + [Test] + public async Task DoubleSchedulingStartsOnlyOnce() + { + var runOrder = new List(); + var queue = new TaskQueue(); + var task1 = new FuncTask(Token, () => + { + runOrder.Add("1"); + return "2"; + }); + task1.OnStart += _ => runOrder.Add("start 1"); + task1.OnEnd += (a, b, c, d) => runOrder.Add("end 1"); + var task2 = new FuncTask(Token, (_, str) => + { + runOrder.Add(str); + return "3"; + }); + task2.OnStart += _ => runOrder.Add("start 2"); + task2.OnEnd += (a, b, c, d) => runOrder.Add("end 2"); + var task3 = new FuncTask(Token, (_, str) => + { + runOrder.Add(str); + return "4"; + }); + task3.OnStart += _ => runOrder.Add("start 3"); + task3.OnEnd += (a, b, c, d) => runOrder.Add("end 3"); + + queue.Queue(task1.Then(task2).Then(task3)); + await queue.StartAwait(); + var expected = new string[] { + "start 1", "1", "end 1", + "start 2", "2", "end 2", + "start 3", "3", "end 3", + }; + Assert.AreEqual(expected.Join(","), runOrder.Join(",")); + } + } + static class KeyValuePair { public static KeyValuePair Create(TKey key, TValue value) From 869133b66d77141f69adcbe21c2ca66b55183de4 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 15:09:53 +0200 Subject: [PATCH 296/567] Ooops this needs initializing --- src/GitHub.Api/IO/NiceIO.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index 3b6a098d4..bf008660c 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -1113,7 +1113,23 @@ public static IFileSystem FileSystem } } - private static FSWrapper FSWrapper { get; set; } + private static FSWrapper _fsWrapper; + private static FSWrapper FSWrapper + { + get + { + if (_fsWrapper == null) + { + // this will initialize both FileSystem and FSWrapper + var fs = FileSystem; + } + return _fsWrapper; + } + set + { + _fsWrapper = value; + } + } private static bool? _isUnix; internal static bool IsUnix From 83d7da8f94207ea2862005bc8f69d7e2bd265c6a Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 15:17:43 +0200 Subject: [PATCH 297/567] Wow VS 2015, go home, you're drunk --- src/tests/TaskSystemIntegrationTests/Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/TaskSystemIntegrationTests/Tests.cs b/src/tests/TaskSystemIntegrationTests/Tests.cs index 160c22290..d4e5ba3a3 100644 --- a/src/tests/TaskSystemIntegrationTests/Tests.cs +++ b/src/tests/TaskSystemIntegrationTests/Tests.cs @@ -1011,7 +1011,7 @@ public void DoesNotThrowIfItCanConvert() public void FailingTasksThrowCorrectlyEvenIfFinallyIsPresent() { var queue = new TaskQueue(); - var task = new ActionTask(Token, () => throw new Exception()) + var task = new ActionTask(Token, () => { throw new Exception(); }) .Finally((s, e) => { }); queue.Queue(task); Assert.Throws(() => queue.RunSynchronously()); From 7c66dde0a98d6f303807da1011ca272cfffe6bd9 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 15:35:05 +0200 Subject: [PATCH 298/567] No need to fire off another thread for this --- src/GitHub.Api/Application/ApplicationManagerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 97465f18d..25c564c35 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -317,7 +317,7 @@ private void CaptureRepoSize() diskUsageTask .Configure(ProcessManager) - .Then((success, kilobytes) => + .Finally((success, kilobytes) => { if (success) { From cf1f9bbcae54a48fbfd3958447e768472df2a627 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 1 Jun 2018 10:03:04 -0400 Subject: [PATCH 299/567] Updating submodule --- script | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script b/script index bab459e1e..259dba7e8 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit bab459e1eefbb6b9ec954c84b281099da1200e0d +Subproject commit 259dba7e8375a96a935b51e2169fe029cd70c039 From 05bc1bfdf809bfd03d8970bc39f4ce6777f5ce8a Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 16:28:09 +0200 Subject: [PATCH 300/567] Make sure we always call UsageTracker off the main thread --- .../Application/ApplicationManagerBase.cs | 18 ++-- src/GitHub.Api/Metrics/UsageTracker.cs | 100 ++++++++++++------ src/GitHub.Api/Tasks/ITaskManager.cs | 4 +- src/GitHub.Api/Tasks/TaskManager.cs | 4 +- .../GitHub.Unity/UI/AuthenticationView.cs | 2 +- .../Editor/GitHub.Unity/UI/BranchesView.cs | 8 +- .../Editor/GitHub.Unity/UI/ChangesView.cs | 2 +- .../Editor/GitHub.Unity/UI/LocksView.cs | 4 +- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 6 +- .../Editor/GitHub.Unity/UI/PublishView.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 8 +- .../IntegrationTests/Metrics/MetricsTests.cs | 6 +- 12 files changed, 95 insertions(+), 69 deletions(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 25c564c35..25a44bddd 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -47,7 +47,7 @@ protected void Initialize() ApplicationConfiguration.WebTimeout = UserSettings.Get(Constants.WebTimeoutKey, ApplicationConfiguration.WebTimeout); Platform.Initialize(ProcessManager, TaskManager); progress.OnProgress += progressReporter.UpdateProgress; - UsageTracker = new UsageTracker(UserSettings, Environment, InstanceId.ToString()); + UsageTracker = new UsageTracker(TaskManager, UserSettings, Environment, InstanceId.ToString()); #if ENABLE_METRICS var metricsService = new MetricsService(ProcessManager, @@ -64,12 +64,16 @@ public void Run() isBusy = true; progress.UpdateProgress(0, 100, "Initializing..."); + if (firstRun) + { + UsageTracker.IncrementNumberOfStartups(); + } + var thread = new Thread(() => { GitInstallationState state = new GitInstallationState(); try { - SetupMetrics(); if (Environment.IsMac) { var getEnvPath = new SimpleProcessTask(TaskManager.Token, "bash".ToNPath(), "-c \"/usr/libexec/path_helper\"") @@ -256,7 +260,6 @@ public void InitializeRepository() GitClient.Commit("Initial commit", null).RunWithReturn(true); progress.UpdateProgress(70, 100, "Initializing..."); Environment.InitializeRepository(); - UsageTracker.IncrementProjectsInitialized(); } catch (Exception ex) { @@ -270,6 +273,7 @@ public void InitializeRepository() progress.UpdateProgress(90, 100, "Initializing..."); RestartRepository(); TaskManager.RunInUI(InitializeUI); + UsageTracker.IncrementProjectsInitialized(); progress.UpdateProgress(100, 100, "Initialized"); } isBusy = false; @@ -342,14 +346,6 @@ public void RestartRepository() Logger.Trace($"Got a repository? {(Environment.Repository != null ? Environment.Repository.LocalPath : "null")}"); } - protected void SetupMetrics() - { - if (firstRun) - { - UsageTracker.IncrementNumberOfStartups(); - } - } - protected abstract void InitializeUI(); protected abstract void InitializationComplete(); diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index ec64f3e32..66bcd0150 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -6,7 +6,7 @@ namespace GitHub.Unity { - class UsageTracker : IUsageTracker + class UsageTrackerSync : IUsageTracker { private static ILogging Logger { get; } = LogHelper.GetLogger(); @@ -22,16 +22,7 @@ class UsageTracker : IUsageTracker public IMetricsService MetricsService { get; set; } - public UsageTracker(ISettings userSettings, - IEnvironment environment, string instanceId) - : this(userSettings, - new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), - environment.UnityVersion, instanceId) - { - } - - public UsageTracker(ISettings userSettings, - IUsageLoader usageLoader, + public UsageTrackerSync(ISettings userSettings, IUsageLoader usageLoader, string unityVersion, string instanceId) { this.userSettings = userSettings; @@ -123,7 +114,7 @@ private void SendUsage() } } - public void IncrementNumberOfStartups() + public virtual void IncrementNumberOfStartups() { lock (_lock) { @@ -134,7 +125,7 @@ public void IncrementNumberOfStartups() } } - public void IncrementProjectsInitialized() + public virtual void IncrementProjectsInitialized() { lock (_lock) { @@ -145,7 +136,7 @@ public void IncrementProjectsInitialized() } } - public void IncrementChangesViewButtonCommit() + public virtual void IncrementChangesViewButtonCommit() { lock (_lock) { @@ -156,7 +147,7 @@ public void IncrementChangesViewButtonCommit() } } - public void IncrementHistoryViewToolbarFetch() + public virtual void IncrementHistoryViewToolbarFetch() { lock (_lock) { @@ -167,7 +158,7 @@ public void IncrementHistoryViewToolbarFetch() } } - public void IncrementHistoryViewToolbarPush() + public virtual void IncrementHistoryViewToolbarPush() { lock (_lock) { @@ -178,7 +169,7 @@ public void IncrementHistoryViewToolbarPush() } } - public void IncrementHistoryViewToolbarPull() + public virtual void IncrementHistoryViewToolbarPull() { lock (_lock) { @@ -189,7 +180,7 @@ public void IncrementHistoryViewToolbarPull() } } - public void IncrementBranchesViewButtonCreateBranch() + public virtual void IncrementBranchesViewButtonCreateBranch() { lock (_lock) { @@ -200,7 +191,7 @@ public void IncrementBranchesViewButtonCreateBranch() } } - public void IncrementBranchesViewButtonDeleteBranch() + public virtual void IncrementBranchesViewButtonDeleteBranch() { lock (_lock) { @@ -211,7 +202,7 @@ public void IncrementBranchesViewButtonDeleteBranch() } } - public void IncrementBranchesViewButtonCheckoutLocalBranch() + public virtual void IncrementBranchesViewButtonCheckoutLocalBranch() { lock (_lock) { @@ -222,7 +213,7 @@ public void IncrementBranchesViewButtonCheckoutLocalBranch() } } - public void IncrementBranchesViewButtonCheckoutRemoteBranch() + public virtual void IncrementBranchesViewButtonCheckoutRemoteBranch() { lock (_lock) { @@ -233,7 +224,7 @@ public void IncrementBranchesViewButtonCheckoutRemoteBranch() } } - public void IncrementSettingsViewButtonLfsUnlock() + public virtual void IncrementSettingsViewButtonLfsUnlock() { lock (_lock) { @@ -244,7 +235,7 @@ public void IncrementSettingsViewButtonLfsUnlock() } } - public void IncrementAuthenticationViewButtonAuthentication() + public virtual void IncrementAuthenticationViewButtonAuthentication() { lock (_lock) { @@ -255,7 +246,7 @@ public void IncrementAuthenticationViewButtonAuthentication() } } - public void IncrementUnityProjectViewContextLfsLock() + public virtual void IncrementUnityProjectViewContextLfsLock() { lock (_lock) { @@ -266,7 +257,7 @@ public void IncrementUnityProjectViewContextLfsLock() } } - public void IncrementUnityProjectViewContextLfsUnlock() + public virtual void IncrementUnityProjectViewContextLfsUnlock() { lock (_lock) { @@ -277,7 +268,7 @@ public void IncrementUnityProjectViewContextLfsUnlock() } } - public void IncrementPublishViewButtonPublish() + public virtual void IncrementPublishViewButtonPublish() { lock (_lock) { @@ -288,7 +279,7 @@ public void IncrementPublishViewButtonPublish() } } - public void IncrementApplicationMenuMenuItemCommandLine() + public virtual void IncrementApplicationMenuMenuItemCommandLine() { lock (_lock) { @@ -299,18 +290,24 @@ public void IncrementApplicationMenuMenuItemCommandLine() } } - public void UpdateRepoSize(int kilobytes) + public virtual void UpdateRepoSize(int kilobytes) { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).GitRepoSize = kilobytes; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).GitRepoSize = kilobytes; + usageLoader.Save(usage); + } } - public void UpdateLfsDiskUsage(int kilobytes) + public virtual void UpdateLfsDiskUsage(int kilobytes) { - var usage = usageLoader.Load(userId); - usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).LfsDiskUsage = kilobytes; - usageLoader.Save(usage); + lock (_lock) + { + var usage = usageLoader.Load(userId); + usage.GetCurrentMeasures(appVersion, unityVersion, instanceId).LfsDiskUsage = kilobytes; + usageLoader.Save(usage); + } } public bool Enabled @@ -337,6 +334,39 @@ public bool Enabled } } + class UsageTracker : UsageTrackerSync + { + public UsageTracker(ITaskManager taskManager, ISettings userSettings, + IEnvironment environment, string instanceId) + : base(userSettings, + new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), + environment.UnityVersion, instanceId) + { + TaskManager = taskManager; + } + + public override void IncrementApplicationMenuMenuItemCommandLine() => TaskManager.Run(base.IncrementApplicationMenuMenuItemCommandLine); + public override void IncrementAuthenticationViewButtonAuthentication() => TaskManager.Run(base.IncrementAuthenticationViewButtonAuthentication); + public override void IncrementBranchesViewButtonCheckoutLocalBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCheckoutLocalBranch); + public override void IncrementBranchesViewButtonCheckoutRemoteBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCheckoutRemoteBranch); + public override void IncrementBranchesViewButtonCreateBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCreateBranch); + public override void IncrementBranchesViewButtonDeleteBranch() => TaskManager.Run(base.IncrementBranchesViewButtonDeleteBranch); + public override void IncrementChangesViewButtonCommit() => TaskManager.Run(base.IncrementChangesViewButtonCommit); + public override void IncrementHistoryViewToolbarFetch() => TaskManager.Run(base.IncrementHistoryViewToolbarFetch); + public override void IncrementHistoryViewToolbarPull() => TaskManager.Run(base.IncrementHistoryViewToolbarPull); + public override void IncrementHistoryViewToolbarPush() => TaskManager.Run(base.IncrementHistoryViewToolbarPush); + public override void IncrementNumberOfStartups() => TaskManager.Run(base.IncrementNumberOfStartups); + public override void IncrementProjectsInitialized() => TaskManager.Run(base.IncrementProjectsInitialized); + public override void IncrementPublishViewButtonPublish() => TaskManager.Run(base.IncrementPublishViewButtonPublish); + public override void IncrementSettingsViewButtonLfsUnlock() => TaskManager.Run(base.IncrementSettingsViewButtonLfsUnlock); + public override void IncrementUnityProjectViewContextLfsLock() => TaskManager.Run(base.IncrementUnityProjectViewContextLfsLock); + public override void IncrementUnityProjectViewContextLfsUnlock() => TaskManager.Run(base.IncrementUnityProjectViewContextLfsUnlock); + public override void UpdateLfsDiskUsage(int kilobytes) => TaskManager.Run(() => base.UpdateLfsDiskUsage(kilobytes)); + public override void UpdateRepoSize(int kilobytes) => TaskManager.Run(() => base.UpdateRepoSize(kilobytes)); + + protected ITaskManager TaskManager { get; } + } + interface IUsageLoader { UsageStore Load(string userId); diff --git a/src/GitHub.Api/Tasks/ITaskManager.cs b/src/GitHub.Api/Tasks/ITaskManager.cs index 35ed2cea8..d5587872d 100644 --- a/src/GitHub.Api/Tasks/ITaskManager.cs +++ b/src/GitHub.Api/Tasks/ITaskManager.cs @@ -13,8 +13,8 @@ public interface ITaskManager : IDisposable T Schedule(T task) where T : ITask; Task Wait(); - ITask Run(Action action, string message); + ITask Run(Action action, string message = null); ITask RunInUI(Action action); event Action OnProgress; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/TaskManager.cs b/src/GitHub.Api/Tasks/TaskManager.cs index 98a9f185e..5bff94012 100644 --- a/src/GitHub.Api/Tasks/TaskManager.cs +++ b/src/GitHub.Api/Tasks/TaskManager.cs @@ -58,7 +58,7 @@ public static TaskScheduler GetScheduler(TaskAffinity affinity) } } - public ITask Run(Action action, string message) + public ITask Run(Action action, string message = null) { return new ActionTask(Token, action) { Message = message }.Start(); } @@ -167,4 +167,4 @@ public void Dispose() Dispose(true); } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 9436f0779..0b7c55c9f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -209,7 +209,7 @@ private void DoResult(bool success, string msg) isBusy = false; if (success) { - TaskManager.Run(UsageTracker.IncrementAuthenticationViewButtonAuthentication, null); + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); Clear(); Finish(true); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index 9ebb344b9..aad002371 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -319,7 +319,7 @@ private void OnButtonBarGUI() { if (success) { - TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCreateBranch, null); + UsageTracker.IncrementBranchesViewButtonCreateBranch(); Redraw(); } else @@ -489,7 +489,7 @@ private void CheckoutRemoteBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCheckoutRemoteBranch, null); + UsageTracker.IncrementBranchesViewButtonCheckoutRemoteBranch(); Redraw(); } else @@ -512,7 +512,7 @@ private void SwitchBranch(string branch) { if (success) { - TaskManager.Run(UsageTracker.IncrementBranchesViewButtonCheckoutLocalBranch, null); + UsageTracker.IncrementBranchesViewButtonCheckoutLocalBranch(); Redraw(); } else @@ -530,7 +530,7 @@ private void DeleteLocalBranch(string branch) if (EditorUtility.DisplayDialog(DeleteBranchTitle, dialogMessage, DeleteBranchButton, CancelButtonLabel)) { Repository.DeleteBranch(branch, true) - .Then(UsageTracker.IncrementBranchesViewButtonDeleteBranch) + .Finally(s => { if (s) UsageTracker.IncrementBranchesViewButtonDeleteBranch(); } ) .Start(); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 600f8d134..7b0022a34 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -405,7 +405,7 @@ private void Commit() { if (success) { - TaskManager.Run(UsageTracker.IncrementChangesViewButtonCommit, null); + UsageTracker.IncrementChangesViewButtonCommit(); commitMessage = ""; commitBody = ""; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 6ba86b54f..46ce96a12 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -466,7 +466,7 @@ private void UnlockSelectedEntry() { if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + Manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock(); } else { @@ -492,7 +492,7 @@ private void ForceUnlockSelectedEntry() { if (success) { - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + Manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock(); } else { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 4cb27e686..5eb649890 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -120,7 +120,7 @@ private static void ContextMenu_Lock() { if (success) { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsLock, null); + manager.UsageTracker.IncrementUnityProjectViewContextLfsLock(); } else { @@ -172,7 +172,7 @@ private static void ContextMenu_Unlock() { if (success) { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock(); } else { @@ -224,7 +224,7 @@ private static void ContextMenu_UnlockForce() { if (success) { - manager.TaskManager.Run(manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock, null); + manager.UsageTracker.IncrementUnityProjectViewContextLfsUnlock(); } else { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index 16822aca5..65ef5fb71 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -168,7 +168,7 @@ public override void OnGUI() return; } - TaskManager.Run(UsageTracker.IncrementPublishViewButtonPublish, null); + UsageTracker.IncrementPublishViewButtonPublish(); if (repository == null) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index faaf926e5..d25c08066 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -65,7 +65,7 @@ public static void Window_GitHub() public static void GitHub_CommandLine() { EntryPoint.ApplicationManager.ProcessManager.RunCommandLineWindow(NPath.CurrentDirectory); - EntryPoint.ApplicationManager.TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementApplicationMenuMenuItemCommandLine, null); + EntryPoint.ApplicationManager.UsageTracker.IncrementApplicationMenuMenuItemCommandLine(); } #if DEBUG @@ -682,7 +682,7 @@ private void Pull() if (success) { SetProgressMessage(Localization.MessagePulled, 100); - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarPull, null); + Manager.UsageTracker.IncrementHistoryViewToolbarPull(); EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), @@ -710,7 +710,7 @@ private void Push() if (success) { SetProgressMessage(Localization.MessagePushed, 100); - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarPush, null); + Manager.UsageTracker.IncrementHistoryViewToolbarPush(); EditorUtility.DisplayDialog(Localization.PushActionTitle, String.Format(Localization.PushSuccessDescription, currentRemoteName), @@ -737,7 +737,7 @@ private void Fetch() if (success) { SetProgressMessage(Localization.MessageFetched, 100); - TaskManager.Run(EntryPoint.ApplicationManager.UsageTracker.IncrementHistoryViewToolbarFetch, null); + Manager.UsageTracker.IncrementHistoryViewToolbarFetch(); } else { diff --git a/src/tests/IntegrationTests/Metrics/MetricsTests.cs b/src/tests/IntegrationTests/Metrics/MetricsTests.cs index 4f20328fb..3d7fa1bdb 100644 --- a/src/tests/IntegrationTests/Metrics/MetricsTests.cs +++ b/src/tests/IntegrationTests/Metrics/MetricsTests.cs @@ -41,7 +41,7 @@ public void IncrementMetricsWorks(string measureName) usageStore.Model.Guid = userId; usageLoader.Load(Arg.Is(userId)).Returns(usageStore); - var usageTracker = new UsageTracker(settings, usageLoader, unityVersion, instanceId); + var usageTracker = new UsageTrackerSync(settings, usageLoader, unityVersion, instanceId); var currentUsage = usageStore.GetCurrentMeasures(appVersion, unityVersion, instanceId); var prop = currentUsage.GetType().GetProperty(measureName); @@ -68,7 +68,7 @@ public void LoadingWorks() var settings = Substitute.For(); settings.Exists(Arg.Is(Constants.GuidKey)).Returns(true); settings.Get(Arg.Is(Constants.GuidKey)).Returns(userId); - var usageTracker = new UsageTracker(settings, usageLoader, unityVersion, instanceId); + var usageTracker = new UsageTrackerSync(settings, usageLoader, unityVersion, instanceId); usageTracker.IncrementNumberOfStartups(); usageTracker.IncrementNumberOfStartups(); @@ -96,7 +96,7 @@ public void SubmissionWorks() var settings = Substitute.For(); settings.Exists(Arg.Is(Constants.GuidKey)).Returns(true); settings.Get(Arg.Is(Constants.GuidKey)).Returns(userId); - var usageTracker = new UsageTracker(settings, usageLoader, unityVersion, instanceId); + var usageTracker = new UsageTrackerSync(settings, usageLoader, unityVersion, instanceId); usageTracker.IncrementNumberOfStartups(); usageTracker.IncrementNumberOfStartups(); From 62aa7c115d889ac9ba534537399f69f95cde41c9 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 16:32:56 +0200 Subject: [PATCH 301/567] Add support for serializing UriString to json as a simple string --- src/GitHub.Api/Helpers/SimpleJson.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index f664adfff..2a0dafd78 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -1374,6 +1374,8 @@ public virtual object DeserializeObject(object value, Type type) return DateTimeOffset.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) return new Guid(str); + if (type == typeof(UriString) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(UriString))) + return new UriString(str); if (type == typeof(Uri)) { bool isValid = Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute); @@ -1499,7 +1501,7 @@ protected virtual object SerializeEnum(Enum p) protected virtual bool TrySerializeKnownTypes(object input, out object output) { bool returnValue = true; - if (input is NPath) + if (input is NPath || input is UriString) output = input.ToString(); else if (input is DateTime) output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); From 17621b9d0764b9f216279471830b4d1666d6dd62 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 16:30:29 +0200 Subject: [PATCH 302/567] We really don't need System.Data or System.Xml references --- src/GitHub.Api/GitHub.Api.csproj | 4 ---- src/GitHub.Api/Primitives/StringEquivalent.cs | 20 +------------------ src/GitHub.Logging/GitHub.Logging.csproj | 4 ---- .../Editor/GitHub.Unity/GitHub.Unity.csproj | 4 ---- .../Editor/UnityTests/UnityTests.csproj | 4 ---- 5 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 30462a466..afaf9e19a 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -80,10 +80,6 @@ $(SolutionDir).\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll True - - - - diff --git a/src/GitHub.Api/Primitives/StringEquivalent.cs b/src/GitHub.Api/Primitives/StringEquivalent.cs index 5dd82b3c7..5e360e2c5 100644 --- a/src/GitHub.Api/Primitives/StringEquivalent.cs +++ b/src/GitHub.Api/Primitives/StringEquivalent.cs @@ -1,14 +1,11 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; namespace GitHub.Unity { [Serializable] - public abstract class StringEquivalent : ISerializable, IXmlSerializable where T : StringEquivalent + public abstract class StringEquivalent : ISerializable where T : StringEquivalent { protected string Value; @@ -86,21 +83,6 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte info.AddValue("Value", Value); } - public XmlSchema GetSchema() - { - return null; - } - - public void ReadXml(XmlReader reader) - { - Value = reader.ReadString(); - } - - public void WriteXml(XmlWriter writer) - { - writer.WriteString(Value); - } - public int Length { get { return Value != null ? Value.Length : 0; } diff --git a/src/GitHub.Logging/GitHub.Logging.csproj b/src/GitHub.Logging/GitHub.Logging.csproj index dbe4582e5..ef69bfeac 100644 --- a/src/GitHub.Logging/GitHub.Logging.csproj +++ b/src/GitHub.Logging/GitHub.Logging.csproj @@ -53,10 +53,6 @@ - - - - diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index 1fa8438aa..33b95c8ba 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -58,10 +58,6 @@ $(SolutionDir)\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll True - - - - $(UnityDir)Managed\UnityEditor.dll False diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj index 7476be989..63cceb65d 100644 --- a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.csproj @@ -40,10 +40,6 @@ - - - - $(UnityDir)Managed\UnityEditor.dll From ae8fda9b514827b9e744e2ca710e4521cb579296 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 17:59:56 +0200 Subject: [PATCH 303/567] Our node list is not a tree, fix duplicate recursiveness --- src/GitHub.Api/UI/TreeBase.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 7305f3c7f..d3cfb88d1 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -325,17 +325,11 @@ private List GetLeafNodes(TNode node, int idx) for (var i = idx + 1; i < Nodes.Count && node.Level < Nodes[i].Level; i++) { var childNode = Nodes[i]; - if (childNode.IsFolder) - { - var leafNodes = GetLeafNodes(childNode, i); - results.AddRange(leafNodes); - } - else + if (!childNode.IsFolder) { results.Add(childNode); } } - return results; } From 68d700ed82c3f6b6c06d4b3abc01539790d37912 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 18:00:16 +0200 Subject: [PATCH 304/567] Fix diffing for new and deleted files --- .../Editor/GitHub.Unity/UI/ChangesView.cs | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs index 9c83217b9..a8953397a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesView.cs @@ -280,18 +280,32 @@ private ITask CalculateFileDiff(ChangesTreeNode node) var tmpDir = Manager.Environment.UnityProjectPath.Combine("Temp", "ghu-diffs").EnsureDirectoryExists(); var leftFile = tmpDir.Combine(rightFile.FileName + "_" + Repository.CurrentHead + rightFile.ExtensionWithDot); return new SimpleProcessTask(TaskManager.Token, "show HEAD:\"" + rightFile.ToString(SlashMode.Forward) + "\"") - .Configure(Manager.ProcessManager, false) - .Catch(_ => true) - .Then((success, txt) => - { - if (success) - leftFile.WriteAllText(txt); - else - leftFile = NPath.Default; - if (!rightFile.FileExists()) - rightFile = NPath.Default; - return new NPath[] { leftFile, rightFile }; - }); + .Configure(Manager.ProcessManager, false) + .Catch(_ => true) + .Then((success, txt) => + { + // both files exist, just compare them + if (success && rightFile.FileExists()) + { + leftFile.WriteAllText(txt); + return new NPath[] { leftFile, rightFile }; + } + + var leftFolder = tmpDir.Combine("left", leftFile.FileName).EnsureDirectoryExists(); + var rightFolder = tmpDir.Combine("right", leftFile.FileName).EnsureDirectoryExists(); + // file was deleted + if (!rightFile.FileExists()) + { + leftFolder.Combine(rightFile).WriteAllText(txt); + } + + // file was created + if (!success) + { + rightFolder.Combine(rightFile).WriteAllText(rightFile.ReadAllText()); + } + return new NPath[] { leftFolder, rightFolder }; + }); } private void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdateEvent) From 6425e3767d1ce5fd086a297abcfc23ca67377353 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 18:44:47 +0200 Subject: [PATCH 305/567] Fix package assets --- unity/PackageProject/Assets/Default.unity | 243 ------------------ .../PackageProject/Assets/Default.unity.meta | 8 - .../GitHub/Editor}/big-logo@2x.png | 0 .../GitHub/Editor}/big-logo@2x.png.meta | 8 +- 4 files changed, 4 insertions(+), 255 deletions(-) delete mode 100644 unity/PackageProject/Assets/Default.unity delete mode 100644 unity/PackageProject/Assets/Default.unity.meta rename unity/PackageProject/Assets/{ => Plugins/GitHub/Editor}/big-logo@2x.png (100%) rename unity/PackageProject/Assets/{ => Plugins/GitHub/Editor}/big-logo@2x.png.meta (96%) diff --git a/unity/PackageProject/Assets/Default.unity b/unity/PackageProject/Assets/Default.unity deleted file mode 100644 index 2f2478ff4..000000000 --- a/unity/PackageProject/Assets/Default.unity +++ /dev/null @@ -1,243 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!29 &1 -OcclusionCullingSettings: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_OcclusionBakeSettings: - smallestOccluder: 5 - smallestHole: 0.25 - backfaceThreshold: 100 - m_SceneGUID: 00000000000000000000000000000000 - m_OcclusionCullingData: {fileID: 0} ---- !u!104 &2 -RenderSettings: - m_ObjectHideFlags: 0 - serializedVersion: 7 - m_Fog: 0 - m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} - m_FogMode: 3 - m_FogDensity: 0.01 - m_LinearFogStart: 0 - m_LinearFogEnd: 300 - m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} - m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} - m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} - m_AmbientIntensity: 1 - m_AmbientMode: 0 - m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} - m_HaloStrength: 0.5 - m_FlareStrength: 1 - m_FlareFadeSpeed: 3 - m_HaloTexture: {fileID: 0} - m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} - m_DefaultReflectionMode: 0 - m_DefaultReflectionResolution: 128 - m_ReflectionBounces: 1 - m_ReflectionIntensity: 1 - m_CustomReflection: {fileID: 0} - m_Sun: {fileID: 0} - m_IndirectSpecularColor: {r: 0.44657898, g: 0.4964133, b: 0.5748178, a: 1} ---- !u!157 &3 -LightmapSettings: - m_ObjectHideFlags: 0 - serializedVersion: 7 - m_GIWorkflowMode: 0 - m_GISettings: - serializedVersion: 2 - m_BounceScale: 1 - m_IndirectOutputScale: 1 - m_AlbedoBoost: 1 - m_TemporalCoherenceThreshold: 1 - m_EnvironmentLightingMode: 0 - m_EnableBakedLightmaps: 1 - m_EnableRealtimeLightmaps: 1 - m_LightmapEditorSettings: - serializedVersion: 4 - m_Resolution: 2 - m_BakeResolution: 40 - m_TextureWidth: 1024 - m_TextureHeight: 1024 - m_AO: 0 - m_AOMaxDistance: 1 - m_CompAOExponent: 1 - m_CompAOExponentDirect: 0 - m_Padding: 2 - m_LightmapParameters: {fileID: 0} - m_LightmapsBakeMode: 1 - m_TextureCompression: 1 - m_DirectLightInLightProbes: 1 - m_FinalGather: 0 - m_FinalGatherFiltering: 1 - m_FinalGatherRayCount: 256 - m_ReflectionCompression: 2 - m_LightingDataAsset: {fileID: 0} - m_RuntimeCPUUsage: 25 ---- !u!196 &4 -NavMeshSettings: - serializedVersion: 2 - m_ObjectHideFlags: 0 - m_BuildSettings: - serializedVersion: 2 - agentTypeID: 0 - agentRadius: 0.5 - agentHeight: 2 - agentSlope: 45 - agentClimb: 0.4 - ledgeDropHeight: 0 - maxJumpAcrossDistance: 0 - minRegionArea: 2 - manualCellSize: 0 - cellSize: 0.16666667 - accuratePlacement: 0 - m_NavMeshData: {fileID: 0} ---- !u!1 &525953239 -GameObject: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - serializedVersion: 5 - m_Component: - - component: {fileID: 525953241} - - component: {fileID: 525953240} - m_Layer: 0 - m_Name: Directional Light - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!108 &525953240 -Light: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 525953239} - m_Enabled: 1 - serializedVersion: 7 - m_Type: 1 - m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} - m_Intensity: 1 - m_Range: 10 - m_SpotAngle: 30 - m_CookieSize: 10 - m_Shadows: - m_Type: 2 - m_Resolution: -1 - m_CustomResolution: -1 - m_Strength: 1 - m_Bias: 0.05 - m_NormalBias: 0.4 - m_NearPlane: 0.2 - m_Cookie: {fileID: 0} - m_DrawHalo: 0 - m_Flare: {fileID: 0} - m_RenderMode: 0 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_Lightmapping: 4 - m_AreaSize: {x: 1, y: 1} - m_BounceIntensity: 1 - m_ShadowRadius: 0 - m_ShadowAngle: 0 ---- !u!4 &525953241 -Transform: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 525953239} - m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} - m_LocalPosition: {x: 0, y: 3, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 1 - m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} ---- !u!1 &596041789 -GameObject: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - serializedVersion: 5 - m_Component: - - component: {fileID: 596041794} - - component: {fileID: 596041793} - - component: {fileID: 596041792} - - component: {fileID: 596041791} - - component: {fileID: 596041790} - m_Layer: 0 - m_Name: Main Camera - m_TagString: MainCamera - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!81 &596041790 -AudioListener: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 596041789} - m_Enabled: 1 ---- !u!124 &596041791 -Behaviour: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 596041789} - m_Enabled: 1 ---- !u!92 &596041792 -Behaviour: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 596041789} - m_Enabled: 1 ---- !u!20 &596041793 -Camera: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 596041789} - m_Enabled: 1 - serializedVersion: 2 - m_ClearFlags: 1 - m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} - m_NormalizedViewPortRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 1 - height: 1 - near clip plane: 0.3 - far clip plane: 1000 - field of view: 60 - orthographic: 0 - orthographic size: 5 - m_Depth: -1 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingPath: -1 - m_TargetTexture: {fileID: 0} - m_TargetDisplay: 0 - m_TargetEye: 3 - m_HDR: 0 - m_OcclusionCulling: 1 - m_StereoConvergence: 10 - m_StereoSeparation: 0.022 - m_StereoMirrorMode: 0 ---- !u!4 &596041794 -Transform: - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 596041789} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 1, z: -10} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 0} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/unity/PackageProject/Assets/Default.unity.meta b/unity/PackageProject/Assets/Default.unity.meta deleted file mode 100644 index 9ec15c833..000000000 --- a/unity/PackageProject/Assets/Default.unity.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: ac725c942578f7543b8c5961b040c01b -timeCreated: 1491392921 -licenseType: Pro -DefaultImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity/PackageProject/Assets/big-logo@2x.png b/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png similarity index 100% rename from unity/PackageProject/Assets/big-logo@2x.png rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png diff --git a/unity/PackageProject/Assets/big-logo@2x.png.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png.meta similarity index 96% rename from unity/PackageProject/Assets/big-logo@2x.png.meta rename to unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png.meta index 52c1f7bf2..2f3ab2e87 100644 --- a/unity/PackageProject/Assets/big-logo@2x.png.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png.meta @@ -102,7 +102,7 @@ TextureImporter: serializedVersion: 2 sprites: [] outline: [] - spritePackingTag: - userData: - assetBundleName: - assetBundleVariant: + spritePackingTag: + userData: + assetBundleName: + assetBundleVariant: From 6cc72b76dc80d822f56c92d0afa9ae72fe025c0c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 19:08:14 +0200 Subject: [PATCH 306/567] Sigh, why were these not tracked --- unity/.gitignore | 4 - unity/PackageProject/.gitignore | 2 - .../GitHub/Editor/GitHub.Unity.dll.mdb.meta | 8 ++ .../GitHub/Editor/GitHub.Unity.dll.meta | 34 ++++++ .../Plugins/GitHub/Editor/libsfw.bundle.meta | 106 ++++++++++++++++++ .../Plugins/GitHub/Editor/libsfw.so.meta | 106 ++++++++++++++++++ 6 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.mdb.meta create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta diff --git a/unity/.gitignore b/unity/.gitignore index 4069ed5b2..87ec145f8 100644 --- a/unity/.gitignore +++ b/unity/.gitignore @@ -1,7 +1,3 @@ -GitHub.Unity.dll* -GitHub.Unity.pdb* -GitHub.Unity.mdb* - [Ll]ibrary/ [Tt]emp/ [Oo]bj/ diff --git a/unity/PackageProject/.gitignore b/unity/PackageProject/.gitignore index 42cb35a28..01498ea52 100644 --- a/unity/PackageProject/.gitignore +++ b/unity/PackageProject/.gitignore @@ -14,7 +14,5 @@ ProjectVersion.txt Library/ // These files come from lib/ -Assets/Plugins/GitHub/Editor/libsfw.bundle.meta -Assets/Plugins/GitHub/Editor/libsfw.so.meta Assets/Plugins/GitHub/Editor/x64/ Assets/Plugins/GitHub/Editor/x86/ \ No newline at end of file diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.mdb.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.mdb.meta new file mode 100644 index 000000000..312a6c06f --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.mdb.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2590b98e5cafc4e32b61170d7b917ee6 +timeCreated: 1527097373 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta new file mode 100644 index 000000000..2187c3a65 --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 68c7e4565cde54155bb78d8e935f1dd4 +timeCreated: 1527097377 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta new file mode 100644 index 000000000..907426232 --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 636d33ae594884e7d80b569f429d245d +timeCreated: 1503667182 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXIntel: 1 + Exclude OSXIntel64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + data: + first: + '': Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + OS: OSX + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: x86 + data: + first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: x86_64 + data: + first: + Standalone: OSXIntel + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: OSXIntel64 + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta new file mode 100644 index 000000000..f5ba9573e --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta @@ -0,0 +1,106 @@ +fileFormatVersion: 2 +guid: 21206c65839f84d0e9ae14bc1fdc68db +timeCreated: 1503931807 +licenseType: Pro +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + '': Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux: 1 + Exclude Linux64: 1 + Exclude LinuxUniversal: 1 + Exclude OSXIntel: 1 + Exclude OSXIntel64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + data: + first: + '': Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + OS: Linux + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Facebook: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Facebook: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: Linux + second: + enabled: 0 + settings: + CPU: x86 + data: + first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: x86_64 + data: + first: + Standalone: OSXIntel + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: OSXIntel64 + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: AnyCPU + data: + first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: From 38f85a549d0dd73aa7cb7fe9fee27ac759b0286f Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 20:12:44 +0200 Subject: [PATCH 307/567] Remove the last traces of async/await in the codebase (excluding tests) --- src/GitHub.Api/Application/ApiClient.cs | 244 +++++++++--------- src/GitHub.Api/Application/IApiClient.cs | 13 +- .../Authentication/ICredentialManager.cs | 8 +- src/GitHub.Api/Authentication/IKeychain.cs | 9 +- .../Authentication/ILoginManager.cs | 6 +- src/GitHub.Api/Authentication/Keychain.cs | 25 +- src/GitHub.Api/Authentication/LoginManager.cs | 32 ++- src/GitHub.Api/Git/GitCredentialManager.cs | 31 ++- .../Services/AuthenticationService.cs | 4 +- .../GitHub.Unity/UI/AuthenticationView.cs | 2 +- .../UnitTests/Authentication/KeychainTests.cs | 20 +- 11 files changed, 187 insertions(+), 207 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 7010dfad8..cac8b5418 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using GitHub.Logging; using System.Runtime.Serialization; using System.Text; @@ -45,115 +44,17 @@ public ITask Logout(UriString host) return loginManager.Logout(host); } - public async Task CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null) + public void CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null) { Guard.ArgumentNotNull(callback, "callback"); - try - { - var repository = await CreateRepositoryInternal(name, organization, description, isPrivate); - callback(repository, null); - } - catch (Exception e) - { - callback(null, e); - } - } - - public async Task GetOrganizations(Action onSuccess, Action onError = null) - { - Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); - await GetOrganizationInternal(onSuccess, onError); - } - public async Task GetCurrentUser(Action onSuccess, Action onError = null) - { - Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); - try - { - var user = await GetCurrentUser(); - onSuccess(user); - } - catch (Exception e) + new FuncTask(taskManager.Token, () => { - onError?.Invoke(e); - } - } - - public async Task Login(string username, string password, Action need2faCode, Action result) - { - Guard.ArgumentNotNull(need2faCode, "need2faCode"); - Guard.ArgumentNotNull(result, "result"); - - LoginResultData res = null; - try - { - res = await loginManager.Login(OriginalUrl, username, password); - } - catch (Exception ex) - { - logger.Warning(ex); - result(false, ex.Message); - return; - } - - if (res.Code == LoginResultCodes.CodeRequired) - { - var resultCache = new LoginResult(res, result, need2faCode); - need2faCode(resultCache); - } - else - { - result(res.Code == LoginResultCodes.Success, res.Message); - } - } - - public async Task ContinueLogin(LoginResult loginResult, string code) - { - LoginResultData result = null; - try - { - result = await loginManager.ContinueLogin(loginResult.Data, code); - } - catch (Exception ex) - { - loginResult.Callback(false, ex.Message); - return; - } - if (result.Code == LoginResultCodes.CodeFailed) - { - loginResult.TwoFACallback(new LoginResult(result, loginResult.Callback, loginResult.TwoFACallback)); - } - loginResult.Callback(result.Code == LoginResultCodes.Success, result.Message); - } - - private async Task GetCurrentUser() - { - //TODO: ONE_USER_LOGIN This assumes we only support one login - var keychainConnection = keychain.Connections.FirstOrDefault(); - if (keychainConnection == null) - throw new KeychainEmptyException(); - - var keychainAdapter = await GetValidatedKeychainAdapter(keychainConnection); - - // we can't trust that the system keychain has the username filled out correctly. - // if it doesn't, we need to grab the username from the server and check it - // unfortunately this means that things will be slower when the keychain doesn't have all the info - if (keychainConnection.User == null || keychainAdapter.Credential.Username != keychainConnection.Username) - { - keychainConnection.User = await GetValidatedGitHubUser(keychainConnection, keychainAdapter); - } - return keychainConnection.User; - } - - private async Task CreateRepositoryInternal(string repositoryName, string organization, string description, bool isPrivate) - { - try - { - var user = await GetCurrentUser(); + var user = GetCurrentUser(); var keychainAdapter = keychain.Connect(OriginalUrl); var command = new StringBuilder("publish -r \""); - command.Append(repositoryName); + command.Append(name); command.Append("\""); if (!string.IsNullOrEmpty(description)) @@ -176,10 +77,10 @@ private async Task CreateRepositoryInternal(string repositoryN } var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, command.ToString(), - user: user.Login, userToken: keychainAdapter.Credential.Token) + user: user.Login, userToken: keychainAdapter.Credential.Token) .Configure(processManager); - var ret = await octorunTask.StartAwait(); + var ret = octorunTask.RunSynchronously(); if (ret.IsSuccess && ret.Output.Length == 2) { return new GitHubRepository @@ -190,26 +91,33 @@ private async Task CreateRepositoryInternal(string repositoryN } throw new ApiClientException(ret.GetApiErrorMessage() ?? "Publish failed"); - } - catch (Exception ex) + }) + .FinallyInUI((success, ex, repository) => { - logger.Error(ex, "Error Creating Repository"); - throw; - } + if (success) + callback(repository, null); + else + { + logger.Error(ex, "Error creating repository"); + callback(null, ex); + } + }) + .Start(); } - private async Task GetOrganizationInternal(Action onSuccess, Action onError = null) + public void GetOrganizations(Action onSuccess, Action onError = null) { - try + Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); + new FuncTask(taskManager.Token, () => { - var user = await GetCurrentUser(); + var user = GetCurrentUser(); var keychainAdapter = keychain.Connect(OriginalUrl); var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, "organizations", user: user.Login, userToken: keychainAdapter.Credential.Token) .Configure(processManager); - var ret = await octorunTask.StartAsAsync(); + var ret = octorunTask.RunSynchronously(); if (ret.IsSuccess) { var organizations = new List(); @@ -221,23 +129,109 @@ private async Task GetOrganizationInternal(Action onSuccess, Act Login = ret.Output[i + 1] }); } - - onSuccess(organizations.ToArray()); - return; + return organizations.ToArray(); } throw new ApiClientException(ret.GetApiErrorMessage() ?? "Error getting organizations"); - } - catch (Exception ex) + }) + .FinallyInUI((success, ex, orgs) => + { + if (success) + onSuccess(orgs); + else + { + logger.Error(ex, "Error Getting Organizations"); + onError?.Invoke(ex); + } + }) + .Start(); + } + + public void GetCurrentUser(Action onSuccess, Action onError = null) + { + Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); + new FuncTask(taskManager.Token, GetCurrentUser) + .FinallyInUI((success, ex, user) => + { + if (success) + onSuccess(user); + else + onError?.Invoke(ex); + }) + .Start(); + } + + public void Login(string username, string password, Action need2faCode, Action result) + { + Guard.ArgumentNotNull(need2faCode, "need2faCode"); + Guard.ArgumentNotNull(result, "result"); + + new FuncTask(taskManager.Token, + () => loginManager.Login(OriginalUrl, username, password)) + .FinallyInUI((success, ex, res) => + { + if (!success) + { + logger.Warning(ex); + result(false, ex.Message); + return; + } + + if (res.Code == LoginResultCodes.CodeRequired) + { + var resultCache = new LoginResult(res, result, need2faCode); + need2faCode(resultCache); + } + else + { + result(res.Code == LoginResultCodes.Success, res.Message); + } + }) + .Start(); + } + + public void ContinueLogin(LoginResult loginResult, string code) + { + new FuncTask(taskManager.Token, + () => loginManager.ContinueLogin(loginResult.Data, code)) + .FinallyInUI((success, ex, result) => + { + if (!success) + { + loginResult.Callback(false, ex.Message); + return; + } + if (result.Code == LoginResultCodes.CodeFailed) + { + loginResult.TwoFACallback(new LoginResult(result, loginResult.Callback, loginResult.TwoFACallback)); + } + loginResult.Callback(result.Code == LoginResultCodes.Success, result.Message); + }) + .Start(); + } + + private GitHubUser GetCurrentUser() + { + //TODO: ONE_USER_LOGIN This assumes we only support one login + var keychainConnection = keychain.Connections.FirstOrDefault(); + if (keychainConnection == null) + throw new KeychainEmptyException(); + + var keychainAdapter = GetValidatedKeychainAdapter(keychainConnection); + + // we can't trust that the system keychain has the username filled out correctly. + // if it doesn't, we need to grab the username from the server and check it + // unfortunately this means that things will be slower when the keychain doesn't have all the info + if (keychainConnection.User == null || keychainAdapter.Credential.Username != keychainConnection.Username) { - logger.Error(ex, "Error Getting Organizations"); - onError?.Invoke(ex); + keychainConnection.User = GetValidatedGitHubUser(keychainConnection, keychainAdapter); } + return keychainConnection.User; } - private async Task GetValidatedKeychainAdapter(Connection keychainConnection) + private IKeychainAdapter GetValidatedKeychainAdapter(Connection keychainConnection) { - var keychainAdapter = await keychain.Load(keychainConnection.Host); + var keychainAdapter = keychain.Load(keychainConnection.Host); if (keychainAdapter == null) throw new KeychainEmptyException(); @@ -255,7 +249,7 @@ private async Task GetValidatedKeychainAdapter(Connection keyc return keychainAdapter; } - private async Task GetValidatedGitHubUser(Connection keychainConnection, IKeychainAdapter keychainAdapter) + private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeychainAdapter keychainAdapter) { try { @@ -263,7 +257,7 @@ private async Task GetValidatedGitHubUser(Connection keychainConnect user: keychainConnection.Username, userToken: keychainAdapter.Credential.Token) .Configure(processManager); - var ret = await octorunTask.StartAsAsync(); + var ret = octorunTask.RunSynchronously(); if (ret.IsSuccess) { var login = ret.Output[1]; diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index 12ce14e28..650595ce2 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using System; +using System; namespace GitHub.Unity { @@ -7,12 +6,12 @@ interface IApiClient { HostAddress HostAddress { get; } UriString OriginalUrl { get; } - Task CreateRepository(string name, string description, bool isPrivate, + void CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null); - Task GetOrganizations(Action onSuccess, Action onError = null); - Task Login(string username, string password, Action need2faCode, Action result); - Task ContinueLogin(LoginResult loginResult, string code); + void GetOrganizations(Action onSuccess, Action onError = null); + void Login(string username, string password, Action need2faCode, Action result); + void ContinueLogin(LoginResult loginResult, string code); ITask Logout(UriString host); - Task GetCurrentUser(Action onSuccess, Action onError = null); + void GetCurrentUser(Action onSuccess, Action onError = null); } } diff --git a/src/GitHub.Api/Authentication/ICredentialManager.cs b/src/GitHub.Api/Authentication/ICredentialManager.cs index b601d3633..68bf53eb6 100644 --- a/src/GitHub.Api/Authentication/ICredentialManager.cs +++ b/src/GitHub.Api/Authentication/ICredentialManager.cs @@ -13,10 +13,10 @@ public interface ICredential : IDisposable public interface ICredentialManager { - Task Load(UriString host); - Task Save(ICredential cred); - Task Delete(UriString host); + ICredential Load(UriString host); + void Save(ICredential cred); + void Delete(UriString host); bool HasCredentials(); ICredential CachedCredentials { get; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/IKeychain.cs b/src/GitHub.Api/Authentication/IKeychain.cs index a05a748dc..92d5dc524 100644 --- a/src/GitHub.Api/Authentication/IKeychain.cs +++ b/src/GitHub.Api/Authentication/IKeychain.cs @@ -1,15 +1,14 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; namespace GitHub.Unity { public interface IKeychain { IKeychainAdapter Connect(UriString host); - Task Load(UriString host); - Task Clear(UriString host, bool deleteFromCredentialManager); - Task Save(UriString host); + IKeychainAdapter Load(UriString host); + void Clear(UriString host, bool deleteFromCredentialManager); + void Save(UriString host); void SetCredentials(ICredential credential); void Initialize(); Connection[] Connections { get; } @@ -19,4 +18,4 @@ public interface IKeychain event Action ConnectionsChanged; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/ILoginManager.cs b/src/GitHub.Api/Authentication/ILoginManager.cs index b2e7dd9b5..66d982ae0 100644 --- a/src/GitHub.Api/Authentication/ILoginManager.cs +++ b/src/GitHub.Api/Authentication/ILoginManager.cs @@ -17,8 +17,8 @@ interface ILoginManager /// /// The login authorization failed. /// - Task Login(UriString host, string username, string password); - Task ContinueLogin(LoginResultData loginResultData, string twofacode); + LoginResultData Login(UriString host, string username, string password); + LoginResultData ContinueLogin(LoginResultData loginResultData, string twofacode); /// /// Logs out of GitHub server. @@ -27,4 +27,4 @@ interface ILoginManager /// ITask Logout(UriString hostAddress); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index b6f41d57c..992cc26b3 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using GitHub.Logging; namespace GitHub.Unity @@ -100,18 +99,18 @@ public IKeychainAdapter Connect(UriString host) return FindOrCreateAdapter(host); } - public async Task Load(UriString host) + public IKeychainAdapter Load(UriString host) { Guard.ArgumentNotNull(host, nameof(host)); var keychainAdapter = FindOrCreateAdapter(host); var connection = GetConnection(host); - var keychainItem = await credentialManager.Load(host); + var keychainItem = credentialManager.Load(host); if (keychainItem == null) { logger.Warning("Cannot load host from Credential Manager; removing from cache"); - await Clear(host, false); + Clear(host, false); keychainAdapter = null; } else @@ -142,21 +141,21 @@ public void Initialize() LoadConnectionsFromDisk(); } - public async Task Clear(UriString host, bool deleteFromCredentialManager) + public void Clear(UriString host, bool deleteFromCredentialManager) { Guard.ArgumentNotNull(host, nameof(host)); RemoveConnection(host); //clear octokit credentials - await RemoveCredential(host, deleteFromCredentialManager); + RemoveCredential(host, deleteFromCredentialManager); } - public async Task Save(UriString host) + public void Save(UriString host) { Guard.ArgumentNotNull(host, nameof(host)); - var keychainAdapter = await AddCredential(host); + var keychainAdapter = AddCredential(host); AddConnection(new Connection(host, keychainAdapter.Credential.Username)); } @@ -231,7 +230,7 @@ private KeychainAdapter GetKeychainAdapter(UriString host) return credentialAdapter; } - private async Task AddCredential(UriString host) + private KeychainAdapter AddCredential(UriString host) { var keychainAdapter = GetKeychainAdapter(host); if (string.IsNullOrEmpty(keychainAdapter.Credential.Token)) @@ -240,12 +239,12 @@ private async Task AddCredential(UriString host) } // saves credential in git credential manager (host, username, token) - await credentialManager.Delete(host); - await credentialManager.Save(keychainAdapter.Credential); + credentialManager.Delete(host); + credentialManager.Save(keychainAdapter.Credential); return keychainAdapter; } - private async Task RemoveCredential(UriString host, bool deleteFromCredentialManager) + private void RemoveCredential(UriString host, bool deleteFromCredentialManager) { KeychainAdapter k; if (keychainAdapters.TryGetValue(host, out k)) @@ -256,7 +255,7 @@ private async Task RemoveCredential(UriString host, bool deleteFromCredentialMan if (deleteFromCredentialManager) { - await credentialManager.Delete(host); + credentialManager.Delete(host); } } diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index d59bbcd5d..42f1a7f91 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using GitHub.Logging; namespace GitHub.Unity @@ -48,7 +47,7 @@ public LoginManager( } /// - public async Task Login( + public LoginResultData Login( UriString host, string username, string password) @@ -64,7 +63,7 @@ public async Task Login( try { - var loginResultData = await TryLogin(host, username, password); + var loginResultData = TryLogin(host, username, password); if (loginResultData.Code == LoginResultCodes.Success || loginResultData.Code == LoginResultCodes.CodeRequired) { if (string.IsNullOrEmpty(loginResultData.Token)) @@ -74,14 +73,14 @@ public async Task Login( if (loginResultData.Code == LoginResultCodes.Success) { - username = await RetrieveUsername(loginResultData, username); + username = RetrieveUsername(loginResultData, username); } keychain.SetToken(host, loginResultData.Token, username); if (loginResultData.Code == LoginResultCodes.Success) { - await keychain.Save(host); + keychain.Save(host); } return loginResultData; @@ -93,12 +92,12 @@ public async Task Login( { logger.Warning(e, "Login Exception"); - await keychain.Clear(host, false); + keychain.Clear(host, false); return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); } } - public async Task ContinueLogin(LoginResultData loginResultData, string twofacode) + public LoginResultData ContinueLogin(LoginResultData loginResultData, string twofacode) { var host = loginResultData.Host; var keychainAdapter = keychain.Connect(host); @@ -106,7 +105,7 @@ public async Task ContinueLogin(LoginResultData loginResultData var password = keychainAdapter.Credential.Token; try { - loginResultData = await TryLogin(host, username, password, twofacode); + loginResultData = TryLogin(host, username, password, twofacode); if (loginResultData.Code == LoginResultCodes.Success) { @@ -115,9 +114,9 @@ public async Task ContinueLogin(LoginResultData loginResultData throw new InvalidOperationException("Returned token is null or empty"); } - username = await RetrieveUsername(loginResultData, username); + username = RetrieveUsername(loginResultData, username); keychain.SetToken(host, loginResultData.Token, username); - await keychain.Save(host); + keychain.Save(host); return loginResultData; } @@ -128,7 +127,7 @@ public async Task ContinueLogin(LoginResultData loginResultData { logger.Warning(e, "Login Exception"); - await keychain.Clear(host, false); + keychain.Clear(host, false); return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); } } @@ -137,11 +136,10 @@ public async Task ContinueLogin(LoginResultData loginResultData public ITask Logout(UriString hostAddress) { Guard.ArgumentNotNull(hostAddress, nameof(hostAddress)); - - return new TPLTask(keychain.Clear(hostAddress, true)) { Message = "Signing out" }.Start(); + return taskManager.Run(() => keychain.Clear(hostAddress, true), "Signing out"); } - private async Task TryLogin( + private LoginResultData TryLogin( UriString host, string username, string password, @@ -175,7 +173,7 @@ private async Task TryLogin( proc.StandardInput.Close(); }; - var ret = await loginTask.StartAwait(); + var ret = loginTask.RunSynchronously(); if (ret.IsSuccess) { @@ -193,7 +191,7 @@ private async Task TryLogin( return new LoginResultData(LoginResultCodes.Failed, ret.GetApiErrorMessage() ?? "Failed.", host); } - private async Task RetrieveUsername(LoginResultData loginResultData, string username) + private string RetrieveUsername(LoginResultData loginResultData, string username) { if (!username.Contains("@")) { @@ -203,7 +201,7 @@ private async Task RetrieveUsername(LoginResultData loginResultData, str var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, "validate", user: username, userToken: loginResultData.Token).Configure(processManager); - var validateResult = await octorunTask.StartAsAsync(); + var validateResult = octorunTask.RunSynchronously(); if (!validateResult.IsSuccess) { throw new InvalidOperationException("Authentication validation failed"); diff --git a/src/GitHub.Api/Git/GitCredentialManager.cs b/src/GitHub.Api/Git/GitCredentialManager.cs index 1478c431d..25b946d1b 100644 --- a/src/GitHub.Api/Git/GitCredentialManager.cs +++ b/src/GitHub.Api/Git/GitCredentialManager.cs @@ -1,7 +1,6 @@ using GitHub.Logging; using System; using System.Collections.Generic; -using System.Threading.Tasks; namespace GitHub.Unity { @@ -29,35 +28,35 @@ public bool HasCredentials() public ICredential CachedCredentials { get { return credential; } } - public async Task Delete(UriString host) + public void Delete(UriString host) { - if (!await LoadCredentialHelper()) + if (!LoadCredentialHelper()) return; - await RunCredentialHelper( + RunCredentialHelper( "erase", new string[] { String.Format("protocol={0}", host.Protocol), String.Format("host={0}", host.Host) - }).StartAwait(); + }).RunSynchronously(); credential = null; } - public async Task Load(UriString host) + public ICredential Load(UriString host) { if (credential == null) { - if (!await LoadCredentialHelper()) + if (!LoadCredentialHelper()) return null; string kvpCreds = null; - kvpCreds = await RunCredentialHelper( + kvpCreds = RunCredentialHelper( "get", new string[] { String.Format("protocol={0}", host.Protocol), String.Format("host={0}", host.Host) - }).StartAwait(); + }).RunSynchronously(); if (String.IsNullOrEmpty(kvpCreds)) { @@ -92,11 +91,11 @@ public async Task Load(UriString host) return credential; } - public async Task Save(ICredential cred) + public void Save(ICredential cred) { this.credential = cred; - if (!await LoadCredentialHelper()) + if (!LoadCredentialHelper()) return; var data = new List @@ -108,21 +107,21 @@ public async Task Save(ICredential cred) }; var task = RunCredentialHelper("store", data.ToArray()); - await task.StartAwait(); + task.RunSynchronously(); if (!task.Successful) { Logger.Error("Failed to save credentials"); } } - private async Task LoadCredentialHelper() + private bool LoadCredentialHelper() { if (credHelper != null) return true; - credHelper = await new GitConfigGetTask("credential.helper", GitConfigSource.NonSpecified, taskManager.Token) + credHelper = new GitConfigGetTask("credential.helper", GitConfigSource.NonSpecified, taskManager.Token) .Configure(processManager) - .StartAwait(); + .RunSynchronously(); //Logger.Trace("Loaded Credential Helper: {0}", credHelper); @@ -163,4 +162,4 @@ private ITask RunCredentialHelper(string action, string[] lines) return task; } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index cf6f12222..bd215045e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -8,9 +8,9 @@ class AuthenticationService private LoginResult loginResultData; - public AuthenticationService(UriString host, IKeychain keychain, NPath nodeJsExecutablePath, NPath octorunExecutablePath) + public AuthenticationService(IProcessManager processManager, ITaskManager taskManager, UriString host, IKeychain keychain, NPath nodeJsExecutablePath, NPath octorunExecutablePath) { - client = new ApiClient(host, keychain, EntryPoint.ApplicationManager.ProcessManager, EntryPoint.ApplicationManager.TaskManager, nodeJsExecutablePath, octorunExecutablePath); + client = new ApiClient(host, keychain, processManager, taskManager, nodeJsExecutablePath, octorunExecutablePath); } public void Login(string username, string password, Action twofaRequired, Action authResult) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 0b7c55c9f..b9430852c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -254,7 +254,7 @@ private AuthenticationService AuthenticationService host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); } - AuthenticationService = new AuthenticationService(host, Platform.Keychain, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); + AuthenticationService = new AuthenticationService(Manager.ProcessManager, Manager.TaskManager, host, Platform.Keychain, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); } return authenticationService; } diff --git a/src/tests/UnitTests/Authentication/KeychainTests.cs b/src/tests/UnitTests/Authentication/KeychainTests.cs index 23786626b..6833fc748 100644 --- a/src/tests/UnitTests/Authentication/KeychainTests.cs +++ b/src/tests/UnitTests/Authentication/KeychainTests.cs @@ -162,7 +162,7 @@ public void ShouldLoadFromConnectionManager() credential.Username.Returns(username); credential.Token.Returns(token); credential.Host.Returns(hostUri); - return TaskEx.FromResult(credential); + return credential; }); var keychain = new Keychain(environment, credentialManager); @@ -176,7 +176,7 @@ public void ShouldLoadFromConnectionManager() fileSystem.DidNotReceive().WriteAllLines(Args.String, Arg.Any()); var uriString = keychain.Hosts.FirstOrDefault(); - var keychainAdapter = keychain.Load(uriString).Result; + var keychainAdapter = keychain.Load(uriString); keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(token); keychainAdapter.Credential.Host.Should().Be(hostUri); @@ -209,7 +209,7 @@ public void ShouldDeleteFromCacheWhenLoadReturnsNullFromConnectionManager() environment.FileSystem.Returns(fileSystem); var credentialManager = Substitute.For(); - credentialManager.Load(hostUri).Returns(info => TaskEx.FromResult(null)); + credentialManager.Load(hostUri).Returns(info => null); var keychain = new Keychain(environment, credentialManager); keychain.Initialize(); @@ -222,7 +222,7 @@ public void ShouldDeleteFromCacheWhenLoadReturnsNullFromConnectionManager() fileSystem.ClearReceivedCalls(); var uriString = keychain.Hosts.FirstOrDefault(); - var keychainAdapter = keychain.Load(uriString).Result; + var keychainAdapter = keychain.Load(uriString); keychainAdapter.Should().BeNull(); fileSystem.DidNotReceive().FileExists(Args.String); @@ -259,10 +259,6 @@ public void ShouldConnectSetCredentialsTokenAndSave() var credentialManager = Substitute.For(); - credentialManager.Delete(Args.UriString).Returns(info => TaskEx.FromResult(0)); - - credentialManager.Save(Arg.Any()).Returns(info => TaskEx.FromResult(0)); - var keychain = new Keychain(environment, credentialManager); keychain.Initialize(); @@ -299,7 +295,7 @@ public void ShouldConnectSetCredentialsTokenAndSave() keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(token); - keychain.Save(hostUri).Wait(); + keychain.Save(hostUri); fileSystem.DidNotReceive().FileExists(Args.String); fileSystem.DidNotReceive().FileDelete(Args.String); @@ -334,10 +330,6 @@ public void ShouldConnectSetCredentialsAndClear() var credentialManager = Substitute.For(); - credentialManager.Delete(Args.UriString).Returns(info => TaskEx.FromResult(0)); - - credentialManager.Save(Arg.Any()).Returns(info => TaskEx.FromResult(0)); - var keychain = new Keychain(environment, credentialManager); keychain.Initialize(); @@ -367,7 +359,7 @@ public void ShouldConnectSetCredentialsAndClear() keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(password); - keychain.Clear(hostUri, false).Wait(); + keychain.Clear(hostUri, false); keychainAdapter.Credential.Should().BeNull(); From 36115fb039c440f878b619ef9ba42fe8378486b7 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 20:21:26 +0200 Subject: [PATCH 308/567] Fix nulls being passed around --- src/GitHub.Api/Application/ApiClient.cs | 6 +----- src/GitHub.Api/Authentication/LoginManager.cs | 2 +- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index cac8b5418..73055228b 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -32,11 +32,7 @@ public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processM this.taskManager = taskManager; this.nodeJsExecutablePath = nodeJsExecutablePath; this.octorunScriptPath = octorunScriptPath; - loginManager = new LoginManager(keychain, - processManager: processManager, - taskManager: taskManager, - nodeJsExecutablePath: nodeJsExecutablePath, - octorunScript: octorunScriptPath); + loginManager = new LoginManager(keychain, processManager, taskManager, nodeJsExecutablePath, octorunScriptPath); } public ITask Logout(UriString host) diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 42f1a7f91..44a337a54 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -34,7 +34,7 @@ class LoginManager : ILoginManager /// /// public LoginManager( - IKeychain keychain, IProcessManager processManager = null, ITaskManager taskManager = null, + IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, NPath? nodeJsExecutablePath = null, NPath? octorunScript = null) { Guard.ArgumentNotNull(keychain, nameof(keychain)); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index d25c08066..affb6edd7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -816,7 +816,7 @@ private void SignOut(object obj) host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); } - var apiClient = new ApiClient(host, Platform.Keychain, null, null, NPath.Default, NPath.Default); + var apiClient = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); apiClient.Logout(host); } From 2d589248578773391f2689677745425fd8d911cc Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 20:40:44 +0200 Subject: [PATCH 309/567] TryParse throws if it gets a null value, and structs often have null values --- src/GitHub.Api/Cache/CacheContainer.cs | 2 +- src/GitHub.Api/Extensions/StringExtensions.cs | 5 +++++ src/GitHub.Api/Git/GitLock.cs | 3 ++- src/GitHub.Api/Git/GitLogEntry.cs | 6 +++--- src/GitHub.Api/Platform/Settings.cs | 2 +- .../Assets/Editor/GitHub.Unity/ApplicationCache.cs | 6 +++--- src/tests/IntegrationTests/CachingClasses.cs | 4 ++-- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/GitHub.Api/Cache/CacheContainer.cs b/src/GitHub.Api/Cache/CacheContainer.cs index 85b561f41..40f1802fb 100644 --- a/src/GitHub.Api/Cache/CacheContainer.cs +++ b/src/GitHub.Api/Cache/CacheContainer.cs @@ -162,7 +162,7 @@ public DateTimeOffset UpdatedTime if (!updatedTimeValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(updatedTimeString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(updatedTimeString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { updatedTimeValue = result; } diff --git a/src/GitHub.Api/Extensions/StringExtensions.cs b/src/GitHub.Api/Extensions/StringExtensions.cs index 5a638ee32..ebb2c4351 100644 --- a/src/GitHub.Api/Extensions/StringExtensions.cs +++ b/src/GitHub.Api/Extensions/StringExtensions.cs @@ -40,6 +40,11 @@ public static string ToNullIfEmpty(this string s) return String.IsNullOrEmpty(s) ? null : s; } + public static string ToEmptyIfNull(this string s) + { + return String.IsNullOrEmpty(s) ? String.Empty : s; + } + public static bool StartsWith(this string s, char c) { if (String.IsNullOrEmpty(s)) return false; diff --git a/src/GitHub.Api/Git/GitLock.cs b/src/GitHub.Api/Git/GitLock.cs index ae57d3b6f..e7a961ae7 100644 --- a/src/GitHub.Api/Git/GitLock.cs +++ b/src/GitHub.Api/Git/GitLock.cs @@ -13,12 +13,13 @@ public struct GitLock public string path; public GitUser owner; [NotSerialized] public string lockedAtString; + private string LockedAtString { get { return lockedAtString != null ? lockedAtString : String.Empty; } } public DateTimeOffset locked_at { get { DateTimeOffset dt; - if (!DateTimeOffset.TryParseExact(lockedAtString, Constants.Iso8601Formats, + if (!DateTimeOffset.TryParseExact(LockedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle, out dt)) { locked_at = DateTimeOffset.MinValue; diff --git a/src/GitHub.Api/Git/GitLogEntry.cs b/src/GitHub.Api/Git/GitLogEntry.cs index a5fc97377..a4656fdef 100644 --- a/src/GitHub.Api/Git/GitLogEntry.cs +++ b/src/GitHub.Api/Git/GitLogEntry.cs @@ -68,7 +68,7 @@ public DateTimeOffset Time if (!timeValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(TimeString, Constants.Iso8601Formats, CultureInfo.InvariantCulture,DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(TimeString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture,DateTimeStyles.None, out result)) { timeValue = result; } @@ -95,7 +95,7 @@ public DateTimeOffset CommitTime if (!commitTimeValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(CommitTimeString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(CommitTimeString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { commitTimeValue = result; } @@ -216,4 +216,4 @@ public override string ToString() return sb.ToString(); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Platform/Settings.cs b/src/GitHub.Api/Platform/Settings.cs index fd4ff51be..1a2e02d2e 100644 --- a/src/GitHub.Api/Platform/Settings.cs +++ b/src/GitHub.Api/Platform/Settings.cs @@ -74,7 +74,7 @@ public override string Get(string key, string fallback = "") if (typeof(T) == typeof(DateTimeOffset)) { DateTimeOffset dt; - if (DateTimeOffset.TryParseExact(value?.ToString(), Constants.Iso8601Formats, + if (DateTimeOffset.TryParseExact(value?.ToString().ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) { value = dt; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs index b1de8018d..d316fdf9d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs @@ -38,7 +38,7 @@ public DateTimeOffset FirstRunAt if (!firstRunAtValue.HasValue) { DateTimeOffset dt; - if (!DateTimeOffset.TryParseExact(firstRunAtString, Constants.Iso8601Formats, + if (!DateTimeOffset.TryParseExact(firstRunAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt)) { dt = DateTimeOffset.Now; @@ -261,7 +261,7 @@ public DateTimeOffset LastUpdatedAt if (!lastUpdatedAtValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(LastUpdatedAtString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(LastUpdatedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { lastUpdatedAtValue = result; } @@ -287,7 +287,7 @@ public DateTimeOffset InitializedAt if (!initializedAtValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(InitializedAtString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(InitializedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { initializedAtValue = result; } diff --git a/src/tests/IntegrationTests/CachingClasses.cs b/src/tests/IntegrationTests/CachingClasses.cs index b359118c7..944c584e1 100644 --- a/src/tests/IntegrationTests/CachingClasses.cs +++ b/src/tests/IntegrationTests/CachingClasses.cs @@ -184,7 +184,7 @@ public DateTimeOffset LastUpdatedAt if (!lastUpdatedAtValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(LastUpdatedAtString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(LastUpdatedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { lastUpdatedAtValue = result; } @@ -210,7 +210,7 @@ public DateTimeOffset InitializedAt if (!initializedAtValue.HasValue) { DateTimeOffset result; - if (DateTimeOffset.TryParseExact(InitializedAtString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + if (DateTimeOffset.TryParseExact(InitializedAtString.ToEmptyIfNull(), Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) { initializedAtValue = result; } From ad28118b1b169f9a30f1f8e110c44a30d9519c04 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 3 Apr 2018 19:48:29 +0200 Subject: [PATCH 310/567] Fix account dropdown, and show username while we're at it --- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index d25c08066..bfc6f2b95 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -54,6 +54,7 @@ class Window : BaseWindow [SerializeField] private string repositoryProgressMessage; [SerializeField] private float appManagerProgressValue; [SerializeField] private string appManagerProgressMessage; + [SerializeField] private Connection connection; [MenuItem(Menu_Window_GitHub)] public static void Window_GitHub() @@ -210,6 +211,7 @@ private void ValidateCachedData(IRepository repository) private void MaybeUpdateData() { + connection = Platform.Keychain.Connections.FirstOrDefault(); if (repositoryProgressHasUpdate) { if (repositoryProgress != null) @@ -351,6 +353,7 @@ private void AttachHandlers(IRepository repository) repository.TrackingStatusChanged += RepositoryOnTrackingStatusChanged; repository.StatusEntriesChanged += RepositoryOnStatusEntriesChanged; repository.OnProgress += UpdateProgress; + Platform.Keychain.ConnectionsChanged += ConnectionsChanged; } private void DetachHandlers(IRepository repository) @@ -362,6 +365,7 @@ private void DetachHandlers(IRepository repository) repository.StatusEntriesChanged -= RepositoryOnStatusEntriesChanged; repository.OnProgress -= UpdateProgress; Manager.OnProgress -= ApplicationManagerOnProgress; + Platform.Keychain.ConnectionsChanged -= ConnectionsChanged; } private void RepositoryOnCurrentBranchAndRemoteChanged(CacheUpdateEvent cacheUpdateEvent) @@ -419,6 +423,12 @@ private void ApplicationManagerOnProgress(IProgress progress) appManagerProgressHasUpdate = true; } + private void ConnectionsChanged() + { + connection = Platform.Keychain.Connections.FirstOrDefault(); + Redraw(); + } + public override void OnUI() { base.OnUI(); @@ -541,6 +551,18 @@ private void DoToolbarGUI() } GUILayout.FlexibleSpace(); + + if (connection == null) + { + if (GUILayout.Button("Sign in", Styles.HistoryToolbarButtonStyle)) + SignIn(null); + } + else + { + if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) + { + DoAccountDropdown(); + } } EditorGUILayout.EndHorizontal(); } @@ -770,23 +792,15 @@ private void SwitchView(Subview fromView, Subview toView) toView.OnDataUpdate(); // this triggers a repaint - Repaint(); + Redraw(); } private void DoAccountDropdown() { GenericMenu accountMenu = new GenericMenu(); - - if (!Platform.Keychain.HasKeys) - { - accountMenu.AddItem(new GUIContent("Sign in"), false, SignIn, "sign in"); - } - else - { - accountMenu.AddItem(new GUIContent("Go to Profile"), false, GoToProfile, "profile"); - accountMenu.AddSeparator(""); - accountMenu.AddItem(new GUIContent("Sign out"), false, SignOut, "sign out"); - } + accountMenu.AddItem(new GUIContent("Go to Profile"), false, GoToProfile, "profile"); + accountMenu.AddSeparator(""); + accountMenu.AddItem(new GUIContent("Sign out"), false, SignOut, "sign out"); accountMenu.ShowAsContext(); } From 9dd153c923c179d90b34264c477660bf57629bb0 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 21:04:57 +0200 Subject: [PATCH 311/567] Fix logging out not logging out properly --- src/GitHub.Api/Primitives/UriString.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/GitHub.Api/Primitives/UriString.cs b/src/GitHub.Api/Primitives/UriString.cs index 6cf6522ad..a60decfb7 100644 --- a/src/GitHub.Api/Primitives/UriString.cs +++ b/src/GitHub.Api/Primitives/UriString.cs @@ -267,7 +267,7 @@ static string GetSerializedValue(SerializationInfo info) static string NormalizePath(string path) { - return path?.Replace('\\', '/'); + return path?.Replace('\\', '/').TrimEnd('/'); } static string GetRepositoryName(string repositoryNameSegment) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index bfc6f2b95..f6d26047f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -551,18 +551,6 @@ private void DoToolbarGUI() } GUILayout.FlexibleSpace(); - - if (connection == null) - { - if (GUILayout.Button("Sign in", Styles.HistoryToolbarButtonStyle)) - SignIn(null); - } - else - { - if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) - { - DoAccountDropdown(); - } } EditorGUILayout.EndHorizontal(); } @@ -632,8 +620,18 @@ private void DoActionbarGUI() GUILayout.FlexibleSpace(); - if (GUILayout.Button(Localization.AccountButton, EditorStyles.toolbarDropDown)) - DoAccountDropdown(); + if (connection == null) + { + if (GUILayout.Button("Sign in", EditorStyles.toolbarButton)) + SignIn(null); + } + else + { + if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) + { + DoAccountDropdown(); + } + } } EditorGUILayout.EndHorizontal(); } @@ -823,7 +821,7 @@ private void SignOut(object obj) if (Repository != null && Repository.CloneUrl != null && Repository.CloneUrl.IsValidUri) { host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); } else { @@ -831,7 +829,7 @@ private void SignOut(object obj) } var apiClient = new ApiClient(host, Platform.Keychain, null, null, NPath.Default, NPath.Default); - apiClient.Logout(host); + apiClient.Logout(host).FinallyInUI((s, e) => Redraw()); } public new void ShowNotification(GUIContent content) From fa2f48ea719443d73f6af8e579b71dc619e8180a Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 21:10:55 +0200 Subject: [PATCH 312/567] No trailing slash for you --- src/tests/UnitTests/Authentication/KeychainTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/UnitTests/Authentication/KeychainTests.cs b/src/tests/UnitTests/Authentication/KeychainTests.cs index 23786626b..0a06aad00 100644 --- a/src/tests/UnitTests/Authentication/KeychainTests.cs +++ b/src/tests/UnitTests/Authentication/KeychainTests.cs @@ -305,7 +305,7 @@ public void ShouldConnectSetCredentialsTokenAndSave() fileSystem.DidNotReceive().FileDelete(Args.String); fileSystem.DidNotReceive().ReadAllText(Args.String); fileSystem.DidNotReceive().ReadAllLines(Args.String); - fileSystem.Received(1).WriteAllText(connectionsCacheFile, @"[{""Host"":""https://github.com/"",""Username"":""SomeUser""}]"); + fileSystem.Received(1).WriteAllText(connectionsCacheFile, @"[{""Host"":""https://github.com"",""Username"":""SomeUser""}]"); credentialManager.DidNotReceive().Load(Args.UriString); credentialManager.DidNotReceive().HasCredentials(); From ad23dcd042c29a70ccc37fde58ece3e03c2204d5 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 21:16:44 +0200 Subject: [PATCH 313/567] We shouldn't update data here and we should redraw on the main thread --- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index f6d26047f..bb5a5e7df 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -212,6 +212,7 @@ private void ValidateCachedData(IRepository repository) private void MaybeUpdateData() { connection = Platform.Keychain.Connections.FirstOrDefault(); + if (repositoryProgressHasUpdate) { if (repositoryProgress != null) @@ -425,8 +426,10 @@ private void ApplicationManagerOnProgress(IProgress progress) private void ConnectionsChanged() { - connection = Platform.Keychain.Connections.FirstOrDefault(); - Redraw(); + if (!ThreadingHelper.InUIThread) + TaskManager.RunInUI(Redraw); + else + Redraw(); } public override void OnUI() From f93bc27c862350f80f8a887f69f71a407d83b238 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 1 Jun 2018 21:37:36 +0200 Subject: [PATCH 314/567] Fix typo in setting build properties --- common/properties.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/properties.props b/common/properties.props index 18fb9aa60..977f217b3 100644 --- a/common/properties.props +++ b/common/properties.props @@ -3,8 +3,8 @@ Internal - ENABLE_METRICS - ENABLE_MONO + ENABLE_METRICS + $(BuildDefs);ENABLE_MONO $(SolutionDir)script\lib\ $(SolutionDir)lib\ From f8853cd3403ce2e2ff0638edd94622952ebdfeef Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Sat, 2 Jun 2018 02:20:40 +0200 Subject: [PATCH 315/567] Update logo --- .../Assets/Plugins/GitHub/Editor/big-logo@2x.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png b/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png index 4618fffc5..9665a0090 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/big-logo@2x.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:267b005091b31403c42d9a5d4236fb08b69280c98ccc4c770e2092456983c480 -size 54108 +oid sha256:e986d8a21f6f621e89885d0e4e28a9c6171e02f2c095fbeb075e22d9b0f40f74 +size 9961 From 43e1e78712a11ce80f1cde3d6542cb37b978b80e Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 4 Jun 2018 12:50:24 +0200 Subject: [PATCH 316/567] Bump version to 1.0.0rc4 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 9d03c1349..f06012e7b 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -34,6 +34,6 @@ internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0rc3"; + internal const string Version = "1.0.0rc4"; } } From 5a6a09312ad3a9d41c6a758efc79a57ad8fa09d3 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 4 Jun 2018 12:15:50 +0200 Subject: [PATCH 317/567] Making sure running processes don't hang Unity when the domain reloads, part deux Also ensuring that we get data from git commands that output progress in stderr --- .gitignore | 3 +- .../Application/ApplicationConfiguration.cs | 2 + .../Application/ApplicationManagerBase.cs | 45 ++----- src/GitHub.Api/Git/GitClient.cs | 10 +- src/GitHub.Api/Git/GitCountObjects.cs | 23 ---- src/GitHub.Api/Git/Repository.cs | 6 +- src/GitHub.Api/Git/RepositoryManager.cs | 10 ++ .../Git/Tasks/GitCountObjectsTask.cs | 6 +- src/GitHub.Api/Git/Tasks/GitListLocksTask.cs | 5 +- src/GitHub.Api/Git/Tasks/GitLockTask.cs | 2 +- src/GitHub.Api/Git/Tasks/GitUnlockTask.cs | 2 +- src/GitHub.Api/GitHub.Api.csproj | 3 +- src/GitHub.Api/Helpers/Constants.cs | 1 + src/GitHub.Api/Metrics/UsageTracker.cs | 39 +++++- .../BranchListOutputProcessor.cs | 50 ++++---- .../GitCountObjectsProcessor.cs | 20 +-- .../OutputProcessors/IProcessManager.cs | 3 +- .../LinuxDiskUsageOutputProcessor.cs | 23 +--- .../OutputProcessors/ProcessManager.cs | 16 ++- src/GitHub.Api/Platform/LinuxDiskUsageTask.cs | 6 +- src/GitHub.Api/Platform/ProcessEnvironment.cs | 3 +- src/GitHub.Api/Tasks/ProcessTask.cs | 116 +++++++++++++----- src/GitHub.Logging/GitHub.Logging.csproj | 2 +- .../Editor/GitHub.Unity/ApplicationCache.cs | 30 ++++- .../Editor/GitHub.Unity/GitHub.Unity.csproj | 2 +- .../Editor/GitHub.Unity/UI/BaseWindow.cs | 32 +++++ .../Assets/Editor/GitHub.Unity/UI/IView.cs | 4 + .../Editor/GitHub.Unity/UI/LocksView.cs | 4 +- .../Editor/GitHub.Unity/UI/SettingsView.cs | 74 +++++------ .../Assets/Editor/GitHub.Unity/UI/Subview.cs | 42 +++---- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 1 + .../IntegrationTests/BaseIntegrationTest.cs | 1 + src/tests/IntegrationTests/UnzipTaskTests.cs | 5 +- .../TestUtils/Helpers/AssertExtensions.cs | 6 - .../UnitTests/IO/CountObjectProcessorTests.cs | 10 +- 35 files changed, 356 insertions(+), 251 deletions(-) delete mode 100644 src/GitHub.Api/Git/GitCountObjects.cs diff --git a/.gitignore b/.gitignore index d9015e717..67d4e53bc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ _NCrunch_GitHub.Unity build/ TestResult.xml submodules/ -*.stackdump \ No newline at end of file +*.stackdump +*.lastcodeanalysissucceeded \ No newline at end of file diff --git a/src/GitHub.Api/Application/ApplicationConfiguration.cs b/src/GitHub.Api/Application/ApplicationConfiguration.cs index 577aa938f..5c8358303 100644 --- a/src/GitHub.Api/Application/ApplicationConfiguration.cs +++ b/src/GitHub.Api/Application/ApplicationConfiguration.cs @@ -5,6 +5,8 @@ namespace GitHub.Unity public static class ApplicationConfiguration { public const int DefaultWebTimeout = 3000; + public const int DefaultGitTimeout = 5000; public static int WebTimeout { get; set; } = DefaultWebTimeout; + public static int GitTimeout { get; set; } = DefaultGitTimeout; } } diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 8cb135fe8..e009573d2 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -7,7 +7,7 @@ namespace GitHub.Unity { - abstract class ApplicationManagerBase : IApplicationManager + class ApplicationManagerBase : IApplicationManager { protected static ILogging Logger { get; } = LogHelper.GetLogger(); @@ -45,9 +45,10 @@ protected void Initialize() { LogHelper.TracingEnabled = UserSettings.Get(Constants.TraceLoggingKey, false); ApplicationConfiguration.WebTimeout = UserSettings.Get(Constants.WebTimeoutKey, ApplicationConfiguration.WebTimeout); + ApplicationConfiguration.GitTimeout = UserSettings.Get(Constants.GitTimeoutKey, ApplicationConfiguration.GitTimeout); Platform.Initialize(ProcessManager, TaskManager); progress.OnProgress += progressReporter.UpdateProgress; - UsageTracker = new UsageTracker(TaskManager, UserSettings, Environment, InstanceId.ToString()); + UsageTracker = new UsageTracker(TaskManager, GitClient, ProcessManager, UserSettings, Environment, InstanceId.ToString()); #if ENABLE_METRICS var metricsService = new MetricsService(ProcessManager, @@ -191,7 +192,6 @@ public void SetupGit(GitInstaller.GitInstallationState state) if (Environment.RepositoryPath.IsInitialized) { ConfigureMergeSettings(); - CaptureRepoSize(); GitClient.LfsInstall() .Catch(e => @@ -300,37 +300,6 @@ private void ConfigureMergeSettings() }).RunSynchronously(); } - private void CaptureRepoSize() - { - GitClient.CountObjects() - .Finally((success, gitObjects) => - { - if (success) - { - UsageTracker.UpdateRepoSize(gitObjects.kilobytes); - } - }) - .Start(); - - var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); - if (gitLfsDataPath.Exists()) - { - var diskUsageTask = Environment.IsWindows - ? (IProcessTask)new WindowsDiskUsageTask(gitLfsDataPath, TaskManager.Token) - : new LinuxDiskUsageTask(gitLfsDataPath, TaskManager.Token); - - diskUsageTask - .Configure(ProcessManager) - .Finally((success, kilobytes) => - { - if (success) - { - UsageTracker.UpdateLfsDiskUsage(kilobytes); - } - }).Start(); - } - } - public void RestartRepository() { if (!Environment.RepositoryPath.IsInitialized) @@ -346,8 +315,8 @@ public void RestartRepository() Logger.Trace($"Got a repository? {(Environment.Repository != null ? Environment.Repository.LocalPath : "null")}"); } - protected abstract void InitializeUI(); - protected abstract void InitializationComplete(); + protected virtual void InitializeUI() {} + protected virtual void InitializationComplete() {} private bool disposed = false; protected virtual void Dispose(bool disposing) @@ -356,6 +325,10 @@ protected virtual void Dispose(bool disposing) { if (disposed) return; disposed = true; + if (ProcessManager != null) + { + ProcessManager.Stop(); + } if (TaskManager != null) { TaskManager.Dispose(); diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 827bd2006..8dc75d6b6 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -38,7 +38,7 @@ public interface IGitClient ITask> Log(BaseOutputListProcessor processor = null); ITask Version(IOutputProcessor processor = null); ITask LfsVersion(IOutputProcessor processor = null); - ITask CountObjects(IOutputProcessor processor = null); + ITask CountObjects(IOutputProcessor processor = null); ITask SetConfigNameAndEmail(string username, string email); ITask GetHead(IOutputProcessor processor = null); } @@ -100,7 +100,7 @@ public ITask LfsVersion(IOutputProcessor processor = nul .Configure(processManager); } - public ITask CountObjects(IOutputProcessor processor = null) + public ITask CountObjects(IOutputProcessor processor = null) { return new GitCountObjectsTask(cancellationToken, processor) .Configure(processManager); @@ -151,7 +151,7 @@ public ITask SetConfigNameAndEmail(string username, string email) public ITask> ListLocks(bool local, BaseOutputListProcessor processor = null) { return new GitListLocksTask(local, cancellationToken, processor) - .Configure(processManager); + .Configure(processManager, environment.GitLfsExecutablePath); } public ITask Pull(string remote, string branch, IOutputProcessor processor = null) @@ -301,14 +301,14 @@ public ITask Lock(NPath file, IOutputProcessor processor = null) { return new GitLockTask(file, cancellationToken, processor) - .Configure(processManager); + .Configure(processManager, environment.GitLfsExecutablePath); } public ITask Unlock(NPath file, bool force, IOutputProcessor processor = null) { return new GitUnlockTask(file, force, cancellationToken, processor) - .Configure(processManager); + .Configure(processManager, environment.GitLfsExecutablePath); } public ITask GetHead(IOutputProcessor processor = null) diff --git a/src/GitHub.Api/Git/GitCountObjects.cs b/src/GitHub.Api/Git/GitCountObjects.cs deleted file mode 100644 index 61ba720b7..000000000 --- a/src/GitHub.Api/Git/GitCountObjects.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace GitHub.Unity -{ - [Serializable] - public struct GitCountObjects - { - public static GitCountObjects Default = new GitCountObjects(); - - public int objects; - public int kilobytes; - - public GitCountObjects(int objects, int kilobytes) - { - this.objects = objects; - this.kilobytes = kilobytes; - } - - public int Objects => objects; - - public int Kilobytes => kilobytes; - } -} \ No newline at end of file diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 926afcf70..f9d6fe38e 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -188,9 +188,6 @@ public void Refresh(CacheType cacheType) { var cache = cacheContainer.GetCache(cacheType); cache.InvalidateData(); - - // Ensuring that the GitLock cache is kept up to date - cacheContainer.GetCache(CacheType.GitLocks).ValidateData(); } private void CacheHasBeenInvalidated(CacheType cacheType) @@ -309,7 +306,6 @@ private static GitBranch GetLocalGitBranch(string currentBranchName, ConfigBranc { var branchName = x.Name; var trackingName = x.IsTracking ? x.Remote.Value.Name + "/" + branchName : "[None]"; - var isActive = branchName == currentBranchName; return new GitBranch(branchName, trackingName); } @@ -504,4 +500,4 @@ public string Email protected static ILogging Logger { get; } = LogHelper.GetLogger(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index 42854f2fa..63c08ea49 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -195,6 +195,11 @@ public ITask CommitFiles(List files, string message, string body) public ITask Fetch(string remote) { var task = GitClient.Fetch(remote); + task.OnEnd += (_, __, success, ___) => + { + if (success) + UpdateGitAheadBehindStatus().Start(); + }; return HookupHandlers(task, false); } @@ -207,6 +212,11 @@ public ITask Pull(string remote, string branch) public ITask Push(string remote, string branch) { var task = GitClient.Push(remote, branch); + task.OnEnd += (_, __, success, ___) => + { + if (success) + UpdateGitAheadBehindStatus().Start(); + }; return HookupHandlers(task, false); } diff --git a/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs b/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs index 0ee157fe7..6489e8574 100644 --- a/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCountObjectsTask.cs @@ -2,11 +2,11 @@ namespace GitHub.Unity { - class GitCountObjectsTask : ProcessTask + class GitCountObjectsTask : ProcessTask { private const string TaskName = "git count-objects"; - public GitCountObjectsTask(CancellationToken token, IOutputProcessor processor = null) + public GitCountObjectsTask(CancellationToken token, IOutputProcessor processor = null) : base(token, processor ?? new GitCountObjectsProcessor()) { Name = TaskName; @@ -19,4 +19,4 @@ public override string ProcessArguments public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } public override string Message { get; set; } = "Counting git objects..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs b/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs index 2cf39d68c..134cb78b2 100644 --- a/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitListLocksTask.cs @@ -4,18 +4,19 @@ namespace GitHub.Unity { class GitListLocksTask : ProcessTaskWithListOutput { + private const string TaskName = "git lfs locks"; private readonly string args; public GitListLocksTask(bool local, CancellationToken token, BaseOutputListProcessor processor = null) : base(token, processor ?? new LocksOutputProcessor()) { - args = "lfs locks --json"; + Name = TaskName; + args = "locks --json"; if (local) { args += " --local"; } - Name = args; } public override string ProcessArguments => args; diff --git a/src/GitHub.Api/Git/Tasks/GitLockTask.cs b/src/GitHub.Api/Git/Tasks/GitLockTask.cs index a2353957c..e77b371ce 100644 --- a/src/GitHub.Api/Git/Tasks/GitLockTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLockTask.cs @@ -14,7 +14,7 @@ public GitLockTask(string path, { Name = TaskName; Guard.ArgumentNotNullOrWhiteSpace(path, "path"); - arguments = String.Format("lfs lock \"{0}\"", path); + arguments = String.Format("lock \"{0}\"", path); } public override string ProcessArguments => arguments; diff --git a/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs b/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs index e2a423bcd..94ac54e40 100644 --- a/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitUnlockTask.cs @@ -15,7 +15,7 @@ public GitUnlockTask(NPath path, bool force, Guard.ArgumentNotNullOrWhiteSpace(path, "path"); Name = TaskName; - var stringBuilder = new StringBuilder("lfs unlock "); + var stringBuilder = new StringBuilder("unlock "); if (force) { diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index a296a9ab0..4b2137e88 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -25,7 +25,7 @@ DEBUG;TRACE;$(BuildDefs) prompt 4 - true + false false true @@ -91,7 +91,6 @@ - diff --git a/src/GitHub.Api/Helpers/Constants.cs b/src/GitHub.Api/Helpers/Constants.cs index 19cec7a19..4380a57c9 100644 --- a/src/GitHub.Api/Helpers/Constants.cs +++ b/src/GitHub.Api/Helpers/Constants.cs @@ -10,6 +10,7 @@ static class Constants public const string GitInstallPathKey = "GitInstallPath"; public const string TraceLoggingKey = "EnableTraceLogging"; public const string WebTimeoutKey = "WebTimeout"; + public const string GitTimeoutKey = "GitTimeout"; public const string Iso8601Format = @"yyyy-MM-dd\THH\:mm\:ss.fffzzz"; public const string Iso8601FormatZ = @"yyyy-MM-dd\THH\:mm\:ss\Z"; public static readonly string[] Iso8601Formats = { diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 66bcd0150..08c515b21 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -105,15 +105,21 @@ private void SendUsage() } // if we're here, success! - lock(_lock) + lock (_lock) { usageStore = usageLoader.Load(userId); usageStore.LastSubmissionDate = currentTimeOffset; usageStore.Model.RemoveReports(currentTimeOffset.Date); usageLoader.Save(usageStore); } + + // update the repo size for the current report, while we're at it + CaptureRepoSize(); } + protected virtual void CaptureRepoSize() + {} + public virtual void IncrementNumberOfStartups() { lock (_lock) @@ -336,13 +342,39 @@ public bool Enabled class UsageTracker : UsageTrackerSync { - public UsageTracker(ITaskManager taskManager, ISettings userSettings, + public UsageTracker(ITaskManager taskManager, IGitClient gitClient, IProcessManager processManager, + ISettings userSettings, IEnvironment environment, string instanceId) : base(userSettings, new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), environment.UnityVersion, instanceId) { TaskManager = taskManager; + Environment = environment; + GitClient = gitClient; + ProcessManager = processManager; + } + + protected override void CaptureRepoSize() + { + try + { + var gitSize = GitClient.CountObjects() + .Catch(_ => true) + .RunSynchronously(); + base.UpdateRepoSize(gitSize); + + var gitLfsDataPath = Environment.RepositoryPath.Combine(".git", "lfs"); + if (gitLfsDataPath.Exists()) + { + var lfsSize = new LinuxDiskUsageTask(gitLfsDataPath, TaskManager.Token) + .Configure(ProcessManager) + .Catch(_ => true) + .RunSynchronously(); + base.UpdateLfsDiskUsage(lfsSize); + } + } + catch {} } public override void IncrementApplicationMenuMenuItemCommandLine() => TaskManager.Run(base.IncrementApplicationMenuMenuItemCommandLine); @@ -365,6 +397,9 @@ public UsageTracker(ITaskManager taskManager, ISettings userSettings, public override void UpdateRepoSize(int kilobytes) => TaskManager.Run(() => base.UpdateRepoSize(kilobytes)); protected ITaskManager TaskManager { get; } + protected IEnvironment Environment { get; } + protected IGitClient GitClient { get; } + public IProcessManager ProcessManager { get; } } interface IUsageLoader diff --git a/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs index 320ea9435..2a46e7474 100644 --- a/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs @@ -1,3 +1,4 @@ +using System; using System.Text.RegularExpressions; namespace GitHub.Unity @@ -15,30 +16,37 @@ public override void LineReceived(string line) if (proc.IsAtEnd) return; - var active = proc.Matches('*'); - proc.SkipWhitespace(); - var detached = proc.Matches("(HEAD "); - var name = "detached"; - if (detached) + try { - proc.MoveToAfter(')'); - } - else - { - name = proc.ReadUntilWhitespace(); + proc.Matches('*'); + proc.SkipWhitespace(); + var detached = proc.Matches("(HEAD "); + var name = "detached"; + if (detached) + { + proc.MoveToAfter(')'); + } + else + { + name = proc.ReadUntilWhitespace(); + } + proc.SkipWhitespace(); + proc.ReadUntilWhitespace(); + var tracking = proc.Matches(trackingBranchRegex); + var trackingName = ""; + if (tracking) + { + trackingName = proc.ReadChunk('[', ']'); + } + + var branch = new GitBranch(name, trackingName); + + RaiseOnEntry(branch); } - proc.SkipWhitespace(); - proc.ReadUntilWhitespace(); - var tracking = proc.Matches(trackingBranchRegex); - var trackingName = ""; - if (tracking) + catch(Exception ex) { - trackingName = proc.ReadChunk('[', ']'); + Logger.Warning(ex, "Unexpected input when listing branches"); } - - var branch = new GitBranch(name, trackingName); - - RaiseOnEntry(branch); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs b/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs index 4d544e877..56b4d96ff 100644 --- a/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/GitCountObjectsProcessor.cs @@ -1,6 +1,6 @@ namespace GitHub.Unity { - public class GitCountObjectsProcessor : BaseOutputProcessor + public class GitCountObjectsProcessor : BaseOutputProcessor { public override void LineReceived(string line) { @@ -11,14 +11,18 @@ public override void LineReceived(string line) //2488 objects, 4237 kilobytes - var proc = new LineParser(line); + try + { + var proc = new LineParser(line); - var objects = int.Parse(proc.ReadUntilWhitespace()); - proc.ReadUntil(','); - proc.SkipWhitespace(); - var kilobytes = int.Parse(proc.ReadUntilWhitespace()); + proc.ReadUntil(','); + proc.SkipWhitespace(); + var kilobytes = int.Parse(proc.ReadUntilWhitespace()); - RaiseOnEntry(new GitCountObjects(objects, kilobytes)); + RaiseOnEntry(kilobytes); + } + catch {} + return; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/IProcessManager.cs b/src/GitHub.Api/OutputProcessors/IProcessManager.cs index a41b7ccc5..c46ff495d 100644 --- a/src/GitHub.Api/OutputProcessors/IProcessManager.cs +++ b/src/GitHub.Api/OutputProcessors/IProcessManager.cs @@ -10,5 +10,6 @@ T Configure(T processTask, NPath? executable = null, string arguments = null, IProcess Reconnect(IProcess processTask, int i); CancellationToken CancellationToken { get; } void RunCommandLineWindow(NPath workingDirectory); + void Stop(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs index 782d41bd9..0ce27f2ad 100644 --- a/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/LinuxDiskUsageOutputProcessor.cs @@ -4,26 +4,15 @@ namespace GitHub.Unity { public class LinuxDiskUsageOutputProcessor : BaseOutputProcessor { - private string buffer; - public override void LineReceived(string line) { if (line == null) - { - if (buffer == null) - { - throw new InvalidOperationException("Not enough input"); - } - - var proc = new LineParser(buffer); - var kilobytes = int.Parse(proc.ReadUntilWhitespace()); + return; - RaiseOnEntry(kilobytes); - } - else - { - buffer = line; - } + int kb; + var proc = new LineParser(line); + if (int.TryParse(proc.ReadUntilWhitespace(), out kb)) + RaiseOnEntry(kb); } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/ProcessManager.cs b/src/GitHub.Api/OutputProcessors/ProcessManager.cs index 017b20a66..f67c6a6c2 100644 --- a/src/GitHub.Api/OutputProcessors/ProcessManager.cs +++ b/src/GitHub.Api/OutputProcessors/ProcessManager.cs @@ -1,8 +1,6 @@ using GitHub.Logging; -using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Text; using System.Threading; @@ -16,6 +14,7 @@ class ProcessManager : IProcessManager private readonly IEnvironment environment; private readonly IProcessEnvironment gitEnvironment; private readonly CancellationToken cancellationToken; + private readonly HashSet processes = new HashSet(); public ProcessManager(IEnvironment environment, IProcessEnvironment gitEnvironment, CancellationToken cancellationToken) { @@ -49,7 +48,7 @@ public T Configure(T processTask, NPath? executable = null, string arguments gitEnvironment.Configure(startInfo, workingDirectory ?? environment.RepositoryPath, dontSetupGit); string filename = executable.Value; - if (executable.Value.IsRelative && filename != "where" && filename != "which") + if (executable.Value.IsRelative && filename.StartsWith("git")) { var file = FindExecutableInPath(executable.Value.FileName, false, startInfo.EnvironmentVariables["PATH"].ToNPathList(environment).ToArray()); filename = file.IsInitialized ? file : executable.Value.FileName; @@ -57,6 +56,11 @@ public T Configure(T processTask, NPath? executable = null, string arguments startInfo.FileName = filename; startInfo.Arguments = arguments ?? processTask.ProcessArguments; processTask.Configure(startInfo); + processTask.OnStartProcess += p => processes.Add(p); + processTask.OnEndProcess += p => { + if (processes.Contains(p)) + processes.Remove(p); + }; return processTask; } @@ -114,6 +118,12 @@ public IProcess Reconnect(IProcess processTask, int pid) return processTask; } + public void Stop() + { + foreach (var p in processes.ToArray()) + p.Stop(); + } + public static NPath FindExecutableInPath(string executable, bool recurse = false, params NPath[] searchPaths) { Guard.ArgumentNotNullOrWhiteSpace(executable, "executable"); diff --git a/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs b/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs index 5f8d07bad..962f62f1a 100644 --- a/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs +++ b/src/GitHub.Api/Platform/LinuxDiskUsageTask.cs @@ -9,8 +9,8 @@ class LinuxDiskUsageTask : ProcessTask public LinuxDiskUsageTask(NPath directory, CancellationToken token) : base(token, new LinuxDiskUsageOutputProcessor()) { - Name = "du"; - arguments = string.Format("-h \"{0}\"", directory); + Name = "du" + DefaultEnvironment.ExecutableExt; + arguments = string.Format("-sH \"{0}\"", directory); } public override string ProcessName { get { return Name; } } @@ -18,4 +18,4 @@ public LinuxDiskUsageTask(NPath directory, CancellationToken token) public override TaskAffinity Affinity { get { return TaskAffinity.Concurrent; } } public override string Message { get; set; } = "Getting directory size..."; } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Platform/ProcessEnvironment.cs b/src/GitHub.Api/Platform/ProcessEnvironment.cs index 0f094f2cb..b1609faa3 100644 --- a/src/GitHub.Api/Platform/ProcessEnvironment.cs +++ b/src/GitHub.Api/Platform/ProcessEnvironment.cs @@ -108,6 +108,7 @@ public void Configure(ProcessStartInfo psi, NPath workingDirectory, bool dontSet var httpsProxy = Environment.GetEnvironmentVariable("HTTPS_PROXY"); if (!String.IsNullOrEmpty(httpsProxy)) psi.EnvironmentVariables["HTTPS_PROXY"] = httpsProxy; + psi.EnvironmentVariables["DISPLAY"] = "0"; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index 62233fa14..bbf56ebbf 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -6,7 +6,6 @@ using System.IO; using System.Text; using System.Threading; -using System.Threading.Tasks; namespace GitHub.Unity { @@ -39,6 +38,7 @@ public interface IProcess { void Configure(Process existingProcess); void Configure(ProcessStartInfo psi); + void Stop(); event Action OnErrorData; StreamWriter StandardInput { get; } int ProcessId { get; } @@ -90,7 +90,9 @@ public ProcessWrapper(string taskName, Process process, IOutputProcessor outputP public void Run() { + DateTimeOffset lastOutput = DateTimeOffset.UtcNow; Exception thrownException = null; + var gotOutput = new AutoResetEvent(false); if (Process.StartInfo.RedirectStandardError) { Process.ErrorDataReceived += (s, e) => @@ -100,18 +102,36 @@ public void Run() // Logger.Trace("ErrorData \"" + (e.Data == null ? "'null'" : e.Data) + "\""); //} - string encodedData = null; + lastOutput = DateTimeOffset.UtcNow; + gotOutput.Set(); if (e.Data != null) { - encodedData = Encoding.UTF8.GetString(Encoding.Default.GetBytes(e.Data)); - errors.Add(encodedData); + var line = Encoding.UTF8.GetString(Encoding.Default.GetBytes(e.Data)); + errors.Add(line.TrimEnd('\r', '\n')); + Logger.Trace(line); } }; } + if (Process.StartInfo.RedirectStandardOutput) + { + Process.OutputDataReceived += (s, e) => + { + lastOutput = DateTimeOffset.UtcNow; + gotOutput.Set(); + if (e.Data != null) + { + var line = Encoding.UTF8.GetString(Encoding.Default.GetBytes(e.Data)); + outputProcessor.LineReceived(line.TrimEnd('\r','\n')); + } + else + outputProcessor.LineReceived(null); + }; + } + try { - Logger.Trace($"Running '{Process.StartInfo.FileName} {taskName}'"); + Logger.Trace($"Running '{Process.StartInfo.FileName} {Process.StartInfo.Arguments}'"); token.ThrowIfCancellationRequested(); Process.Start(); @@ -120,40 +140,29 @@ public void Run() Input = new StreamWriter(Process.StandardInput.BaseStream, new UTF8Encoding(false)); if (Process.StartInfo.RedirectStandardError) Process.BeginErrorReadLine(); + if (Process.StartInfo.RedirectStandardOutput) + Process.BeginOutputReadLine(); onStart?.Invoke(); - if (Process.StartInfo.RedirectStandardOutput) + if (Process.StartInfo.CreateNoWindow) { - var outputStream = Process.StandardOutput; - var line = outputStream.ReadLine(); - while (line != null) + bool done = false; + while (!done) { - outputProcessor.LineReceived(line); - - if (token.IsCancellationRequested) + var exited = WaitForExit(500); + if (exited) { - if (!Process.HasExited) - Process.Kill(); - Process.Close(); - token.ThrowIfCancellationRequested(); + // process is done and we haven't seen output, we're done + done = !gotOutput.WaitOne(100); } - - line = outputStream.ReadLine(); - } - outputProcessor.LineReceived(null); - } - - if (Process.StartInfo.CreateNoWindow) - { - while (!WaitForExit(500)) - { - if (token.IsCancellationRequested) + else if (token.IsCancellationRequested || (taskName.Contains("git lfs") && lastOutput.AddMilliseconds(ApplicationConfiguration.DefaultGitTimeout) < DateTimeOffset.UtcNow)) + // if we're exiting or we haven't had output for a while { - Process.Kill(); - Process.Close(); + Stop(true); + token.ThrowIfCancellationRequested(); + throw new ProcessException(-2, "Process timed out"); } - token.ThrowIfCancellationRequested(); } if (Process.ExitCode != 0 && errors.Count > 0) @@ -190,6 +199,43 @@ public void Run() onEnd?.Invoke(); } + public void Stop(bool dontWait = false) + { + try + { + if (Process.StartInfo.RedirectStandardError) + Process.CancelErrorRead(); + if (Process.StartInfo.RedirectStandardOutput) + Process.CancelOutputRead(); + if (!Process.HasExited && Process.StartInfo.RedirectStandardInput) + Input.WriteLine("\x3"); + } + catch + {} + + try + { + + if (!Process.HasExited) + { + Process.Kill(); + } + + if (!dontWait) + { + bool waitSucceeded = Process.WaitForExit(500); + if (waitSucceeded) + { + Process.Close(); + } + } + } + catch(Exception ex) + { + Logger.Trace(ex); + } + } + private bool WaitForExit(int milliseconds) { //Logger.Debug("WaitForExit - time: {0}ms", milliseconds); @@ -279,6 +325,11 @@ public void Configure(Process existingProcess) Name = ProcessArguments; } + public void Stop() + { + wrapper?.Stop(); + } + protected override void RaiseOnEnd() { base.RaiseOnEnd(); @@ -397,6 +448,11 @@ public virtual void Configure(ProcessStartInfo psi, IOutputProcessor> ProcessName = psi.FileName; } + public void Stop() + { + wrapper?.Stop(); + } + protected override void RaiseOnEnd() { base.RaiseOnEnd(); diff --git a/src/GitHub.Logging/GitHub.Logging.csproj b/src/GitHub.Logging/GitHub.Logging.csproj index ef69bfeac..5456d2117 100644 --- a/src/GitHub.Logging/GitHub.Logging.csproj +++ b/src/GitHub.Logging/GitHub.Logging.csproj @@ -21,7 +21,7 @@ false DEBUG;TRACE prompt - true + false false true diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs index d316fdf9d..0a2160be2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ApplicationCache.cs @@ -599,7 +599,15 @@ public List Log var now = DateTimeOffset.Now; var isUpdated = false; - if (forcedInvalidation || !log.SequenceEqual(value)) + if (value == null) + { + if (forcedInvalidation || log.Count > 0) + { + log.Clear(); + isUpdated = true; + } + } + else if (forcedInvalidation || !log.SequenceEqual(value)) { log = value; isUpdated = true; @@ -688,7 +696,15 @@ public List Entries var now = DateTimeOffset.Now; var isUpdated = false; - if (forcedInvalidation || !entries.SequenceEqual(value)) + if (value == null) + { + if (forcedInvalidation || entries.Count > 0) + { + entries.Clear(); + isUpdated = true; + } + } + else if (forcedInvalidation || !entries.SequenceEqual(value)) { entries = value; isUpdated = true; @@ -721,7 +737,15 @@ public List GitLocks var now = DateTimeOffset.Now; var isUpdated = false; - if (forcedInvalidation || !gitLocks.SequenceEqual(value)) + if (value == null) + { + if (forcedInvalidation || gitLocks.Count > 0) + { + gitLocks.Clear(); + isUpdated = true; + } + } + else if (forcedInvalidation || !gitLocks.SequenceEqual(value)) { gitLocks = value; isUpdated = true; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index b0f2f4e03..95b438d79 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -23,7 +23,7 @@ prompt 4 4 - true + false false true diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index 9d2836ad9..fa21bebf3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -1,5 +1,7 @@ using GitHub.Logging; using System; +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEngine; @@ -13,6 +15,11 @@ abstract class BaseWindow : EditorWindow, IView [NonSerialized] private bool initializeWasCalled; [NonSerialized] protected bool inLayout; + public BaseWindow() + { + RefreshEvents = new Dictionary(); + } + public virtual void Initialize(IApplicationManager applicationManager) { } @@ -117,6 +124,30 @@ public virtual void DoneRefreshing() IsRefreshing = false; } + public void Refresh(CacheType type) + { + if (Repository == null) + return; + + IsRefreshing = true; + if (!RefreshEvents.ContainsKey(type)) + RefreshEvents.Add(type, 0); + RefreshEvents[type]++; + Repository.Refresh(type); + } + + public void ReceivedEvent(CacheType type) + { + if (!RefreshEvents.ContainsKey(type)) + RefreshEvents.Add(type, 0); + var val = RefreshEvents[type] - 1; + RefreshEvents[type] = val > -1 ? val : 0; + if (IsRefreshing && !RefreshEvents.Values.Any(x => x > 0)) + { + DoneRefreshing(); + } + } + public virtual void DoEmptyGUI() {} public virtual void DoProgressGUI() @@ -138,6 +169,7 @@ public virtual void UpdateProgress(IProgress progress) protected IGitClient GitClient { get { return Manager.GitClient; } } protected IEnvironment Environment { get { return Manager.Environment; } } protected IPlatform Platform { get { return Manager.Platform; } } + public Dictionary RefreshEvents { get; set; } private ILogging logger; protected ILogging Logger { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs index f20b1e6e2..4b284f519 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/IView.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using UnityEngine; namespace GitHub.Unity @@ -9,6 +10,8 @@ interface IView : IUIEmpty, IUIProgress void OnDisable(); void Refresh(); void Redraw(); + void Refresh(CacheType type); + void ReceivedEvent(CacheType type); void DoneRefreshing(); Rect Position { get; } @@ -21,6 +24,7 @@ interface IView : IUIEmpty, IUIProgress bool IsBusy { get; } bool IsRefreshing { get; } bool HasFocus { get; } + Dictionary RefreshEvents { get; } } interface IUIEmpty diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 9e21c6b36..68208cc74 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -418,8 +418,8 @@ public override void OnDisable() public override void Refresh() { base.Refresh(); - Repository.Refresh(CacheType.GitStatus); - Repository.Refresh(CacheType.GitLocks); + Refresh(CacheType.GitStatus); + Refresh(CacheType.GitLocks); } public override void OnDataUpdate() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs index fe5146dc0..890fb09c7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/SettingsView.cs @@ -18,6 +18,7 @@ class SettingsView : Subview private const string DebugSettingsTitle = "Debug"; private const string PrivacyTitle = "Privacy"; private const string WebTimeoutLabel = "Timeout of web requests"; + private const string GitTimeoutLabel = "Timeout of git commands"; private const string EnableTraceLoggingLabel = "Enable Trace Logging"; private const string MetricsOptInLabel = "Help us improve by sending anonymous usage data"; private const string DefaultRepositoryRemoteName = "origin"; @@ -36,6 +37,7 @@ class SettingsView : Subview [SerializeField] private Vector2 scroll; [SerializeField] private UserSettingsView userSettingsView = new UserSettingsView(); [SerializeField] private int webTimeout; + [SerializeField] private int gitTimeout; public override void InitializeView(IView parent) { @@ -82,7 +84,6 @@ public override void Refresh() gitPathView.Refresh(); userSettingsView.Refresh(); Refresh(CacheType.RepositoryInfo); - Refresh(CacheType.GitLocks); } public override void OnGUI() @@ -212,18 +213,14 @@ private void OnPrivacyGui() { GUILayout.Label(PrivacyTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(IsBusy && Manager.UsageTracker != null); + EditorGUI.BeginChangeCheck(); { - - EditorGUI.BeginChangeCheck(); - { - metricsEnabled = GUILayout.Toggle(metricsEnabled, MetricsOptInLabel); - } - if (EditorGUI.EndChangeCheck()) - { - if (Manager.UsageTracker != null) - Manager.UsageTracker.Enabled = metricsEnabled; - } + metricsEnabled = GUILayout.Toggle(metricsEnabled, MetricsOptInLabel); + } + if (EditorGUI.EndChangeCheck()) + { + if (Manager.UsageTracker != null) + Manager.UsageTracker.Enabled = metricsEnabled; } EditorGUI.EndDisabledGroup(); } @@ -232,41 +229,44 @@ private void OnLoggingSettingsGui() { GUILayout.Label(DebugSettingsTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(IsBusy); - { - var traceLogging = LogHelper.TracingEnabled; + var traceLogging = LogHelper.TracingEnabled; - EditorGUI.BeginChangeCheck(); - { - traceLogging = GUILayout.Toggle(traceLogging, EnableTraceLoggingLabel); - } - if (EditorGUI.EndChangeCheck()) - { - LogHelper.TracingEnabled = traceLogging; - Manager.UserSettings.Set(Constants.TraceLoggingKey, traceLogging); - } + EditorGUI.BeginChangeCheck(); + { + traceLogging = GUILayout.Toggle(traceLogging, EnableTraceLoggingLabel); + } + if (EditorGUI.EndChangeCheck()) + { + LogHelper.TracingEnabled = traceLogging; + Manager.UserSettings.Set(Constants.TraceLoggingKey, traceLogging); } - EditorGUI.EndDisabledGroup(); } private void OnGeneralSettingsGui() { GUILayout.Label(GeneralSettingsTitle, EditorStyles.boldLabel); - EditorGUI.BeginDisabledGroup(IsBusy); + webTimeout = ApplicationConfiguration.WebTimeout; + EditorGUI.BeginChangeCheck(); { - webTimeout = ApplicationConfiguration.WebTimeout; - EditorGUI.BeginChangeCheck(); - { - webTimeout = EditorGUILayout.IntField(WebTimeoutLabel, webTimeout); - } - if (EditorGUI.EndChangeCheck()) - { - ApplicationConfiguration.WebTimeout = webTimeout; - Manager.UserSettings.Set(Constants.WebTimeoutKey, webTimeout); - } + webTimeout = EditorGUILayout.IntField(WebTimeoutLabel, webTimeout); + } + if (EditorGUI.EndChangeCheck()) + { + ApplicationConfiguration.WebTimeout = webTimeout; + Manager.UserSettings.Set(Constants.WebTimeoutKey, webTimeout); + } + + gitTimeout = ApplicationConfiguration.GitTimeout; + EditorGUI.BeginChangeCheck(); + { + gitTimeout = EditorGUILayout.IntField(GitTimeoutLabel, gitTimeout); + } + if (EditorGUI.EndChangeCheck()) + { + ApplicationConfiguration.GitTimeout = gitTimeout; + Manager.UserSettings.Set(Constants.GitTimeoutKey, gitTimeout); } - EditorGUI.EndDisabledGroup(); } public override bool IsBusy diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index dc09a6e6c..c3c3f1514 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -11,7 +11,6 @@ abstract class Subview : IView public Subview() { - RefreshEvents = new Dictionary(); } public virtual void InitializeView(IView parent) @@ -66,58 +65,45 @@ public void UpdateProgress(IProgress progress) Parent.UpdateProgress(progress); } - protected void Refresh(CacheType type) + public void Refresh(CacheType type) { - if (Repository == null) - return; - - IsRefreshing = true; - if (!RefreshEvents.ContainsKey(type)) - RefreshEvents.Add(type, 0); - RefreshEvents[type]++; - Repository.Refresh(type); + Parent.Refresh(type); } - protected void ReceivedEvent(CacheType type) + public void ReceivedEvent(CacheType type) { - if (!RefreshEvents.ContainsKey(type)) - RefreshEvents.Add(type, 0); - var val = RefreshEvents[type] - 1; - RefreshEvents[type] = val > -1 ? val : 0; - if (IsRefreshing && !RefreshEvents.Values.Any(x => x > 0)) - { - DoneRefreshing(); - } + Parent.ReceivedEvent(type); } - public void DoneRefreshing() + public virtual void DoneRefreshing() { - IsRefreshing = false; Parent.DoneRefreshing(); } protected IView Parent { get; private set; } + public IApplicationManager Manager { get { return Parent.Manager; } } public IRepository Repository { get { return Parent.Repository; } } public bool HasRepository { get { return Parent.HasRepository; } } public IUser User { get { return Parent.User; } } public bool HasUser { get { return Parent.HasUser; } } + protected ITaskManager TaskManager { get { return Manager.TaskManager; } } + protected IGitClient GitClient { get { return Manager.GitClient; } } + protected IEnvironment Environment { get { return Manager.Environment; } } + protected IPlatform Platform { get { return Manager.Platform; } } + protected IUsageTracker UsageTracker { get { return Manager.UsageTracker; } } + public bool HasFocus { get { return Parent != null && Parent.HasFocus; } } public virtual bool IsBusy { get { return (Manager != null && Manager.IsBusy) || (Repository != null && Repository.IsBusy); } } - protected ITaskManager TaskManager { get { return Manager.TaskManager; } } - protected IGitClient GitClient { get { return Manager.GitClient; } } - protected IEnvironment Environment { get { return Manager.Environment; } } - protected IPlatform Platform { get { return Manager.Platform; } } - protected IUsageTracker UsageTracker { get { return Manager.UsageTracker; } } public Rect Position { get { return Parent.Position; } } public string Title { get; protected set; } public Vector2 Size { get; protected set; } - protected Dictionary RefreshEvents { get; set; } - public bool IsRefreshing { get; set; } + public Dictionary RefreshEvents { get { return Parent.RefreshEvents; } } + public bool IsRefreshing { get { return Parent.IsRefreshing; } } private ILogging logger; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 07d0b09fd..53a757910 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -195,6 +195,7 @@ public override void Refresh() base.Refresh(); if (ActiveView != null) ActiveView.Refresh(); + Refresh(CacheType.GitLocks); Redraw(); } diff --git a/src/tests/IntegrationTests/BaseIntegrationTest.cs b/src/tests/IntegrationTests/BaseIntegrationTest.cs index b5646a9d1..3041951e2 100644 --- a/src/tests/IntegrationTests/BaseIntegrationTest.cs +++ b/src/tests/IntegrationTests/BaseIntegrationTest.cs @@ -86,6 +86,7 @@ protected void InitializeTaskManager() TaskManager = new TaskManager(); SyncContext = new ThreadSynchronizationContext(TaskManager.Token); TaskManager.UIScheduler = new SynchronizationContextTaskScheduler(SyncContext); + ApplicationManager = new ApplicationManagerBase(SyncContext, Environment); } protected IEnvironment InitializePlatformAndEnvironment(NPath repoPath, diff --git a/src/tests/IntegrationTests/UnzipTaskTests.cs b/src/tests/IntegrationTests/UnzipTaskTests.cs index 5176ced43..0fa1a24ff 100644 --- a/src/tests/IntegrationTests/UnzipTaskTests.cs +++ b/src/tests/IntegrationTests/UnzipTaskTests.cs @@ -15,10 +15,9 @@ class UnzipTaskTests : BaseIntegrationTest [Test] public async Task UnzipWorks() { - InitializeTaskManager(); - var cacheContainer = Substitute.For(); Environment = new IntegrationTestEnvironment(cacheContainer, TestBasePath, SolutionDirectory); + InitializeTaskManager(); var destinationPath = TestBasePath.Combine("gitlfs_zip").CreateDirectory(); var archiveFilePath = AssemblyResources.ToFile(ResourceType.Platform, "git-lfs.zip", destinationPath, Environment); @@ -37,4 +36,4 @@ public async Task UnzipWorks() extractedPath.DirectoryExists().Should().BeTrue(); } } -} \ No newline at end of file +} diff --git a/src/tests/TestUtils/Helpers/AssertExtensions.cs b/src/tests/TestUtils/Helpers/AssertExtensions.cs index 6a5477b2b..e02f70c79 100644 --- a/src/tests/TestUtils/Helpers/AssertExtensions.cs +++ b/src/tests/TestUtils/Helpers/AssertExtensions.cs @@ -114,11 +114,5 @@ public static void AssertNotEqual(this GitStatus gitStatus, GitStatus other) Action action = () => gitStatus.AssertEqual(other); action.ShouldThrow(); } - - public static void AssertEqual(this GitCountObjects gitStatus, GitCountObjects other) - { - gitStatus.Objects.Should().Be(other.Objects, "Objects should be equal"); - gitStatus.Kilobytes.Should().Be(other.Kilobytes, "KilobytesS should be equal"); - } } } diff --git a/src/tests/UnitTests/IO/CountObjectProcessorTests.cs b/src/tests/UnitTests/IO/CountObjectProcessorTests.cs index 98234090a..906f02b8e 100644 --- a/src/tests/UnitTests/IO/CountObjectProcessorTests.cs +++ b/src/tests/UnitTests/IO/CountObjectProcessorTests.cs @@ -18,12 +18,12 @@ public void ShouldParseGitCountOutput() null }; - AssertProcessOutput(output, new GitCountObjects(2488, 4237)); + AssertProcessOutput(output, 4237); } - private void AssertProcessOutput(IEnumerable lines, GitCountObjects expected) + private void AssertProcessOutput(IEnumerable lines, int expected) { - GitCountObjects? result = null; + int? result = null; var outputProcessor = new GitCountObjectsProcessor(); outputProcessor.OnEntry += status => { result = status; }; @@ -33,7 +33,7 @@ private void AssertProcessOutput(IEnumerable lines, GitCountObjects expe } Assert.IsTrue(result.HasValue); - result.Value.AssertEqual(expected); + Assert.AreEqual(expected, result.Value); } } -} \ No newline at end of file +} From 0b655ef5b0ce72edc292de98bc30da6c8f89ca98 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 4 Jun 2018 14:46:50 +0200 Subject: [PATCH 318/567] Update git to not use gcm by default, since it locks up lfs --- src/GitHub.Api/PlatformResources/windows/git.json | 2 +- src/GitHub.Api/PlatformResources/windows/git.zip | 2 +- src/GitHub.Api/PlatformResources/windows/gitconfig | 5 +++++ src/tests/CommandLine/Program.cs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/PlatformResources/windows/git.json b/src/GitHub.Api/PlatformResources/windows/git.json index 9b633cbd9..a8f6a18c9 100644 --- a/src/GitHub.Api/PlatformResources/windows/git.json +++ b/src/GitHub.Api/PlatformResources/windows/git.json @@ -1 +1 @@ -{"md5":"75c42f674b9ff32ebe338ff204f907cf","url":"http://ghfvs-installer.github.com/unity/git/windows/git.zip","releaseNotes":null,"releaseNotesUrl":null,"message":null,"version":"2.17.0.windows.1"} +{"md5":"58ee14cb4ce8767167db64b271a0a599","url":"http://ghfvs-installer.github.com/unity/git/windows/git.zip","releaseNotes":null,"releaseNotesUrl":null,"message":null,"version":"2.17.0.1-windows.1"} \ No newline at end of file diff --git a/src/GitHub.Api/PlatformResources/windows/git.zip b/src/GitHub.Api/PlatformResources/windows/git.zip index e4e6544e7..7e5aff5b5 100644 --- a/src/GitHub.Api/PlatformResources/windows/git.zip +++ b/src/GitHub.Api/PlatformResources/windows/git.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e60087e32bfe32d897d2b20f013527be79a9596a280dc92bc793bcd744cc914 +oid sha256:b5fbf6c5a45df0297328402bdedaed95b33b6288e7b25257213f992a07e3ec67 size 97180593 diff --git a/src/GitHub.Api/PlatformResources/windows/gitconfig b/src/GitHub.Api/PlatformResources/windows/gitconfig index 07adb5f18..2d15e427f 100644 --- a/src/GitHub.Api/PlatformResources/windows/gitconfig +++ b/src/GitHub.Api/PlatformResources/windows/gitconfig @@ -16,5 +16,10 @@ textconv = astextplain [rebase] autosquash = true +[filter "lfs"] + clean = git-lfs clean -- %f + smudge = git-lfs smudge -- %f + process = git-lfs filter-process + required = true [credential] helper = wincred diff --git a/src/tests/CommandLine/Program.cs b/src/tests/CommandLine/Program.cs index a5e930ea7..6c76dcaac 100644 --- a/src/tests/CommandLine/Program.cs +++ b/src/tests/CommandLine/Program.cs @@ -158,7 +158,7 @@ static int Main(string[] args) if (outfile.IsInitialized) outfile.WriteAllText(json); else - Logger.Info(json); + Console.WriteLine(json); return 0; } From 506939b3adc422a5401c55ab235da9cddfecd43c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 4 Jun 2018 15:28:31 +0200 Subject: [PATCH 319/567] Bump version to 1.0.0rc5 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index f06012e7b..62edf1036 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -34,6 +34,6 @@ internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0rc4"; + internal const string Version = "1.0.0rc5"; } } From 9548857ecf3e0778ffbb3285f707d021b15fba10 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 4 Jun 2018 15:29:26 +0200 Subject: [PATCH 320/567] Fix regression when reading output in non-ascii locales --- src/GitHub.Api/Tasks/ProcessTask.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Tasks/ProcessTask.cs b/src/GitHub.Api/Tasks/ProcessTask.cs index bbf56ebbf..c6802c4bc 100644 --- a/src/GitHub.Api/Tasks/ProcessTask.cs +++ b/src/GitHub.Api/Tasks/ProcessTask.cs @@ -106,7 +106,7 @@ public void Run() gotOutput.Set(); if (e.Data != null) { - var line = Encoding.UTF8.GetString(Encoding.Default.GetBytes(e.Data)); + var line = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(e.Data)); errors.Add(line.TrimEnd('\r', '\n')); Logger.Trace(line); } @@ -121,7 +121,7 @@ public void Run() gotOutput.Set(); if (e.Data != null) { - var line = Encoding.UTF8.GetString(Encoding.Default.GetBytes(e.Data)); + var line = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(e.Data)); outputProcessor.LineReceived(line.TrimEnd('\r','\n')); } else From 718733d83b61e9779ad082009b0945eb1fdb2ae0 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 12 Jun 2018 11:00:12 +0100 Subject: [PATCH 321/567] Make sure we always send the correct user agent --- octorun/src/api.js | 8 ++++---- octorun/src/authentication.js | 3 +-- octorun/src/octokit.js | 10 ++-------- src/GitHub.Api/Application/ApplicationInfo.cs | 3 ++- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/octorun/src/api.js b/octorun/src/api.js index fcaf96c50..1c8c37e42 100644 --- a/octorun/src/api.js +++ b/octorun/src/api.js @@ -2,15 +2,15 @@ var config = require("./configuration"); var octokitWrapper = require("./octokit"); function ApiWrapper() { - this.octokit = octokitWrapper.createOctokit(); + if (!config.appName) { + throw "appName missing"; + } if (!config.user || !config.token) { throw "user and/or token missing"; } - if (!config.appName) { - throw "appName missing"; - } + this.octokit = octokitWrapper.createOctokit(config.appName); this.octokit.authenticate({ type: "oauth", diff --git a/octorun/src/authentication.js b/octorun/src/authentication.js index 43a2de5bb..191bf7700 100644 --- a/octorun/src/authentication.js +++ b/octorun/src/authentication.js @@ -15,7 +15,7 @@ var handleAuthentication = function (username, password, onSuccess, onFailure, t throw "appName missing"; } - var octokit = octokitWrapper.createOctokit(); + var octokit = octokitWrapper.createOctokit(config.appName); octokit.authenticate({ type: "basic", @@ -27,7 +27,6 @@ var handleAuthentication = function (username, password, onSuccess, onFailure, t if (twoFactor) { headers = { "X-GitHub-OTP": twoFactor, - "user-agent": config.appName }; } diff --git a/octorun/src/octokit.js b/octorun/src/octokit.js index 1cf90b1ac..f304d297a 100644 --- a/octorun/src/octokit.js +++ b/octorun/src/octokit.js @@ -1,18 +1,12 @@ var Octokit = require('octokit-rest-for-node-v0.12'); -var createOctokit = function () { +var createOctokit = function (appName) { return Octokit({ timeout: 0, requestMedia: 'application/vnd.github.v3+json', headers: { - 'user-agent': 'octokit/rest.js v1.2.3' + 'user-agent': appName } - - // change for custom GitHub Enterprise URL - //host: 'api.github.com', - //pathPrefix: '', - //protocol: 'https', - //port: 443 }); }; diff --git a/src/GitHub.Api/Application/ApplicationInfo.cs b/src/GitHub.Api/Application/ApplicationInfo.cs index f70bf4241..931e60cac 100644 --- a/src/GitHub.Api/Application/ApplicationInfo.cs +++ b/src/GitHub.Api/Application/ApplicationInfo.cs @@ -6,11 +6,12 @@ static partial class ApplicationInfo #if DEBUG public const string ApplicationName = "GitHub for Unity Debug"; public const string ApplicationProvider = "GitHub"; + public const string ApplicationSafeName = "GitHubUnity-dev"; #else public const string ApplicationName = "GitHubUnity"; public const string ApplicationProvider = "GitHub"; -#endif public const string ApplicationSafeName = "GitHubUnity"; +#endif public const string ApplicationDescription = "GitHub for Unity"; internal static string ClientId { get; private set; } = ""; From 546eb3c50fdc262790872290a379941af99286ef Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 12 Jun 2018 11:42:34 -0400 Subject: [PATCH 322/567] Adding a "How to Authenticate" document --- docs/using/authenticating-to-github.md | 35 +++++++++++++++++++++ docs/using/images/github-authenticate.png | 3 ++ docs/using/images/github-menu-item.png | 3 ++ docs/using/images/github-sign-in-button.png | 3 ++ docs/using/images/github-two-factor.png | 3 ++ 5 files changed, 47 insertions(+) create mode 100644 docs/using/authenticating-to-github.md create mode 100644 docs/using/images/github-authenticate.png create mode 100644 docs/using/images/github-menu-item.png create mode 100644 docs/using/images/github-sign-in-button.png create mode 100644 docs/using/images/github-two-factor.png diff --git a/docs/using/authenticating-to-github.md b/docs/using/authenticating-to-github.md new file mode 100644 index 000000000..663515d76 --- /dev/null +++ b/docs/using/authenticating-to-github.md @@ -0,0 +1,35 @@ +# Authenticating to GitHub + +## How to login to GitHub + +1. In Unity find the **GitHub** window. If you can't find it, you can switch to it by choosing **GitHub** from the **Window** menu. + + GitHub menu item in the Window menu + +1. Click the **Sign in** button at the top right of the window. + + GitHub menu item in the Window menu + +1. In the **Authenticate** dialog, enter your username or email and password + + GitHub menu item in the Window menu + + If your account requires Two Factor Authencation. You will be prompted for your auth code. + + GitHub menu item in the Window menu + +Before you authenticate, you must already have a GitHub account. + +- For more information on creating a GitHub account, see "[Signing up for a new GitHub account](https://help.github.com/articles/signing-up-for-a-new-github-account/)". + +### Personal access tokens + +If all signin options above fail, you can manually create a personal access token and use it as your password. + +The scopes for the personal access token are: `user`, `repo`. +- *user* scope: Grants access to the user profile data. We currently use this to display your avatar and check whether your plans lets you publish private repositories. +- *repo* scope: Grants read/write access to code, commit statuses, invitations, collaborators, adding team memberships, and deployment statuses for public and private repositories and organizations. This is needed for all git network operations (push, pull, fetch), and for getting information about the repository you're currently working on. + +For more information on creating personal access tokens, see "[Creating a personal access token for the command line](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). + +For more information on authenticating with SAML single sign-on, see "[About authentication with SAML single sign-on](https://help.github.com/articles/about-authentication-with-saml-single-sign-on)." diff --git a/docs/using/images/github-authenticate.png b/docs/using/images/github-authenticate.png new file mode 100644 index 000000000..188121d97 --- /dev/null +++ b/docs/using/images/github-authenticate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31ca0f7c4fc5c737a3db2da6eeb3deaa862d27e2cf3d1aa78fa14555b7cac750 +size 5927 diff --git a/docs/using/images/github-menu-item.png b/docs/using/images/github-menu-item.png new file mode 100644 index 000000000..44f3f9458 --- /dev/null +++ b/docs/using/images/github-menu-item.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c423766a70230c245f4bcdb1e33d9e76f267bf789876ecf9050016f9c8671735 +size 19651 diff --git a/docs/using/images/github-sign-in-button.png b/docs/using/images/github-sign-in-button.png new file mode 100644 index 000000000..cb6132c24 --- /dev/null +++ b/docs/using/images/github-sign-in-button.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79bea09964bcf1964d4d1e1fb9712fe9fc67dc8ffe11f936534c9f2be63fd777 +size 9318 diff --git a/docs/using/images/github-two-factor.png b/docs/using/images/github-two-factor.png new file mode 100644 index 000000000..7a3286616 --- /dev/null +++ b/docs/using/images/github-two-factor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba2174f780ea52a7cf7bbfd5144527bb7b29c3ce6a3200b6b6f8c69b2d6fb48 +size 9665 From fee39e85f091880557a224c5a85059e267964046 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 12 Jun 2018 11:43:07 -0400 Subject: [PATCH 323/567] Reducing the authentication scope requested by GitHub for Unity --- octorun/src/authentication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octorun/src/authentication.js b/octorun/src/authentication.js index 43a2de5bb..9f41f5186 100644 --- a/octorun/src/authentication.js +++ b/octorun/src/authentication.js @@ -4,7 +4,7 @@ var octokitWrapper = require("./octokit"); var twoFactorRegex = new RegExp("must specify two-factor authentication otp code", "gi"); -var scopes = ["user", "repo", "gist", "write:public_key"]; +var scopes = ["user", "repo"]; var handleAuthentication = function (username, password, onSuccess, onFailure, twoFactor) { if (!config.clientId || !config.clientSecret) { From c0b8b3c6356988afd50f43e9e31997c754d3173e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 12 Jun 2018 12:08:02 -0400 Subject: [PATCH 324/567] Making changes to the documentation --- docs/using/authenticating-to-github.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/using/authenticating-to-github.md b/docs/using/authenticating-to-github.md index 663515d76..29290bf1d 100644 --- a/docs/using/authenticating-to-github.md +++ b/docs/using/authenticating-to-github.md @@ -1,8 +1,8 @@ # Authenticating to GitHub -## How to login to GitHub +## How to sign in to GitHub -1. In Unity find the **GitHub** window. If you can't find it, you can switch to it by choosing **GitHub** from the **Window** menu. +1. Open the **GitHub** window by going to the top level **Window** menu and selecting **GitHub**, as shown below. GitHub menu item in the Window menu @@ -14,17 +14,17 @@ GitHub menu item in the Window menu - If your account requires Two Factor Authencation. You will be prompted for your auth code. + If your account requires Two Factor Authentication, you will be prompted for your auth code. GitHub menu item in the Window menu -Before you authenticate, you must already have a GitHub account. +You will need to create a GitHub account before you can sign in, if you don't have one already. - For more information on creating a GitHub account, see "[Signing up for a new GitHub account](https://help.github.com/articles/signing-up-for-a-new-github-account/)". ### Personal access tokens -If all signin options above fail, you can manually create a personal access token and use it as your password. +If the sign in operation above fails, you can manually create a personal access token and use it as your password. The scopes for the personal access token are: `user`, `repo`. - *user* scope: Grants access to the user profile data. We currently use this to display your avatar and check whether your plans lets you publish private repositories. From ccf153ed7aece4f8a976cda892a4b77bb7d8879e Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Tue, 12 Jun 2018 19:20:55 -0700 Subject: [PATCH 325/567] add documentation for working with changes --- docs/using/images/changes-view.png | 3 ++ docs/using/images/confirm-pull-changes.png | 3 ++ docs/using/images/confirm-push-changes.png | 3 ++ docs/using/images/confirm-revert.png | 3 ++ docs/using/images/post-commit-view.png | 3 ++ docs/using/images/post-push-history-view.png | 3 ++ docs/using/images/pull-view.png | 3 ++ docs/using/images/push-view.png | 3 ++ docs/using/images/revert-commit.png | 3 ++ docs/using/images/revert.png | 3 ++ docs/using/images/success-pull-changes.png | 3 ++ docs/using/images/success-push-changes.png | 3 ++ docs/using/working-with-changes.md | 43 ++++++++++++++++++++ 13 files changed, 79 insertions(+) create mode 100644 docs/using/images/changes-view.png create mode 100644 docs/using/images/confirm-pull-changes.png create mode 100644 docs/using/images/confirm-push-changes.png create mode 100644 docs/using/images/confirm-revert.png create mode 100644 docs/using/images/post-commit-view.png create mode 100644 docs/using/images/post-push-history-view.png create mode 100644 docs/using/images/pull-view.png create mode 100644 docs/using/images/push-view.png create mode 100644 docs/using/images/revert-commit.png create mode 100644 docs/using/images/revert.png create mode 100644 docs/using/images/success-pull-changes.png create mode 100644 docs/using/images/success-push-changes.png create mode 100644 docs/using/working-with-changes.md diff --git a/docs/using/images/changes-view.png b/docs/using/images/changes-view.png new file mode 100644 index 000000000..23000f8ef --- /dev/null +++ b/docs/using/images/changes-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ac4637a222a06e6ab57f23082b8e8b2f5cb9e35b9b99776c112ddcebb923dec +size 61918 diff --git a/docs/using/images/confirm-pull-changes.png b/docs/using/images/confirm-pull-changes.png new file mode 100644 index 000000000..76558ce5a --- /dev/null +++ b/docs/using/images/confirm-pull-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8b98eebba825e263321e5501c29ad4e385ea684be456fbbe686e215f2ec8d99 +size 34661 diff --git a/docs/using/images/confirm-push-changes.png b/docs/using/images/confirm-push-changes.png new file mode 100644 index 000000000..ca279cce1 --- /dev/null +++ b/docs/using/images/confirm-push-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a7d8b7d94c8b1d3d11b410a6ab3308011fa4fc5dfd3e46bf97cb39e886d763e +size 29527 diff --git a/docs/using/images/confirm-revert.png b/docs/using/images/confirm-revert.png new file mode 100644 index 000000000..0bba87bbd --- /dev/null +++ b/docs/using/images/confirm-revert.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a3a8fa7349550470ff2347d146a7b74055baca1206a51bb50139fab55414474 +size 42528 diff --git a/docs/using/images/post-commit-view.png b/docs/using/images/post-commit-view.png new file mode 100644 index 000000000..81fc7d135 --- /dev/null +++ b/docs/using/images/post-commit-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91e26123236bdd3e692ca7d30ff0d6477311fe739d8008ba19fd543ee3506176 +size 21345 diff --git a/docs/using/images/post-push-history-view.png b/docs/using/images/post-push-history-view.png new file mode 100644 index 000000000..0d2ed767a --- /dev/null +++ b/docs/using/images/post-push-history-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0c2d5ccaddc88d3268e0d57f02229525240ecc71bded951e23171456560268b +size 22485 diff --git a/docs/using/images/pull-view.png b/docs/using/images/pull-view.png new file mode 100644 index 000000000..842ff9660 --- /dev/null +++ b/docs/using/images/pull-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:feb83a46ebdd34ff46db3a38e8829f5adec5b10824bf69a8c56c9ba6fbd603ff +size 25054 diff --git a/docs/using/images/push-view.png b/docs/using/images/push-view.png new file mode 100644 index 000000000..42bef651a --- /dev/null +++ b/docs/using/images/push-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab87bae0916af3681b7528891a359c77597fa3ea57f13eb6924f1cbcf2f2ab7d +size 14581 diff --git a/docs/using/images/revert-commit.png b/docs/using/images/revert-commit.png new file mode 100644 index 000000000..daad75c8c --- /dev/null +++ b/docs/using/images/revert-commit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7711f428f3e04ea056c47b5d43109f3941761f4c286360d3616e0598d2d7b75 +size 26119 diff --git a/docs/using/images/revert.png b/docs/using/images/revert.png new file mode 100644 index 000000000..326071a28 --- /dev/null +++ b/docs/using/images/revert.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dea7b36eec8e2d99e1906b6cbc9cd22f52d2db09cc1a0cf34563585720b6649d +size 25141 diff --git a/docs/using/images/success-pull-changes.png b/docs/using/images/success-pull-changes.png new file mode 100644 index 000000000..c4ecf2cef --- /dev/null +++ b/docs/using/images/success-pull-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70a4af9d9e0db07d52c1ec6c2ee2c0d59257a6289b00dfd5fc60bd0f1224edf8 +size 35707 diff --git a/docs/using/images/success-push-changes.png b/docs/using/images/success-push-changes.png new file mode 100644 index 000000000..171c328cf --- /dev/null +++ b/docs/using/images/success-push-changes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe63e30112c768abfbde254221c81f3d17aff47c29ba27a50fb4b1ff5e8947ca +size 23415 diff --git a/docs/using/working-with-changes.md b/docs/using/working-with-changes.md new file mode 100644 index 000000000..719651492 --- /dev/null +++ b/docs/using/working-with-changes.md @@ -0,0 +1,43 @@ +# Working with changes + +All changes made to a repository will show up under the **Changes** tab. + +## Commit changes to GitHub + +1. Select the changes to be committed. Can choose the All/None options, or select directories or files individually. +2. Enter a Commit summary which describes the purpose of the commit. An optional Commit description can also be entered. +3. Click the button `Commit to [branch name]`. +Changes view + +The commit will not be shown under the **History** view. On the top bar the button `Push (1)` indicates that there is 1 commit to push. +Post commit view + +## Push changes to GitHub + +1. Click `Push` once ready to push a commit to GitHub. +Push view +2. A dialog will appear asking `Would you like to push changes to remote 'branch name’`? Select `Push`. +Confirm push dialog +3. Another dialog will appear when the push to GitHub is complete saying `Branch pushed`. Select `ok`. +Branch pushed + +## Revert changes + +1. From the **History** view, right-click on a commit in the commit list. A `Revert` option will appear. +2. Click `Revert` +Revert +3. A dialog will appear asking `1. Are you sure you want to revert the following commit: "commit message"?`. Select `Revert`. +Confirm revert dialog +4. A new commit appears titled `Revert "commit summary"` and the view indicates that there is 1 commit to push. +Revert commit +3. A dialog will appear asking `Would you like to pull changes from remote 'branch name'?`. Select `Pull`. +Confirm pull changes dialog +4. Another dialog appears saying `Local branch is up to date with 'branch name'`. Select `ok`. +Changes pulled From df1d0baf90bb7340af80ce1a4b4ebb887ddb33bb Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Tue, 12 Jun 2018 19:40:41 -0700 Subject: [PATCH 326/567] update formatting of doc --- docs/using/working-with-changes.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/using/working-with-changes.md b/docs/using/working-with-changes.md index 719651492..7b8487861 100644 --- a/docs/using/working-with-changes.md +++ b/docs/using/working-with-changes.md @@ -1,43 +1,53 @@ # Working with changes -All changes made to a repository will show up under the **Changes** tab. - ## Commit changes to GitHub +All changes made to a repository will show up under the **Changes** view. + 1. Select the changes to be committed. Can choose the All/None options, or select directories or files individually. 2. Enter a Commit summary which describes the purpose of the commit. An optional Commit description can also be entered. 3. Click the button `Commit to [branch name]`. Changes view The commit will not be shown under the **History** view. On the top bar the button `Push (1)` indicates that there is 1 commit to push. + Post commit view ## Push changes to GitHub 1. Click `Push` once ready to push a commit to GitHub. Push view -2. A dialog will appear asking `Would you like to push changes to remote 'branch name’`? Select `Push`. + +2. A dialog will appear asking `Would you like to push changes to remote 'branch name'?` Select `Push`. Confirm push dialog + 3. Another dialog will appear when the push to GitHub is complete saying `Branch pushed`. Select `ok`. Branch pushed ## Revert changes 1. From the **History** view, right-click on a commit in the commit list. A `Revert` option will appear. -2. Click `Revert` +2. Click `Revert`. Revert -3. A dialog will appear asking `1. Are you sure you want to revert the following commit: "commit message"?`. Select `Revert`. + +3. A dialog will appear asking `Are you sure you want to revert the following commit: "commit message"?`. Select `Revert`. Confirm revert dialog + 4. A new commit appears titled `Revert "commit summary"` and the view indicates that there is 1 commit to push. -Revert commit + 5. Follow the steps to push the reverted commit to GitHub. ## Pulling changes 1. Click the `Fetch` button to get all the latest branches and tags for the repository. The `Pull` button will then show the number of commits to pull from GitHub. 2. Click `Pull`. -Pull changes +Pull changes + 3. A dialog will appear asking `Would you like to pull changes from remote 'branch name'?`. Select `Pull`. -Confirm pull changes dialog -4. Another dialog appears saying `Local branch is up to date with 'branch name'`. Select `ok`. -Changes pulled +Confirm pull changes dialog + +4. Another dialog appears saying `Local branch is up to date with 'branch name'`. Select `ok`. +Changes pulled + + From 28e99c1b0f69e15ac7231bc0e61bcb2e61f68fac Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 13 Jun 2018 09:54:12 -0400 Subject: [PATCH 327/567] Preparing an updated octorun package --- octorun/version | 2 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/octorun/version b/octorun/version index b2dfd9b3b..48417770b 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -b4b80eb4ac \ No newline at end of file +7f160da1 \ No newline at end of file diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 497466be1..1a319c6c4 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -85,7 +85,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "b4b80eb4ac"; + public const string PackageVersion = "7f160da1"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 8a661c7ca..d7c63b3f3 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:802c9a15337ce6692f8c4c215a131b755358972cb3a7e76139193855770ab1c8 -size 214371 +oid sha256:60478c33970a7a8d952777729450a42fa02bf7832514b7d44ec116aa09cb7814 +size 219592 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index d2a4cc5cd..6f12f7867 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -0a49f36d2e8df01456f832c6968a6782 +3d4bea9ae4ca3c4497d6b349166a1e34 \ No newline at end of file From 3dd452a925ba14db1cf1ea2892b4a66fad6dcf5f Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 13 Jun 2018 10:01:07 -0400 Subject: [PATCH 328/567] Tweaking the documentation a bit --- docs/using/authenticating-to-github.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/using/authenticating-to-github.md b/docs/using/authenticating-to-github.md index 29290bf1d..203a3473d 100644 --- a/docs/using/authenticating-to-github.md +++ b/docs/using/authenticating-to-github.md @@ -30,6 +30,8 @@ The scopes for the personal access token are: `user`, `repo`. - *user* scope: Grants access to the user profile data. We currently use this to display your avatar and check whether your plans lets you publish private repositories. - *repo* scope: Grants read/write access to code, commit statuses, invitations, collaborators, adding team memberships, and deployment statuses for public and private repositories and organizations. This is needed for all git network operations (push, pull, fetch), and for getting information about the repository you're currently working on. +***Note:*** *Some older versions of the plugin ask for `gist` and `write:public_key`.* + For more information on creating personal access tokens, see "[Creating a personal access token for the command line](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). For more information on authenticating with SAML single sign-on, see "[About authentication with SAML single sign-on](https://help.github.com/articles/about-authentication-with-saml-single-sign-on)." From b3d5752a03ed7538f6fb8d48968188d73f4ce605 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 13 Jun 2018 18:15:31 +0100 Subject: [PATCH 329/567] Bump version to 1.0.0 --- common/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 62edf1036..2ef396d09 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -34,6 +34,6 @@ internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics internal const string VersionForAssembly = "1.0.0"; // Actual real version - internal const string Version = "1.0.0rc5"; + internal const string Version = "1.0.0"; } } From 1b649435ebae5d0cd29fb78999725d2db44d1b9e Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 13 Jun 2018 18:14:45 +0100 Subject: [PATCH 330/567] Enable lfs polling by regularly triggering a cache invalidation --- src/GitHub.Api/Git/Repository.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index f9d6fe38e..b1516bf85 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -188,6 +188,12 @@ public void Refresh(CacheType cacheType) { var cache = cacheContainer.GetCache(cacheType); cache.InvalidateData(); + + // take the opportunity to possibly refresh the locks cache, if it has timed out + if (cacheType != CacheType.GitLocks) + { + cacheContainer.GetCache(CacheType.GitLocks).ValidateData(); + } } private void CacheHasBeenInvalidated(CacheType cacheType) From 37ccb7a96e0938a33c3de684e5c429129c5fb12c Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Wed, 13 Jun 2018 16:59:24 -0700 Subject: [PATCH 331/567] update images --- docs/using/images/confirm-pull-changes.png | 4 ++-- docs/using/images/confirm-push-changes.png | 4 ++-- docs/using/images/confirm-revert.png | 4 ++-- docs/using/images/success-pull-changes.png | 4 ++-- docs/using/images/success-push-changes.png | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/using/images/confirm-pull-changes.png b/docs/using/images/confirm-pull-changes.png index 76558ce5a..722bbd639 100644 --- a/docs/using/images/confirm-pull-changes.png +++ b/docs/using/images/confirm-pull-changes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8b98eebba825e263321e5501c29ad4e385ea684be456fbbe686e215f2ec8d99 -size 34661 +oid sha256:4b0e1aa1d8d86cb0264c9ca569a5c8e71b0aba73a2eb6611c06ec47670037d7d +size 21478 diff --git a/docs/using/images/confirm-push-changes.png b/docs/using/images/confirm-push-changes.png index ca279cce1..ff1ca1cd0 100644 --- a/docs/using/images/confirm-push-changes.png +++ b/docs/using/images/confirm-push-changes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a7d8b7d94c8b1d3d11b410a6ab3308011fa4fc5dfd3e46bf97cb39e886d763e -size 29527 +oid sha256:9e2e136027b03609f344bfde0c777d8e915544a5e9920f1268e4cc3abc5b1481 +size 18676 diff --git a/docs/using/images/confirm-revert.png b/docs/using/images/confirm-revert.png index 0bba87bbd..cbf5d1566 100644 --- a/docs/using/images/confirm-revert.png +++ b/docs/using/images/confirm-revert.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a3a8fa7349550470ff2347d146a7b74055baca1206a51bb50139fab55414474 -size 42528 +oid sha256:ba990ce12403bfb53fb387ca3f3e2c447b21814ed6cd7e6f88d968463a5253a8 +size 21973 diff --git a/docs/using/images/success-pull-changes.png b/docs/using/images/success-pull-changes.png index c4ecf2cef..a246da405 100644 --- a/docs/using/images/success-pull-changes.png +++ b/docs/using/images/success-pull-changes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70a4af9d9e0db07d52c1ec6c2ee2c0d59257a6289b00dfd5fc60bd0f1224edf8 -size 35707 +oid sha256:e0407ab83ca311f319ff7feac0dac84b862935e2b50353ff82c067bdadf80e16 +size 16888 diff --git a/docs/using/images/success-push-changes.png b/docs/using/images/success-push-changes.png index 171c328cf..8fba1f506 100644 --- a/docs/using/images/success-push-changes.png +++ b/docs/using/images/success-push-changes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe63e30112c768abfbde254221c81f3d17aff47c29ba27a50fb4b1ff5e8947ca -size 23415 +oid sha256:cfd290cb1e52b0acb5503642a3aaef164bf343735729325484e25d8b99293d7e +size 13262 From 2fc8b15de0c65f6452080685c38497e0d6036a44 Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Wed, 13 Jun 2018 18:00:55 -0700 Subject: [PATCH 332/567] add docs for managing branches --- docs/using/images/branches-initial-view.png | 3 ++ docs/using/images/create-new-branch-view.png | 3 ++ docs/using/images/delete-dialog.png | 3 ++ docs/using/images/name-branch.png | 3 ++ docs/using/images/new-branch-created.png | 3 ++ docs/using/images/switch-confirmation.png | 3 ++ docs/using/images/switch-or-delete.png | 3 ++ docs/using/images/switched-branches.png | 3 ++ docs/using/managing-branches.md | 37 ++++++++++++++++++++ 9 files changed, 61 insertions(+) create mode 100644 docs/using/images/branches-initial-view.png create mode 100644 docs/using/images/create-new-branch-view.png create mode 100644 docs/using/images/delete-dialog.png create mode 100644 docs/using/images/name-branch.png create mode 100644 docs/using/images/new-branch-created.png create mode 100644 docs/using/images/switch-confirmation.png create mode 100644 docs/using/images/switch-or-delete.png create mode 100644 docs/using/images/switched-branches.png create mode 100644 docs/using/managing-branches.md diff --git a/docs/using/images/branches-initial-view.png b/docs/using/images/branches-initial-view.png new file mode 100644 index 000000000..811bcf724 --- /dev/null +++ b/docs/using/images/branches-initial-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:558821e67ea26955434da0485785e9240b6c15c44a3790b642fdcd37382e56a2 +size 59798 diff --git a/docs/using/images/create-new-branch-view.png b/docs/using/images/create-new-branch-view.png new file mode 100644 index 000000000..34c69d1ea --- /dev/null +++ b/docs/using/images/create-new-branch-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0239a51f611501dc63c828de206f35d412493871f65e712983666e67dbfc231 +size 62046 diff --git a/docs/using/images/delete-dialog.png b/docs/using/images/delete-dialog.png new file mode 100644 index 000000000..574489f7c --- /dev/null +++ b/docs/using/images/delete-dialog.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99d7cd8ef4957614cc844a08a632159157295414dc4675c1394f3209e30ffa58 +size 76315 diff --git a/docs/using/images/name-branch.png b/docs/using/images/name-branch.png new file mode 100644 index 000000000..9926c06db --- /dev/null +++ b/docs/using/images/name-branch.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08232fa5cfd41a41c8091197ac27f433fdfa865c4996388dd0c731ac75176442 +size 59599 diff --git a/docs/using/images/new-branch-created.png b/docs/using/images/new-branch-created.png new file mode 100644 index 000000000..2a37a9aeb --- /dev/null +++ b/docs/using/images/new-branch-created.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8543da247ee23364c15abec87e4e3de3e62b85a470e7d3d090ade18fb321072c +size 64045 diff --git a/docs/using/images/switch-confirmation.png b/docs/using/images/switch-confirmation.png new file mode 100644 index 000000000..92d623d6c --- /dev/null +++ b/docs/using/images/switch-confirmation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ed80f5cf2fab5fd256dd7cd093ad505c966edb95bdc5a15c3f1ace042bec7f6 +size 73947 diff --git a/docs/using/images/switch-or-delete.png b/docs/using/images/switch-or-delete.png new file mode 100644 index 000000000..deab6badf --- /dev/null +++ b/docs/using/images/switch-or-delete.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c0d171b2e8f7e212f2446b416235c6232c57c9b4927cfa41891bffa1940ae9d +size 73828 diff --git a/docs/using/images/switched-branches.png b/docs/using/images/switched-branches.png new file mode 100644 index 000000000..c185d25c5 --- /dev/null +++ b/docs/using/images/switched-branches.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:542806a8dcc012cdfadadaf9cdab3cf4871bbbbee2c8d73834946df4ed249aec +size 64281 diff --git a/docs/using/managing-branches.md b/docs/using/managing-branches.md new file mode 100644 index 000000000..1bf26e6fb --- /dev/null +++ b/docs/using/managing-branches.md @@ -0,0 +1,37 @@ +# Managing branches + +Initial **Branches** view +Post commit view + +## Create branch + +1. From the **Branches** view, click on `master` under local branches to enable the `New Branch` button and be able to create a new branch from master. +2. Click on `New Branch`. +Post commit view + +3. Enter a name for the branch and click `Create`. +Post commit view + +4. The new branch will be created from master. +Post commit view + +## Checkout branch + +1. Right-click on a local branch and select `Switch` or double-click on the branch to switch to it. +Post commit view + +2. A dialog will appear asking `Switch branch to 'branch name'?`. Select `Switch`. +Post commit view + +The branch will be checked out. +Post commit view + +## Delete branches + +1. Click on the branch name to be deleted and the `Delete` button becomes enabled. +2. Right-click on a local branch and select `Delete` or click the `Delete` button above the Local branches list. +Post commit view + +3. A dialog appears asking `Are you sure you want to delete the branch: ‘branch name’?`. Select `Delete`. + +The branch will be deleted. From e22295dbc0f153c3608eb6cd9f136ad465bc6489 Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Wed, 13 Jun 2018 18:03:48 -0700 Subject: [PATCH 333/567] update format --- docs/using/managing-branches.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/using/managing-branches.md b/docs/using/managing-branches.md index 1bf26e6fb..13fa9c1bd 100644 --- a/docs/using/managing-branches.md +++ b/docs/using/managing-branches.md @@ -1,6 +1,7 @@ # Managing branches Initial **Branches** view + Post commit view ## Create branch @@ -32,6 +33,6 @@ The branch will be checked out. 2. Right-click on a local branch and select `Delete` or click the `Delete` button above the Local branches list. Post commit view -3. A dialog appears asking `Are you sure you want to delete the branch: ‘branch name’?`. Select `Delete`. +3. A dialog appears asking `Are you sure you want to delete the branch: 'branch name'?`. Select `Delete`. The branch will be deleted. From babbd713b40769435fab58faad63e8b843025218 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 19 Jun 2018 18:05:59 -0400 Subject: [PATCH 334/567] Fixing date parsing for VSTS repos --- src/GitHub.Api/Helpers/Constants.cs | 2 ++ .../Primitives/SerializationTests.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/GitHub.Api/Helpers/Constants.cs b/src/GitHub.Api/Helpers/Constants.cs index 4380a57c9..e24580db8 100644 --- a/src/GitHub.Api/Helpers/Constants.cs +++ b/src/GitHub.Api/Helpers/Constants.cs @@ -13,10 +13,12 @@ static class Constants public const string GitTimeoutKey = "GitTimeout"; public const string Iso8601Format = @"yyyy-MM-dd\THH\:mm\:ss.fffzzz"; public const string Iso8601FormatZ = @"yyyy-MM-dd\THH\:mm\:ss\Z"; + public const string Iso8601FormatPointZ = @"yyyy-MM-dd\THH\:mm\:ss.ff\Z"; public static readonly string[] Iso8601Formats = { Iso8601FormatZ, @"yyyy-MM-dd\THH\:mm\:ss.fffffffzzz", Iso8601Format, + Iso8601FormatPointZ, @"yyyy-MM-dd\THH\:mm\:sszzz", }; public const DateTimeStyles DateTimeStyle = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; diff --git a/src/tests/UnitTests/Primitives/SerializationTests.cs b/src/tests/UnitTests/Primitives/SerializationTests.cs index 32bcdf76b..1bf6cef15 100644 --- a/src/tests/UnitTests/Primitives/SerializationTests.cs +++ b/src/tests/UnitTests/Primitives/SerializationTests.cs @@ -37,6 +37,25 @@ public void DateTimeSerializationRoundTrip() Assert.AreEqual(dt3, dt4); } + [Test] + public void DateTimeSerializationRoundTripFormatPointZ() + { + var dt1 = DateTimeOffset.ParseExact("2018-05-01T15:04:29.00Z", new []{ Constants.Iso8601FormatPointZ }, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + DateTimeOffset.ParseExact("2018-05-01T15:04:29.00Z", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var str1 = dt1.ToJson(); + var ret1 = str1.FromJson(); + Assert.AreEqual(dt1, ret1); + } + + [Test] + public void DateTimeSerializationRoundTripFormatZ() + { + var dt1 = DateTimeOffset.ParseExact("2018-05-01T15:04:29Z", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var str1 = dt1.ToJson(); + var ret1 = str1.FromJson(); + Assert.AreEqual(dt1, ret1); + } + class TestData { public List Things { get; set; } = new List(); From de9eebd89dac612a358b1009bbdb59de9b420548 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 19 Jun 2018 18:08:58 -0400 Subject: [PATCH 335/567] Fixing unit test --- .../UnitTests/Primitives/SerializationTests.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/tests/UnitTests/Primitives/SerializationTests.cs b/src/tests/UnitTests/Primitives/SerializationTests.cs index 1bf6cef15..671b55296 100644 --- a/src/tests/UnitTests/Primitives/SerializationTests.cs +++ b/src/tests/UnitTests/Primitives/SerializationTests.cs @@ -40,20 +40,17 @@ public void DateTimeSerializationRoundTrip() [Test] public void DateTimeSerializationRoundTripFormatPointZ() { - var dt1 = DateTimeOffset.ParseExact("2018-05-01T15:04:29.00Z", new []{ Constants.Iso8601FormatPointZ }, CultureInfo.InvariantCulture, Constants.DateTimeStyle); - DateTimeOffset.ParseExact("2018-05-01T15:04:29.00Z", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var dt1 = DateTimeOffset.ParseExact("2018-05-01T15:04:29.00Z", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); var str1 = dt1.ToJson(); var ret1 = str1.FromJson(); Assert.AreEqual(dt1, ret1); - } - [Test] - public void DateTimeSerializationRoundTripFormatZ() - { - var dt1 = DateTimeOffset.ParseExact("2018-05-01T15:04:29Z", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); - var str1 = dt1.ToJson(); - var ret1 = str1.FromJson(); - Assert.AreEqual(dt1, ret1); + var dt2 = DateTimeOffset.ParseExact("2018-05-01T15:04:29Z", Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var str2 = dt2.ToJson(); + var ret2 = str2.FromJson(); + Assert.AreEqual(dt2, ret2); + + Assert.AreEqual(dt1, dt2); } class TestData From 837dae060eb1ce6fa4961b2531882a3101501557 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 20 Jun 2018 09:56:47 -0400 Subject: [PATCH 336/567] Utilizing Constants.Iso8601Formats in SimpleJson deserialization --- src/GitHub.Api/Helpers/SimpleJson.cs | 4 ++-- .../UnitTests/IO/LockOutputProcessorTests.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index 2a0dafd78..7a7b4c4c1 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -1369,9 +1369,9 @@ public virtual object DeserializeObject(object value, Type type) if (type == typeof(NPath) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(NPath))) return new NPath(str); if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) - return DateTime.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + return DateTime.ParseExact(str, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) - return DateTimeOffset.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + return DateTimeOffset.ParseExact(str, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) return new Guid(str); if (type == typeof(UriString) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(UriString))) diff --git a/src/tests/UnitTests/IO/LockOutputProcessorTests.cs b/src/tests/UnitTests/IO/LockOutputProcessorTests.cs index 0fb8c4a44..9cf0a46ce 100644 --- a/src/tests/UnitTests/IO/LockOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/LockOutputProcessorTests.cs @@ -68,6 +68,25 @@ public void ShouldParseTwoLocksFormat() new GitLock("2f9cfde9c159d50e235cc1402c3e534b0bf2198afb20760697a5f9b07bf04fb3", "somezip.zip".ToNPath(), new GitUser("GitHub User", ""), now) }; + AssertProcessOutput(output, expected); + } + + [Test] + public void ShouldParseVSTSLocksFormat() + { + var nowString = DateTimeOffset.UtcNow.ToString(Constants.Iso8601FormatPointZ); + var now = DateTimeOffset.ParseExact(nowString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + var output = new[] + { + $@"[{{""id"":""7"" ,""path"":""Assets/Main.unity"",""owner"":{{""name"":""GitHub User""}},""locked_at"":""{nowString}""}}]", + string.Empty, + "1 lock(s) matched query.", + null + }; + + var expected = new[] { + new GitLock("7", "Assets/Main.unity".ToNPath(), new GitUser("GitHub User", ""), now), + }; AssertProcessOutput(output, expected); } From c4afeff16a1c66db0622c9782f4a6ae439848890 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 20 Jun 2018 10:10:07 -0400 Subject: [PATCH 337/567] Tweaking the way dates are stored --- src/GitHub.Api/Helpers/Constants.cs | 16 +++++++++++++--- src/GitHub.Api/Helpers/SimpleJson.cs | 16 +++------------- .../UnitTests/IO/LockOutputProcessorTests.cs | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/GitHub.Api/Helpers/Constants.cs b/src/GitHub.Api/Helpers/Constants.cs index e24580db8..1d0da6a86 100644 --- a/src/GitHub.Api/Helpers/Constants.cs +++ b/src/GitHub.Api/Helpers/Constants.cs @@ -13,13 +13,23 @@ static class Constants public const string GitTimeoutKey = "GitTimeout"; public const string Iso8601Format = @"yyyy-MM-dd\THH\:mm\:ss.fffzzz"; public const string Iso8601FormatZ = @"yyyy-MM-dd\THH\:mm\:ss\Z"; - public const string Iso8601FormatPointZ = @"yyyy-MM-dd\THH\:mm\:ss.ff\Z"; public static readonly string[] Iso8601Formats = { + Iso8601Format, Iso8601FormatZ, @"yyyy-MM-dd\THH\:mm\:ss.fffffffzzz", - Iso8601Format, - Iso8601FormatPointZ, + @"yyyy-MM-dd\THH\:mm\:ss.ffffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.fffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.ffffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.ffzzz", + @"yyyy-MM-dd\THH\:mm\:ss.fzzz", @"yyyy-MM-dd\THH\:mm\:sszzz", + @"yyyy-MM-dd\THH\:mm\:ss.fffffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.ffffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.fffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.ffff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.fff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.ff\Z", + @"yyyy-MM-dd\THH\:mm\:ss.f\Z", }; public const DateTimeStyles DateTimeStyle = DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal; public const string SkipVersionKey = "SkipVersion"; diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index 7a7b4c4c1..c0d4e7542 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -1250,17 +1250,7 @@ class PocoJsonSerializerStrategy : IJsonSerializerStrategy internal static readonly Type[] EmptyTypes = new Type[0]; internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; - private static readonly string[] Iso8601Format = new string[] - { - @"yyyy-MM-dd\THH\:mm\:sszzz", - @"yyyy-MM-dd\THH\:mm\:ss.fffffffzzz", - @"yyyy-MM-dd\THH\:mm\:ss.fffzzz", - @"yyyy-MM-dd\THH\:mm\:ss\Z", - @"yyyy-MM-dd\THH:mm:ss.fffffffzzz", - @"yyyy-MM-dd\THH:mm:ss.fffzzz", - @"yyyy-MM-dd\THH:mm:sszzz", - @"yyyy-MM-dd\THH:mm:ss\Z", - }; + private static readonly string[] Iso8601Formats = Constants.Iso8601Formats; public PocoJsonSerializerStrategy() { @@ -1504,9 +1494,9 @@ protected virtual bool TrySerializeKnownTypes(object input, out object output) if (input is NPath || input is UriString) output = input.ToString(); else if (input is DateTime) - output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); + output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Formats[0], CultureInfo.InvariantCulture); else if (input is DateTimeOffset) - output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); + output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Formats[0], CultureInfo.InvariantCulture); else if (input is Guid) output = ((Guid)input).ToString("D"); else if (input is Uri) diff --git a/src/tests/UnitTests/IO/LockOutputProcessorTests.cs b/src/tests/UnitTests/IO/LockOutputProcessorTests.cs index 9cf0a46ce..07797f03b 100644 --- a/src/tests/UnitTests/IO/LockOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/LockOutputProcessorTests.cs @@ -74,7 +74,7 @@ public void ShouldParseTwoLocksFormat() [Test] public void ShouldParseVSTSLocksFormat() { - var nowString = DateTimeOffset.UtcNow.ToString(Constants.Iso8601FormatPointZ); + var nowString = DateTimeOffset.UtcNow.ToString(@"yyyy-MM-dd\THH\:mm\:ss.ff\Z"); var now = DateTimeOffset.ParseExact(nowString, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); var output = new[] { From fef789195bacf6ac35498e4a1a6590bf0945a0c8 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 22 Jun 2018 14:36:18 +0200 Subject: [PATCH 338/567] This code shouldn't be touched at all --- src/GitHub.Api/Helpers/SimpleJson.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/GitHub.Api/Helpers/SimpleJson.cs b/src/GitHub.Api/Helpers/SimpleJson.cs index c0d4e7542..cdb9fb7d7 100644 --- a/src/GitHub.Api/Helpers/SimpleJson.cs +++ b/src/GitHub.Api/Helpers/SimpleJson.cs @@ -1250,7 +1250,7 @@ class PocoJsonSerializerStrategy : IJsonSerializerStrategy internal static readonly Type[] EmptyTypes = new Type[0]; internal static readonly Type[] ArrayConstructorParameterTypes = new Type[] { typeof(int) }; - private static readonly string[] Iso8601Formats = Constants.Iso8601Formats; + private static readonly string[] Iso8601Format = Constants.Iso8601Formats; public PocoJsonSerializerStrategy() { @@ -1359,9 +1359,9 @@ public virtual object DeserializeObject(object value, Type type) if (type == typeof(NPath) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(NPath))) return new NPath(str); if (type == typeof(DateTime) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTime))) - return DateTime.ParseExact(str, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + return DateTime.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(DateTimeOffset) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(DateTimeOffset))) - return DateTimeOffset.ParseExact(str, Constants.Iso8601Formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); + return DateTimeOffset.ParseExact(str, Iso8601Format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); if (type == typeof(Guid) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(Guid))) return new Guid(str); if (type == typeof(UriString) || (ReflectionUtils.IsNullableType(type) && Nullable.GetUnderlyingType(type) == typeof(UriString))) @@ -1494,9 +1494,9 @@ protected virtual bool TrySerializeKnownTypes(object input, out object output) if (input is NPath || input is UriString) output = input.ToString(); else if (input is DateTime) - output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Formats[0], CultureInfo.InvariantCulture); + output = ((DateTime)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); else if (input is DateTimeOffset) - output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Formats[0], CultureInfo.InvariantCulture); + output = ((DateTimeOffset)input).ToUniversalTime().ToString(Iso8601Format[0], CultureInfo.InvariantCulture); else if (input is Guid) output = ((Guid)input).ToString("D"); else if (input is Uri) From b75e0f733f1c6cf26be38fe4e496bbc27751c045 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 22 Jun 2018 15:00:08 +0200 Subject: [PATCH 339/567] Add more serialization tests --- .../Primitives/SerializationTests.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/tests/UnitTests/Primitives/SerializationTests.cs b/src/tests/UnitTests/Primitives/SerializationTests.cs index 671b55296..17faab0f3 100644 --- a/src/tests/UnitTests/Primitives/SerializationTests.cs +++ b/src/tests/UnitTests/Primitives/SerializationTests.cs @@ -11,6 +11,41 @@ namespace UnitTests.Primitives [TestFixture] class SerializationTests { + [TestCase("2018-05-01T12:04:29.1234567-02:00", "2018-05-01T14:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.123456-02:00", "2018-05-01T14:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.12345-02:00", "2018-05-01T14:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.1234-02:00", "2018-05-01T14:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.123-02:00", "2018-05-01T14:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.12-02:00", "2018-05-01T14:04:29.120+00:00")] + [TestCase("2018-05-01T12:04:29.1-02:00", "2018-05-01T14:04:29.100+00:00")] + [TestCase("2018-05-01T12:04:29-02:00", "2018-05-01T14:04:29.000+00:00")] + [TestCase("2018-05-01T12:04:29.1234567Z", "2018-05-01T12:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.123456Z", "2018-05-01T12:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.12345Z", "2018-05-01T12:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.1234Z", "2018-05-01T12:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.123Z", "2018-05-01T12:04:29.123+00:00")] + [TestCase("2018-05-01T12:04:29.12Z", "2018-05-01T12:04:29.120+00:00")] + [TestCase("2018-05-01T12:04:29.1Z", "2018-05-01T12:04:29.100+00:00")] + [TestCase("2018-05-01T12:04:29Z", "2018-05-01T12:04:29.000+00:00")] + public void FromLocalStringToUniversalDateTimeOffset(string input, string expected) + { + var dtInput = DateTimeOffset.ParseExact(input, Constants.Iso8601Formats, CultureInfo.InvariantCulture, Constants.DateTimeStyle); + var output = dtInput.ToUniversalTime().ToString(Constants.Iso8601Format); + Assert.AreEqual(expected, output); + + var json = $@"{{""date"":""{input}""}}"; + Assert.DoesNotThrow(() => json.FromJson(lowerCase: true)); + } + + [Test] + public void JsonSerializationUsesKnownFormat() + { + var now = DateTimeOffset.Now; + var output = new ADateTimeOffset { Date = now }; + var json = output.ToJson(lowerCase: true); + Assert.AreEqual($@"{{""date"":""{ now.ToUniversalTime().ToString(Constants.Iso8601Format, CultureInfo.InvariantCulture) }""}}", json); + } + [Test] public void DateTimeSerializationRoundTrip() { @@ -53,6 +88,11 @@ public void DateTimeSerializationRoundTripFormatPointZ() Assert.AreEqual(dt1, dt2); } + class ADateTimeOffset + { + public DateTimeOffset Date; + } + class TestData { public List Things { get; set; } = new List(); From 9d5b2791069bcb8f96b81afab5c7e1c18a9fd773 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 15:12:12 -0400 Subject: [PATCH 340/567] Using a custom comparer to order entries --- .../OutputProcessors/StatusOutputProcessor.cs | 38 +++++++++++- .../IO/StatusOutputProcessorTests.cs | 60 ++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs index ad1e866d9..93bf15d91 100644 --- a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs @@ -191,7 +191,7 @@ private void ReturnStatus() return; gitStatus.Entries = gitStatus.Entries - .OrderBy(entry => entry.Path) + .OrderBy(entry => entry.Path, StatusOutputPathComparer.Instance) .ToList(); RaiseOnEntry(gitStatus); @@ -214,5 +214,41 @@ private void HandleUnexpected(string line) { Logger.Error("Unexpected Input:\"{0}\"", line); } + + class StatusOutputPathComparer : IComparer + { + internal static StatusOutputPathComparer Instance => new StatusOutputPathComparer(); + + public int Compare(string x, string y) + { + Guard.ArgumentNotNull(x, nameof(x)); + Guard.ArgumentNotNull(y, nameof(y)); + + var metaString = ".meta"; + var xIsMeta = x.EndsWith(metaString); + var yIsMeta = y.EndsWith(metaString); + + if (xIsMeta || yIsMeta) + { + var compareX = !xIsMeta ? x : x.Substring(0, x.Length - 5); + var compareY = !yIsMeta ? y : y.Substring(0, y.Length - 5); + + var comparisonResult = StringComparer.InvariantCultureIgnoreCase.Compare(compareX, compareY); + if (comparisonResult != 0) + { + return comparisonResult; + } + + if (xIsMeta) + { + return 1; + } + + return -1; + } + + return StringComparer.InvariantCultureIgnoreCase.Compare(x, y); + } + } } } diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index da18ec51c..4478dd6b3 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -245,6 +245,64 @@ public void ShouldParseCleanWorkingTreeTracked() }); } + [Test] + public void ShouldSortOutputCorrectly() + { + var output = new[] + { + "## master", + "?? GitHub.Unity.dll", + "?? GitHub.Unity.dll.mdb", + "?? GitHub.Unity.dll.mdb.meta", + "?? GitHub.Unity.dll.meta", + null + }; + + AssertProcessOutput(output, new GitStatus + { + LocalBranch = "master", + Entries = new List + { + new GitStatusEntry(@"GitHub.Unity.dll", TestRootPath + @"\GitHub.Unity.dll", null, GitFileStatus.Untracked), + new GitStatusEntry(@"GitHub.Unity.dll.meta", TestRootPath + @"\GitHub.Unity.dll.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"GitHub.Unity.dll.mdb", TestRootPath + @"\GitHub.Unity.dll.mdb", null, GitFileStatus.Untracked), + new GitStatusEntry(@"GitHub.Unity.dll.mdb.meta", TestRootPath + @"\GitHub.Unity.dll.mdb.meta", null, GitFileStatus.Untracked), + } + }); + } + + [Test] + public void ShouldSortOutputCorrectly2() + { + var output = new[] + { + "## master", + "?? Assets/Assets.Test.dll", + "?? Assets/Assets.Test.dll.meta", + "?? Plugins/GitHub.Unity.dll", + "?? Plugins/GitHub.Unity.dll.mdb", + "?? Plugins/GitHub.Unity.dll.mdb.meta", + "?? Plugins/GitHub.Unity.dll.meta", + "?? blah.txt", + null + }; + + AssertProcessOutput(output, new GitStatus + { + LocalBranch = "master", + Entries = new List + { + new GitStatusEntry(@"Assets/Assets.Test.dll", TestRootPath + @"\Assets/Assets.Test.dll", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Assets/Assets.Test.dll.meta", TestRootPath + @"\Assets/Assets.Test.dll.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"blah.txt", TestRootPath + @"\blah.txt", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll", TestRootPath + @"\Plugins/GitHub.Unity.dll", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll.meta", TestRootPath + @"\Plugins/GitHub.Unity.dll.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll.mdb", TestRootPath + @"\Plugins/GitHub.Unity.dll.mdb", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll.mdb.meta", TestRootPath + @"\Plugins/GitHub.Unity.dll.mdb.meta", null, GitFileStatus.Untracked), + } + }); + } + private void AssertProcessOutput(IEnumerable lines, GitStatus expected) { var gitObjectFactory = SubstituteFactory.CreateGitObjectFactory(TestRootPath); @@ -262,4 +320,4 @@ private void AssertProcessOutput(IEnumerable lines, GitStatus expected) result.Value.AssertEqual(expected); } } -} \ No newline at end of file +} From 1ec722553ad3fcde85f3679ed73ca42e9c11f55a Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 15:19:29 -0400 Subject: [PATCH 341/567] TreeBase does not have the functionality to evaluate containers --- src/GitHub.Api/UI/TreeBase.cs | 12 ++++++------ .../Editor/GitHub.Unity/UI/ChangesTreeControl.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/TreeControl.cs | 3 +-- src/tests/UnitTests/UI/TreeBaseTests.cs | 7 +++---- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index d3cfb88d1..915abe89f 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -54,7 +54,7 @@ public void Load(IEnumerable treeDatas) TNode lastAddedNode = null; Clear(); - AddNode(Title, Title, -1 + displayRootLevel, true, false, false, false, isSelected, false, null, false); + AddNode(Title, Title, -1 + displayRootLevel, true, false, false, false, isSelected, false, null); foreach (var treeData in treeDatas) { @@ -123,8 +123,7 @@ public void Load(IEnumerable treeDatas) isSelected = selectedNodePath != null && nodePath == selectedNodePath; - lastAddedNode = AddNode(nodePath, label, level + displayRootLevel + (parentIsPromoted ? 1 : 0), isFolder, isActive, nodeIsHidden, - nodeIsCollapsed, isSelected, isChecked, treeNodeTreeData, false); + lastAddedNode = AddNode(nodePath, label, level + displayRootLevel + (parentIsPromoted ? 1 : 0), isFolder, isActive, nodeIsHidden, nodeIsCollapsed, isSelected, isChecked, treeNodeTreeData); } } } @@ -204,9 +203,9 @@ public void SetCheckStateOnAll(bool isChecked) } } - protected TNode AddNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isSelected, bool isChecked, TData? treeData, bool isContainer) + protected TNode AddNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isSelected, bool isChecked, TData? treeData) { - var node = CreateTreeNode(path, label, level, isFolder, isActive, isHidden, isCollapsed, isChecked, treeData, isContainer); + var node = CreateTreeNode(path, label, level, isFolder, isActive, isHidden, isCollapsed, isChecked, treeData); SetNodeIcon(node); Nodes.Add(node); @@ -407,7 +406,8 @@ private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) protected abstract IEnumerable GetCollapsedFolders(); protected abstract void RemoveCheckedNode(TNode node); protected abstract void AddCheckedNode(TNode node); - protected abstract TNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, TData? treeData, bool isContainer); + protected abstract TNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, TData? treeData); + protected abstract void SetNodeIcon(TNode node); public string SelectedNodePath => SelectedNode?.Path; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs index d7d9a03c4..cd81fcfe2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ChangesTreeControl.cs @@ -186,7 +186,7 @@ protected Texture GetNodeIconBadge(ChangesTreeNode node) return Styles.GetFileStatusIcon(gitFileStatus, node.IsLocked); } - protected override ChangesTreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitStatusEntryTreeData? treeData, bool isContainer) + protected override ChangesTreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitStatusEntryTreeData? treeData) { var gitStatusEntry = GitStatusEntry.Default; var isLocked = false; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs index 206f7edbb..3dc82e14c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/TreeControl.cs @@ -660,14 +660,13 @@ protected Texture GetNodeIcon(TreeNode node) return nodeIcon; } - protected override TreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitBranchTreeData? treeData, bool isContainer) + protected override TreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, GitBranchTreeData? treeData) { var node = new TreeNode { Path = path, Label = label, Level = level, IsFolder = isFolder, - IsContainer = isContainer, IsActive = isActive, IsHidden = isHidden, IsCollapsed = isCollapsed, diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index 94485a63c..ec0c03467 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -113,14 +113,13 @@ protected override void AddCheckedNode(TestTreeNode node) TestTreeListener.AddCheckedNode(node); } - protected override TestTreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, TestTreeData? treeData, bool isContainer) + protected override TestTreeNode CreateTreeNode(string path, string label, int level, bool isFolder, bool isActive, bool isHidden, bool isCollapsed, bool isChecked, TestTreeData? treeData) { if (traceLogging) { Logger.Trace( - "CreateTreeNode(path: {0}, label: {1}, level: {2}, isFolder: {3}, " + - "isActive: {4}, isHidden: {5}, isCollapsed: {6}, isChecked: {7}, treeData: {8})", path, label, - level, isFolder, isActive, isHidden, isCollapsed, isChecked, treeData?.ToString() ?? "[NULL]"); + "CreateTreeNode(path: {0}, label: {1}, level: {2}, isFolder: {3}, isActive: {4}, isHidden: {5}, isCollapsed: {6}, isChecked: {7}, treeData: {8})", + path, label, level, isFolder, isActive, isHidden, isCollapsed, isChecked, treeData?.ToString() ?? "[NULL]"); } TestTreeListener.CreateTreeNode(path, label, level, isFolder, isActive, isHidden, isCollapsed, isChecked, From a9f3837f69c63ff2d021f7d3ddb669b62d2fa9e3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 15:31:28 -0400 Subject: [PATCH 342/567] Formatting --- src/GitHub.Api/Git/GitStatusEntry.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/GitHub.Api/Git/GitStatusEntry.cs b/src/GitHub.Api/Git/GitStatusEntry.cs index 1421c5554..5de898770 100644 --- a/src/GitHub.Api/Git/GitStatusEntry.cs +++ b/src/GitHub.Api/Git/GitStatusEntry.cs @@ -14,8 +14,7 @@ public struct GitStatusEntry public GitFileStatus status; public bool staged; - public GitStatusEntry(string path, string fullPath, string projectPath, - GitFileStatus status, + public GitStatusEntry(string path, string fullPath, string projectPath, GitFileStatus status, string originalPath = null, bool staged = false) { Guard.ArgumentNotNullOrWhiteSpace(path, "path"); From a5f8353435b8099a2fa6f629e1ae769005f80c91 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 17:04:32 -0400 Subject: [PATCH 343/567] Parsing just the remote branch name in BranchListOutputProcessor --- src/GitHub.Api/Git/GitBranch.cs | 9 +++++---- .../OutputProcessors/BranchListOutputProcessor.cs | 5 +++++ .../Process/ProcessManagerIntegrationTests.cs | 2 +- .../UnitTests/IO/BranchListOutputProcessorTests.cs | 12 +++++++++--- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/GitHub.Api/Git/GitBranch.cs b/src/GitHub.Api/Git/GitBranch.cs index 857212810..cef272c6e 100644 --- a/src/GitHub.Api/Git/GitBranch.cs +++ b/src/GitHub.Api/Git/GitBranch.cs @@ -10,12 +10,12 @@ public struct GitBranch public string name; public string tracking; - public GitBranch(string name, string tracking) + public GitBranch(string name, string tracking = null) { Guard.ArgumentNotNullOrWhiteSpace(name, "name"); this.name = name; - this.tracking = tracking; + this.tracking = tracking ?? string.Empty; } public override int GetHashCode() @@ -64,7 +64,8 @@ public bool Equals(GitBranch other) public override string ToString() { - return $"{Name} Tracking? {Tracking}"; + var s = Tracking ?? "[NULL]"; + return $"{Name} Tracking? {s}"; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs index 2a46e7474..85172a96d 100644 --- a/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/BranchListOutputProcessor.cs @@ -37,6 +37,11 @@ public override void LineReceived(string line) if (tracking) { trackingName = proc.ReadChunk('[', ']'); + var indexOf = trackingName.IndexOf(':'); + if (indexOf != -1) + { + trackingName = trackingName.Substring(0, indexOf); + } } var branch = new GitBranch(name, trackingName); diff --git a/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs b/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs index bdd418605..454a81ee2 100644 --- a/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs +++ b/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs @@ -23,7 +23,7 @@ public async Task BranchListTest() .StartAsAsync(); gitBranches.Should().BeEquivalentTo( - new GitBranch("master", "origin/master: behind 1"), + new GitBranch("master", "origin/master"), new GitBranch("feature/document", "origin/feature/document")); } diff --git a/src/tests/UnitTests/IO/BranchListOutputProcessorTests.cs b/src/tests/UnitTests/IO/BranchListOutputProcessorTests.cs index f22cc237a..6041652f2 100644 --- a/src/tests/UnitTests/IO/BranchListOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/BranchListOutputProcessorTests.cs @@ -15,14 +15,20 @@ public void ShouldProcessOutput() { "* master ef7ecf9 [origin/master] Some project master", " feature/feature-1 f47d41b Untracked Feature 1", - " bugfixes/bugfix-1 e1b7f22 [origin/bugfixes/bugfix-1] Tracked Local Bugfix" + " bugfixes/bugfix-1 e1b7f22 [origin/bugfixes/bugfix-1] Tracked Local Bugfix", + " bugfixes/bugfix-2 e1b7f22 [origin/bugfixes/bugfix-2: ahead 3] Ahead with some changes", + " bugfixes/bugfix-3 e1b7f22 [origin/bugfixes/bugfix-3: ahead 3, behind 116] Ahead and Behind", + " bugfixes/bugfix-4 e1b7f22 [origin/bugfixes/bugfix-4: gone] No longer on server", }; AssertProcessOutput(output, new[] { new GitBranch("master", "origin/master"), - new GitBranch("feature/feature-1", ""), + new GitBranch("feature/feature-1"), new GitBranch("bugfixes/bugfix-1", "origin/bugfixes/bugfix-1"), + new GitBranch("bugfixes/bugfix-2", "origin/bugfixes/bugfix-2"), + new GitBranch("bugfixes/bugfix-3", "origin/bugfixes/bugfix-3"), + new GitBranch("bugfixes/bugfix-4", "origin/bugfixes/bugfix-4"), }); } @@ -44,4 +50,4 @@ private void AssertProcessOutput(IEnumerable lines, GitBranch[] expected results.ShouldAllBeEquivalentTo(expected); } } -} \ No newline at end of file +} From ff753f3fef254b33fb79c054c057feaf280b870e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 17:05:32 -0400 Subject: [PATCH 344/567] Window field updated incorrectly --- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 53a757910..c7a664387 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -294,7 +294,7 @@ private void MaybeUpdateData() if (currentRemoteName != updatedRepoRemote) { - currentRemoteName = updatedRepoBranch; + currentRemoteName = updatedRepoRemote; shouldUpdateContentFields = true; } From f91d0920c97bbf6965eb5fadfd8eec8a34d35423 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 17:17:54 -0400 Subject: [PATCH 345/567] Correctly setting the remote tracking branch --- src/GitHub.Api/Git/Repository.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index b1516bf85..38b499f5e 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -247,7 +247,7 @@ private void RepositoryManagerOnCurrentBranchUpdated(ConfigBranch? branch, Confi { var data = new RepositoryInfoCacheData(); data.CurrentConfigBranch = branch; - data.CurrentGitBranch = branch.HasValue ? (GitBranch?)GetLocalGitBranch(branch.Value.name, branch.Value) : null; + data.CurrentGitBranch = branch.HasValue ? (GitBranch?)GetLocalGitBranch(branch.Value) : null; data.CurrentConfigRemote = remote; data.CurrentGitRemote = remote.HasValue ? (GitRemote?)GetGitRemote(remote.Value) : null; data.CurrentHead = head; @@ -303,15 +303,15 @@ private void RepositoryManagerOnRemoteBranchesUpdated(Dictionary localConfigBranchDictionary) { taskManager.RunInUI(() => { - var gitLocalBranches = localConfigBranchDictionary.Values.Select(x => GetLocalGitBranch(CurrentBranchName, x)).ToArray(); + var gitLocalBranches = localConfigBranchDictionary.Values.Select(x => GetLocalGitBranch(x)).ToArray(); cacheContainer.BranchCache.SetLocals(localConfigBranchDictionary, gitLocalBranches); }); } - private static GitBranch GetLocalGitBranch(string currentBranchName, ConfigBranch x) + private static GitBranch GetLocalGitBranch(ConfigBranch x) { var branchName = x.Name; - var trackingName = x.IsTracking ? x.Remote.Value.Name + "/" + branchName : "[None]"; + var trackingName = x.IsTracking ? x.Remote.Value.Name + "/" + branchName : null; return new GitBranch(branchName, trackingName); } From 0e0f69ef6521bc1e9961774a42ce66c40a566bea Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 17:19:33 -0400 Subject: [PATCH 346/567] Adding a field to window to determine a branch is tracked remotely --- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index c7a664387..896160fe9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -40,6 +40,7 @@ class Window : BaseWindow [SerializeField] private int statusAhead; [SerializeField] private int statusBehind; [SerializeField] private bool hasItemsToCommit; + [SerializeField] private bool isTrackingRemoteBranch; [SerializeField] private GUIContent currentBranchContent; [SerializeField] private GUIContent currentRemoteUrlContent; [SerializeField] private CacheUpdateEvent lastCurrentBranchAndRemoteChangedEvent; @@ -279,7 +280,17 @@ private void MaybeUpdateData() currentBranchAndRemoteHasUpdate = false; var repositoryCurrentBranch = Repository.CurrentBranch; - var updatedRepoBranch = repositoryCurrentBranch.HasValue ? repositoryCurrentBranch.Value.Name : null; + string updatedRepoBranch; + if (repositoryCurrentBranch.HasValue) + { + updatedRepoBranch = repositoryCurrentBranch.Value.Name; + isTrackingRemoteBranch = !string.IsNullOrEmpty(repositoryCurrentBranch.Value.Tracking); + } + else + { + updatedRepoBranch = null; + isTrackingRemoteBranch = false; + } var repositoryCurrentRemote = Repository.CurrentRemote; if (repositoryCurrentRemote.HasValue) @@ -313,6 +324,8 @@ private void MaybeUpdateData() } else { + isTrackingRemoteBranch = false; + if (currentRemoteName != null) { currentRemoteName = null; @@ -591,7 +604,7 @@ private void DoActionbarGUI() EditorGUI.EndDisabledGroup(); // Push button - EditorGUI.BeginDisabledGroup(currentRemoteName == null || statusAhead == 0); + EditorGUI.BeginDisabledGroup(currentRemoteName == null || isTrackingRemoteBranch && statusAhead == 0); { var pushButtonText = statusAhead > 0 ? new GUIContent(String.Format(Localization.PushButtonCount, statusAhead)) : pushButtonContent; var pushClicked = GUILayout.Button(pushButtonText, Styles.ToolbarButtonStyle); From a30ab4943f9e5270a02085f29cb2c04b82bb4774 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 17:23:39 -0400 Subject: [PATCH 347/567] Formatting code --- src/GitHub.Api/Git/GitBranch.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/GitHub.Api/Git/GitBranch.cs b/src/GitHub.Api/Git/GitBranch.cs index cef272c6e..ecb6fede9 100644 --- a/src/GitHub.Api/Git/GitBranch.cs +++ b/src/GitHub.Api/Git/GitBranch.cs @@ -64,8 +64,7 @@ public bool Equals(GitBranch other) public override string ToString() { - var s = Tracking ?? "[NULL]"; - return $"{Name} Tracking? {s}"; + return $"{Name} Tracking? {Tracking ?? "[NULL]"}"; } } } From 11a3df1c6c0d0a90b06d9ebd1745176d52a03790 Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Wed, 27 Jun 2018 18:07:39 -0400 Subject: [PATCH 348/567] Revising sorting algorithm because of edge case failure. --- .../OutputProcessors/StatusOutputProcessor.cs | 29 +++++--------- .../IO/StatusOutputProcessorTests.cs | 39 +++++++++++++++++++ 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs index 93bf15d91..ec9743570 100644 --- a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs @@ -224,30 +224,21 @@ public int Compare(string x, string y) Guard.ArgumentNotNull(x, nameof(x)); Guard.ArgumentNotNull(y, nameof(y)); - var metaString = ".meta"; - var xIsMeta = x.EndsWith(metaString); - var yIsMeta = y.EndsWith(metaString); + const string meta = ".meta"; + var xHasMeta = x.EndsWith(meta); + var yHasMeta = y.EndsWith(meta); - if (xIsMeta || yIsMeta) - { - var compareX = !xIsMeta ? x : x.Substring(0, x.Length - 5); - var compareY = !yIsMeta ? y : y.Substring(0, y.Length - 5); + if(!xHasMeta && !yHasMeta) return StringComparer.InvariantCulture.Compare(x, y); - var comparisonResult = StringComparer.InvariantCultureIgnoreCase.Compare(compareX, compareY); - if (comparisonResult != 0) - { - return comparisonResult; - } + var xPure = xHasMeta ? x.Substring(0, x.Length - meta.Length) : x; + var yPure = yHasMeta ? y.Substring(0, y.Length - meta.Length) : y; - if (xIsMeta) - { - return 1; - } - - return -1; + if (xHasMeta) + { + return xPure.Equals(y) ? 1 : StringComparer.InvariantCulture.Compare(xPure, yPure); } - return StringComparer.InvariantCultureIgnoreCase.Compare(x, y); + return yPure.Equals(x) ? -1 : StringComparer.InvariantCulture.Compare(xPure, yPure); } } } diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index 4478dd6b3..acdec8fa3 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -303,6 +303,45 @@ public void ShouldSortOutputCorrectly2() }); } + public void ShouldSortOutputCorrectly3() + { + var output = new[] + { + "## master", + "?? Assets/Assets.Test.dll", + "?? Assets/Assets.Test.dll.meta", + "?? Plugins/GitHub.Unity.dll", + "?? Plugins/GitHub.Unity.dll.mdb", + "?? Plugins/GitHub.Unity.dll.mdb.meta", + "?? Plugins/GitHub.Unity2.dll", + "?? Plugins/GitHub.Unity2.dll.mdb", + "?? Plugins/GitHub.Unity2.dll.mdb.meta", + "?? Plugins/GitHub.Unity2.dll.meta", + "?? Plugins/GitHub.Unity.dll.meta", + "?? blah.txt", + null + }; + + AssertProcessOutput(output, new GitStatus + { + LocalBranch = "master", + Entries = new List + { + new GitStatusEntry(@"Assets/Assets.Test.dll", TestRootPath + @"\Assets/Assets.Test.dll", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Assets/Assets.Test.dll.meta", TestRootPath + @"\Assets/Assets.Test.dll.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"blah.txt", TestRootPath + @"\blah.txt", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll", TestRootPath + @"\Plugins/GitHub.Unity.dll", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll.meta", TestRootPath + @"\Plugins/GitHub.Unity.dll.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll.mdb", TestRootPath + @"\Plugins/GitHub.Unity.dll.mdb", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity.dll.mdb.meta", TestRootPath + @"\Plugins/GitHub.Unity.dll.mdb.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity2.dll", TestRootPath + @"\Plugins/GitHub.Unity2.dll", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity2.dll.meta", TestRootPath + @"\Plugins/GitHub.Unity2.dll.meta", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity2.dll.mdb", TestRootPath + @"\Plugins/GitHub.Unity2.dll.mdb", null, GitFileStatus.Untracked), + new GitStatusEntry(@"Plugins/GitHub.Unity2.dll.mdb.meta", TestRootPath + @"\Plugins/GitHub.Unity2.dll.mdb.meta", null, GitFileStatus.Untracked), + } + }); + } + private void AssertProcessOutput(IEnumerable lines, GitStatus expected) { var gitObjectFactory = SubstituteFactory.CreateGitObjectFactory(TestRootPath); From 30d4de4e6455c73a90b76d0f118a1c31e93a66e2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 18:33:54 -0400 Subject: [PATCH 349/567] Adding missing test attribute --- src/tests/UnitTests/IO/StatusOutputProcessorTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index acdec8fa3..2f885915f 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -303,6 +303,7 @@ public void ShouldSortOutputCorrectly2() }); } + [Test] public void ShouldSortOutputCorrectly3() { var output = new[] From 815d6554dac24c9c590b5e7ae19bb5bbc38236c4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 18:40:55 -0400 Subject: [PATCH 350/567] Removing extra tests --- .../IO/StatusOutputProcessorTests.cs | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index 2f885915f..a0bd010c1 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -247,64 +247,6 @@ public void ShouldParseCleanWorkingTreeTracked() [Test] public void ShouldSortOutputCorrectly() - { - var output = new[] - { - "## master", - "?? GitHub.Unity.dll", - "?? GitHub.Unity.dll.mdb", - "?? GitHub.Unity.dll.mdb.meta", - "?? GitHub.Unity.dll.meta", - null - }; - - AssertProcessOutput(output, new GitStatus - { - LocalBranch = "master", - Entries = new List - { - new GitStatusEntry(@"GitHub.Unity.dll", TestRootPath + @"\GitHub.Unity.dll", null, GitFileStatus.Untracked), - new GitStatusEntry(@"GitHub.Unity.dll.meta", TestRootPath + @"\GitHub.Unity.dll.meta", null, GitFileStatus.Untracked), - new GitStatusEntry(@"GitHub.Unity.dll.mdb", TestRootPath + @"\GitHub.Unity.dll.mdb", null, GitFileStatus.Untracked), - new GitStatusEntry(@"GitHub.Unity.dll.mdb.meta", TestRootPath + @"\GitHub.Unity.dll.mdb.meta", null, GitFileStatus.Untracked), - } - }); - } - - [Test] - public void ShouldSortOutputCorrectly2() - { - var output = new[] - { - "## master", - "?? Assets/Assets.Test.dll", - "?? Assets/Assets.Test.dll.meta", - "?? Plugins/GitHub.Unity.dll", - "?? Plugins/GitHub.Unity.dll.mdb", - "?? Plugins/GitHub.Unity.dll.mdb.meta", - "?? Plugins/GitHub.Unity.dll.meta", - "?? blah.txt", - null - }; - - AssertProcessOutput(output, new GitStatus - { - LocalBranch = "master", - Entries = new List - { - new GitStatusEntry(@"Assets/Assets.Test.dll", TestRootPath + @"\Assets/Assets.Test.dll", null, GitFileStatus.Untracked), - new GitStatusEntry(@"Assets/Assets.Test.dll.meta", TestRootPath + @"\Assets/Assets.Test.dll.meta", null, GitFileStatus.Untracked), - new GitStatusEntry(@"blah.txt", TestRootPath + @"\blah.txt", null, GitFileStatus.Untracked), - new GitStatusEntry(@"Plugins/GitHub.Unity.dll", TestRootPath + @"\Plugins/GitHub.Unity.dll", null, GitFileStatus.Untracked), - new GitStatusEntry(@"Plugins/GitHub.Unity.dll.meta", TestRootPath + @"\Plugins/GitHub.Unity.dll.meta", null, GitFileStatus.Untracked), - new GitStatusEntry(@"Plugins/GitHub.Unity.dll.mdb", TestRootPath + @"\Plugins/GitHub.Unity.dll.mdb", null, GitFileStatus.Untracked), - new GitStatusEntry(@"Plugins/GitHub.Unity.dll.mdb.meta", TestRootPath + @"\Plugins/GitHub.Unity.dll.mdb.meta", null, GitFileStatus.Untracked), - } - }); - } - - [Test] - public void ShouldSortOutputCorrectly3() { var output = new[] { From 5217321ca28f7182ac57cc86c068a28d2b90391c Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 27 Jun 2018 18:42:28 -0400 Subject: [PATCH 351/567] Removing const string --- src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs index ec9743570..99f11ebe3 100644 --- a/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs +++ b/src/GitHub.Api/OutputProcessors/StatusOutputProcessor.cs @@ -224,7 +224,7 @@ public int Compare(string x, string y) Guard.ArgumentNotNull(x, nameof(x)); Guard.ArgumentNotNull(y, nameof(y)); - const string meta = ".meta"; + var meta = ".meta"; var xHasMeta = x.EndsWith(meta); var yHasMeta = y.EndsWith(meta); From 229ff6963e829aa08d800cd621c9ee3b57bd2c21 Mon Sep 17 00:00:00 2001 From: ykush Date: Fri, 29 Jun 2018 23:04:14 +0900 Subject: [PATCH 352/567] Fix ReleaseLock MenuItem --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 9a3d66574..c3cc0fb03 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -119,7 +119,7 @@ private static void ContextMenu_Lock() RunLockUnlock(IsObjectUnlocked, CreateLockObjectTask, Localization.RequestLockActionTitle, "Failed to lock: no permissions"); } - [MenuItem(AssetsMenuReleaseLockForced, false, 10001)] + [MenuItem(AssetsMenuReleaseLock, false, 10001)] private static void ContextMenu_Unlock() { RunLockUnlock(IsObjectLocked, x => CreateUnlockObjectTask(x, false), Localization.ReleaseLockActionTitle, "Failed to unlock: no permissions"); From 135bcb9ae1e98d2d502ef52f92d5d19313f37412 Mon Sep 17 00:00:00 2001 From: Thasan Date: Sun, 1 Jul 2018 13:36:31 +0300 Subject: [PATCH 353/567] Add .xcf (gimp) file to lfs at default --- src/GitHub.Api/Resources/.gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/Resources/.gitattributes b/src/GitHub.Api/Resources/.gitattributes index 8f8c2db53..df7ad1b69 100644 --- a/src/GitHub.Api/Resources/.gitattributes +++ b/src/GitHub.Api/Resources/.gitattributes @@ -17,6 +17,7 @@ *.iff filter=lfs diff=lfs merge=lfs -text *.pict filter=lfs diff=lfs merge=lfs -text *.dds filter=lfs diff=lfs merge=lfs -text +*.xcf filter=lfs diff=lfs merge=lfs -text # Audio formats *.mp3 filter=lfs diff=lfs merge=lfs -text @@ -63,4 +64,4 @@ *.7z filter=lfs diff=lfs merge=lfs -text *.gz filter=lfs diff=lfs merge=lfs -text *.rar filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text \ No newline at end of file +*.tar filter=lfs diff=lfs merge=lfs -text From b6a74c504d6444b760b0a103b238c4634e20e5c9 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 2 Jul 2018 08:16:46 -0400 Subject: [PATCH 354/567] Shaking up the input order a bit --- src/tests/UnitTests/IO/StatusOutputProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs index a0bd010c1..299ce511a 100644 --- a/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs +++ b/src/tests/UnitTests/IO/StatusOutputProcessorTests.cs @@ -251,8 +251,8 @@ public void ShouldSortOutputCorrectly() var output = new[] { "## master", - "?? Assets/Assets.Test.dll", "?? Assets/Assets.Test.dll.meta", + "?? Assets/Assets.Test.dll", "?? Plugins/GitHub.Unity.dll", "?? Plugins/GitHub.Unity.dll.mdb", "?? Plugins/GitHub.Unity.dll.mdb.meta", From ce83a2f573cb6236c9d3597c38bfd0c343ecc4ee Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 2 Jul 2018 14:26:09 +0200 Subject: [PATCH 355/567] Fix contributor builds on CI --- .../Assets/Editor/GitHub.Unity/GitHub.Unity.csproj | 3 --- src/tests/IntegrationTests/IntegrationTests.csproj | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index 95b438d79..8588ccebf 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -104,9 +104,6 @@ - - - {b389adaf-62cc-486e-85b4-2d8b078df763} diff --git a/src/tests/IntegrationTests/IntegrationTests.csproj b/src/tests/IntegrationTests/IntegrationTests.csproj index 351a6d571..aae88d537 100644 --- a/src/tests/IntegrationTests/IntegrationTests.csproj +++ b/src/tests/IntegrationTests/IntegrationTests.csproj @@ -77,7 +77,7 @@ - + From 9a087192053ef6d7b03d133f0674e2eeacb991e9 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 9 Jul 2018 15:01:58 +0200 Subject: [PATCH 356/567] Bump version to 1.0.1 --- common/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 2ef396d09..276bb3268 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -32,8 +32,8 @@ namespace System { internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics - internal const string VersionForAssembly = "1.0.0"; + internal const string VersionForAssembly = "1.0.1"; // Actual real version - internal const string Version = "1.0.0"; + internal const string Version = "1.0.1"; } } From eac57917ed75bfe280cdd56baf0a77a2ede815e6 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Koga Date: Wed, 11 Jul 2018 22:36:19 +0900 Subject: [PATCH 357/567] Support git-worktree on History --- src/GitHub.Api/Git/RepositoryManager.cs | 22 ++++++++++++++++--- src/GitHub.Api/Platform/DefaultEnvironment.cs | 6 ++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index 63c08ea49..8ee1ae242 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -69,6 +69,7 @@ public RepositoryPathConfiguration(NPath repositoryPath) RepositoryPath = repositoryPath; DotGitPath = repositoryPath.Combine(".git"); + NPath CommonPath; if (DotGitPath.FileExists()) { DotGitPath = @@ -76,13 +77,28 @@ public RepositoryPathConfiguration(NPath repositoryPath) .Where(x => x.StartsWith("gitdir:")) .Select(x => x.Substring(7).Trim().ToNPath()) .First(); + if (DotGitPath.Combine("commondir").FileExists()) + { + CommonPath = DotGitPath.Combine("commondir").ReadAllLines() + .Select(x => x.Trim().ToNPath()) + .First(); + CommonPath = DotGitPath.Combine(CommonPath); + } + else + { + CommonPath = DotGitPath; + } + } + else + { + CommonPath = DotGitPath; } - BranchesPath = DotGitPath.Combine("refs", "heads"); - RemotesPath = DotGitPath.Combine("refs", "remotes"); + BranchesPath = CommonPath.Combine("refs", "heads"); + RemotesPath = CommonPath.Combine("refs", "remotes"); DotGitIndex = DotGitPath.Combine("index"); DotGitHead = DotGitPath.Combine("HEAD"); - DotGitConfig = DotGitPath.Combine("config"); + DotGitConfig = CommonPath.Combine("config"); DotGitCommitEditMsg = DotGitPath.Combine("COMMIT_EDITMSG"); } diff --git a/src/GitHub.Api/Platform/DefaultEnvironment.cs b/src/GitHub.Api/Platform/DefaultEnvironment.cs index 86e1ea5d0..d056f21c1 100644 --- a/src/GitHub.Api/Platform/DefaultEnvironment.cs +++ b/src/GitHub.Api/Platform/DefaultEnvironment.cs @@ -89,9 +89,9 @@ public void InitializeRepository(NPath? repositoryPath = null) expectedRepositoryPath = repositoryPath != null ? repositoryPath.Value : UnityProjectPath; - if (!expectedRepositoryPath.DirectoryExists(".git")) + if (!expectedRepositoryPath.Exists(".git")) { - NPath reporoot = UnityProjectPath.RecursiveParents.FirstOrDefault(d => d.DirectoryExists(".git")); + NPath reporoot = UnityProjectPath.RecursiveParents.FirstOrDefault(d => d.Exists(".git")); if (reporoot.IsInitialized) expectedRepositoryPath = reporoot; } @@ -102,7 +102,7 @@ public void InitializeRepository(NPath? repositoryPath = null) } FileSystem.SetCurrentDirectory(expectedRepositoryPath); - if (expectedRepositoryPath.DirectoryExists(".git")) + if (expectedRepositoryPath.Exists(".git")) { RepositoryPath = expectedRepositoryPath; Repository = new Repository(RepositoryPath, CacheContainer); From 549406a9a410850d417795ea3a69333386190419 Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Thu, 12 Jul 2018 13:06:18 -0700 Subject: [PATCH 358/567] add documentation for file locks --- docs/using/images/locked-scene.png | 3 +++ docs/using/images/locks-view-right-click.png | 3 +++ docs/using/images/locks-view.png | 3 +++ docs/using/images/release-lock.png | 3 +++ docs/using/images/request-lock.png | 3 +++ docs/using/locking-files.md | 26 ++++++++++++++++++++ 6 files changed, 41 insertions(+) create mode 100644 docs/using/images/locked-scene.png create mode 100644 docs/using/images/locks-view-right-click.png create mode 100644 docs/using/images/locks-view.png create mode 100644 docs/using/images/release-lock.png create mode 100644 docs/using/images/request-lock.png create mode 100644 docs/using/locking-files.md diff --git a/docs/using/images/locked-scene.png b/docs/using/images/locked-scene.png new file mode 100644 index 000000000..7a839141d --- /dev/null +++ b/docs/using/images/locked-scene.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbc246b18cbd173c19f392fe7ab85bffb8a97a7616ef99e7e37496e307924e49 +size 51983 diff --git a/docs/using/images/locks-view-right-click.png b/docs/using/images/locks-view-right-click.png new file mode 100644 index 000000000..0ee14312c --- /dev/null +++ b/docs/using/images/locks-view-right-click.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:590dae09b3c029e48c3703d6c85a6774b062ebea46f11c2c062d97fedcb436df +size 27251 diff --git a/docs/using/images/locks-view.png b/docs/using/images/locks-view.png new file mode 100644 index 000000000..e8790d74e --- /dev/null +++ b/docs/using/images/locks-view.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:118f2b2e1bb82b598107eaa73db9a077374ff4f9601e47677eca7296353b1bc3 +size 23274 diff --git a/docs/using/images/release-lock.png b/docs/using/images/release-lock.png new file mode 100644 index 000000000..545b81c8a --- /dev/null +++ b/docs/using/images/release-lock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f6e05a714d270016d1f8ad4e018a895ed2b023fe59bd34a28df8d315c065649 +size 140913 diff --git a/docs/using/images/request-lock.png b/docs/using/images/request-lock.png new file mode 100644 index 000000000..b877e1d8f --- /dev/null +++ b/docs/using/images/request-lock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f25877141bed2dcbdc4324edcbd802b6f216771e80de704f20dc99a3fdc718b +size 136078 diff --git a/docs/using/locking-files.md b/docs/using/locking-files.md new file mode 100644 index 000000000..0b3c875b9 --- /dev/null +++ b/docs/using/locking-files.md @@ -0,0 +1,26 @@ +# Locking files + +## Request locks + +From the Project tab, right-click on a file to open the context menu and select `Request Lock`. + + +## View locks + +After requesting a lock, a lock icon appears in the bottom right-hand corner of the file. + + +A list of all locked files will appear in the **Locks** view in the GitHub tab. + + +## Release locks + +There are two ways to release locks: + +1. From the Project tab, right-click on the locked file to open the context menu and select the option to `Release Lock`. + + +2. From the GitHub tab under **Locks** view, right-click to open the context menu and select to `Release Lock`. + + +There are also two options for how to release a lock on a file. Always choose the `Release Lock` option first. The `Release Lock (forced)` option can be used to remove someone else's lock. From 7f33d52fc614919765dd23a1fcf45de4952620ec Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Fri, 13 Jul 2018 17:43:36 -0700 Subject: [PATCH 359/567] add additional option for locking and unlocking files --- docs/using/locking-files.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/using/locking-files.md b/docs/using/locking-files.md index 0b3c875b9..78bacefec 100644 --- a/docs/using/locking-files.md +++ b/docs/using/locking-files.md @@ -5,6 +5,8 @@ From the Project tab, right-click on a file to open the context menu and select `Request Lock`. +An additional way to lock a file is by selecting it and going to `Assets` -> `Request Lock`. + ## View locks After requesting a lock, a lock icon appears in the bottom right-hand corner of the file. @@ -15,7 +17,7 @@ A list of all locked files will appear in the **Locks** view in the GitHub tab. ## Release locks -There are two ways to release locks: +There are three ways to release locks: 1. From the Project tab, right-click on the locked file to open the context menu and select the option to `Release Lock`. @@ -23,4 +25,6 @@ There are two ways to release locks: 2. From the GitHub tab under **Locks** view, right-click to open the context menu and select to `Release Lock`. -There are also two options for how to release a lock on a file. Always choose the `Release Lock` option first. The `Release Lock (forced)` option can be used to remove someone else's lock. +3. Select the file to unlock and go to select the menu option `Assets` -> `Release Lock`. + +Note: There are also two options for how to release a lock on a file. Always choose the `Release Lock` option first. The `Release Lock (forced)` option can be used to remove someone else's lock. From afc0c001d8d377b6a3c0acd721fceea108bb1cb4 Mon Sep 17 00:00:00 2001 From: Meaghan Lewis Date: Fri, 13 Jul 2018 17:48:15 -0700 Subject: [PATCH 360/567] Update ordering of images --- docs/using/locking-files.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using/locking-files.md b/docs/using/locking-files.md index 78bacefec..f90c8e7a3 100644 --- a/docs/using/locking-files.md +++ b/docs/using/locking-files.md @@ -10,10 +10,10 @@ An additional way to lock a file is by selecting it and going to `Assets` -> `Re ## View locks After requesting a lock, a lock icon appears in the bottom right-hand corner of the file. - + A list of all locked files will appear in the **Locks** view in the GitHub tab. - + ## Release locks @@ -22,7 +22,7 @@ There are three ways to release locks: 1. From the Project tab, right-click on the locked file to open the context menu and select the option to `Release Lock`. -2. From the GitHub tab under **Locks** view, right-click to open the context menu and select to `Release Lock`. +2. From the GitHub tab under the **Locks** view, right-click to open the context menu and select to `Release Lock`. 3. Select the file to unlock and go to select the menu option `Assets` -> `Release Lock`. From b9358e226c913f04d2cdc08e5e37c1857ef15e17 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 24 Jul 2018 11:32:19 -0400 Subject: [PATCH 361/567] Add support for worktree to RepositoryWatcher --- src/GitHub.Api/Events/RepositoryWatcher.cs | 29 +++++++++++++++++++++- src/GitHub.Api/Git/RepositoryManager.cs | 24 ++++++++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/GitHub.Api/Events/RepositoryWatcher.cs b/src/GitHub.Api/Events/RepositoryWatcher.cs index 582790303..19a172842 100644 --- a/src/GitHub.Api/Events/RepositoryWatcher.cs +++ b/src/GitHub.Api/Events/RepositoryWatcher.cs @@ -30,6 +30,7 @@ class RepositoryWatcher : IRepositoryWatcher private readonly NPath[] ignoredPaths; private readonly ManualResetEventSlim pauseEvent; private NativeInterface nativeInterface; + private NativeInterface worktreeNativeInterface; private bool running; private int lastCountOfProcessedEvents = 0; private bool processingEvents; @@ -64,6 +65,11 @@ public void Initialize() try { nativeInterface = new NativeInterface(pathsRepositoryPath); + + if (paths.IsWorktree) + { + worktreeNativeInterface = new NativeInterface(paths.WorktreeDotGitPath); + } } catch (Exception ex) { @@ -80,6 +86,18 @@ public void Start() } Logger.Trace("Watching Path: \"{0}\"", paths.RepositoryPath.ToString()); + + if (paths.IsWorktree) + { + if (worktreeNativeInterface == null) + { + Logger.Warning("Worktree NativeInterface is null"); + throw new InvalidOperationException("Worktree NativeInterface is null"); + } + + Logger.Trace("Watching Additional Path for Worktree: \"{0}\"", paths.WorktreeDotGitPath); + } + running = true; pauseEvent.Reset(); Task.Factory.StartNew(WatcherLoop, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); @@ -131,6 +149,15 @@ public int CheckAndProcessEvents() processedEventCount = ProcessEvents(fileEvents); } + if (worktreeNativeInterface != null) + { + fileEvents = worktreeNativeInterface.GetEvents(); + if (fileEvents.Length > 0) + { + processedEventCount = processedEventCount + ProcessEvents(fileEvents); + } + } + lastCountOfProcessedEvents = processedEventCount; processingEvents = false; signalProcessingEventsDone.Set(); @@ -158,7 +185,7 @@ private int ProcessEvents(Event[] fileEvents) var fileA = eventDirectory.Combine(fileEvent.FileA); // handling events in .git/* - if (fileA.IsChildOf(paths.DotGitPath)) + if (fileA.IsChildOf(paths.DotGitPath) || (paths.WorktreeDotGitPath.IsInitialized && fileA.IsChildOf(paths.WorktreeDotGitPath))) { if (!events.Contains(EventType.ConfigChanged) && fileA.Equals(paths.DotGitConfig)) { diff --git a/src/GitHub.Api/Git/RepositoryManager.cs b/src/GitHub.Api/Git/RepositoryManager.cs index 8ee1ae242..d84db5db6 100644 --- a/src/GitHub.Api/Git/RepositoryManager.cs +++ b/src/GitHub.Api/Git/RepositoryManager.cs @@ -60,6 +60,8 @@ interface IRepositoryPathConfiguration NPath DotGitIndex { get; } NPath DotGitHead { get; } NPath DotGitConfig { get; } + NPath WorktreeDotGitPath { get; } + bool IsWorktree { get; } } class RepositoryPathConfiguration : IRepositoryPathConfiguration @@ -67,9 +69,10 @@ class RepositoryPathConfiguration : IRepositoryPathConfiguration public RepositoryPathConfiguration(NPath repositoryPath) { RepositoryPath = repositoryPath; + WorktreeDotGitPath = NPath.Default; DotGitPath = repositoryPath.Combine(".git"); - NPath CommonPath; + NPath commonPath; if (DotGitPath.FileExists()) { DotGitPath = @@ -79,30 +82,35 @@ public RepositoryPathConfiguration(NPath repositoryPath) .First(); if (DotGitPath.Combine("commondir").FileExists()) { - CommonPath = DotGitPath.Combine("commondir").ReadAllLines() + commonPath = DotGitPath.Combine("commondir").ReadAllLines() .Select(x => x.Trim().ToNPath()) .First(); - CommonPath = DotGitPath.Combine(CommonPath); + commonPath = DotGitPath.Combine(commonPath); + + IsWorktree = true; + WorktreeDotGitPath = commonPath; } else { - CommonPath = DotGitPath; + commonPath = DotGitPath; } } else { - CommonPath = DotGitPath; + commonPath = DotGitPath; } - BranchesPath = CommonPath.Combine("refs", "heads"); - RemotesPath = CommonPath.Combine("refs", "remotes"); + BranchesPath = commonPath.Combine("refs", "heads"); + RemotesPath = commonPath.Combine("refs", "remotes"); DotGitIndex = DotGitPath.Combine("index"); DotGitHead = DotGitPath.Combine("HEAD"); - DotGitConfig = CommonPath.Combine("config"); + DotGitConfig = commonPath.Combine("config"); DotGitCommitEditMsg = DotGitPath.Combine("COMMIT_EDITMSG"); } + public bool IsWorktree { get; } public NPath RepositoryPath { get; } + public NPath WorktreeDotGitPath { get; } public NPath DotGitPath { get; } public NPath BranchesPath { get; } public NPath RemotesPath { get; } From 04b8aa4bacae7036a71274f76cfbc499efe8bd62 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 26 Jul 2018 17:36:32 +0200 Subject: [PATCH 362/567] Add a dev oauth app config similar to how desktop does it (https://github.com/desktop/desktop/blob/master/docs/technical/oauth.md) --- src/GitHub.Api/Application/ApplicationInfo.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/GitHub.Api/Application/ApplicationInfo.cs b/src/GitHub.Api/Application/ApplicationInfo.cs index 931e60cac..f15f7d482 100644 --- a/src/GitHub.Api/Application/ApplicationInfo.cs +++ b/src/GitHub.Api/Application/ApplicationInfo.cs @@ -14,8 +14,24 @@ static partial class ApplicationInfo #endif public const string ApplicationDescription = "GitHub for Unity"; +#if DEBUG +/* + For external contributors, we have bundled a developer OAuth application + called `GitHub for Unity (dev)` so that you can complete the sign in flow + locally without needing to configure your own application. + This is for testing only and it is (obviously) public, proceed with caution. + + For a release build, you should create a new oauth application on github.com, + copy the `common/ApplicationInfo_Local.cs-example` + template to `common/ApplicationInfo_Local.cs` and fill out the `myClientId` and + `myClientSecret` fields for your oauth app. + */ + internal static string ClientId { get; private set; } = "924a97f36926f535e72c"; + internal static string ClientSecret { get; private set; } = "b4fa550b7f8e38034c6b1339084fa125eebb6155"; +#else internal static string ClientId { get; private set; } = ""; internal static string ClientSecret { get; private set; } = ""; +#endif public static string Version { get { return System.AssemblyVersionInformation.Version; } } From cb2ab5d331c35fc9493db62dc754eb7cef349cb6 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 26 Jul 2018 17:37:31 +0200 Subject: [PATCH 363/567] Add the git zips to the development folder so it's easy to bootstrap --- src/GitHub.Api/GitHub.Api.csproj | 19 ++++++++++++++++++- src/UnityExtension/.gitignore | 5 ++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 4b2137e88..5c9305279 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -295,8 +295,25 @@ Other similar extension points exist, see Microsoft.Common.targets. + --> + + + + + - --> \ No newline at end of file diff --git a/src/UnityExtension/.gitignore b/src/UnityExtension/.gitignore index a2411d2f3..848aaec94 100644 --- a/src/UnityExtension/.gitignore +++ b/src/UnityExtension/.gitignore @@ -1,4 +1,7 @@ *.csproj UnityPackageManager JetBrains -UnityExtension.sln \ No newline at end of file +UnityExtension.sln +Assets/**/*.zip +Assets/**/*.md5 +Assets/**/*.json From 3b1e9085ad66d8591df4aed97fa9b2426912c3cf Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 26 Jul 2018 17:45:54 +0200 Subject: [PATCH 364/567] Use local git zips when we're in a debug build --- src/GitHub.Api/Installer/GitInstaller.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 32a158504..4feb21c75 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -55,8 +55,14 @@ public GitInstallationState SetupGitIfNeeded(GitInstallationState state = null) } state = VerifyZipFiles(state); + // on developer builds, prefer local zips over downloading +#if DEVELOPER_BUILD + state = GrabZipFromResourcesIfNeeded(state); + state = GetZipsIfNeeded(state); +#else state = GetZipsIfNeeded(state); state = GrabZipFromResourcesIfNeeded(state); +#endif state = ExtractGit(state); // if installing from zip failed (internet down maybe?), try to find a usable system git From 11091bc2462e012981dbfd6a08e1f241e850422e Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 27 Jul 2018 11:25:56 +0200 Subject: [PATCH 365/567] Update the build docs and move the quick guide to its own file --- README.md | 159 ++---------------------------- docs/contributing/how-to-build.md | 22 ++++- docs/readme.md | 9 +- docs/using/quick-guide.md | 153 ++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 160 deletions(-) create mode 100644 docs/using/quick-guide.md diff --git a/README.md b/README.md index 236bdd910..09f32e31b 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ You can reach the team right here by opening a [new issue](https://github.com/gi [![Build Status](https://ci.appveyor.com/api/projects/status/github/github-for-unity/Unity?branch=master&svg=true)](https://ci.appveyor.com/project/github-windows/unity) [![Join the chat at https://discord.gg/5zH8hVx](https://img.shields.io/badge/discord-join%20chat-7289DA.svg)](https://discord.gg/5zH8hVx) -[![GitHub for Unity live coding on Twitch](https://img.shields.io/badge/twitch-live%20coding-6441A4.svg)](https://www.twitch.tv/sh4na) - ## Notices @@ -16,164 +14,19 @@ Please refer to the [list of known issues](https://github.com/github-for-unity/U From version 0.19 onwards, the location of the plugin has moved to `Assets/Plugins/GitHub`. If you have version 0.18 or lower, you need to delete the `Assets/Editor/GitHub` folder before you install newer versions. You should exit Unity and delete the folder from Explorer/Finder, as Unity will not unload native libraries while it's running. Also, remember to update your `.gitignore` file. -#### Table Of Contents - -[Installing GitHub for Unity](#installing-github-for-unity) - * [Requirements](#requirements) - * [Git on macOS](#git-on-macos) - * [Git on Windows](#git-on-windows) - * [Installation](#installation) - * [Log files](#log-files) - * [Windows](#windows) - * [macOS](#macos) - -[Building and Contributing](#building-and-contributing) - -[Quick Guide to GitHub for Unity](#quick-guide-to-github-for-unity) - * [Opening the GitHub window](#opening-the-github-window) - * [Initialize Repository](#initialize-repository) - * [Authentication](#authentication) - * [Publish a new repository](#publish-a-new-repository) - * [Commiting your work - Changes tab](#commiting-your-work---changes-tab) - * [Pushing/pulling your work - History tab](#pushingpulling-your-work---history-tab) - * [Branches tab](#branches-tab) - * [Settings tab](#settings-tab) - -[More Resources](#more-resources) - -[License](#license) - -## Installing GitHub for Unity - -### Requirements - -- Unity 5.4 or higher - - There's currently an blocker issue opened for 5.3 support, so we know it doesn't run there. Personal edition is fine. -- Git and Git LFS 2.x - -#### Git on macOS - -The current release has limited macOS support. macOS users will need to install the latest [Git](https://git-scm.com/downloads) and [Git LFS](https://git-lfs.github.com/) manually, and make sure these are on the path. You can configure the Git location in the Settings tab on the GitHub window. - -The easiest way of installing git and git lfs is to install [Homebrew](https://brew.sh/) and then do `brew install git git-lfs`. - -Make sure a Git user and email address are set in the `~/.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `~/.gitconfig` file and adding the following section, if it doesn't exist yet: - -``` -[user] - name = Your Name - email = Your Email -``` - -#### Git on Windows - -The GitHub for Unity extension ships with a bundle of Git and Git LFS, to ensure that you have the correct version. These will be installed into `%LOCALAPPDATA%\GitHubUnity` when the extension runs for the first time. - -Make sure a Git user and email address are set in the `%HOME%\.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `%HOME%\.gitconfig` file and adding the following section, if it doesn't exist yet: - -``` -[user] - name = Your Name - email = Your Email -``` - -Once the extension is installed, you can open a command line with the same Git and Git LFS version that the extension uses by going to `Window` -> `GitHub Command Line` in Unity. - -### Installation - -This extensions needs to be installed (and updated) for each Unity project that you want to version control. -First step is to download the latest package from [the releases page](https://github.com/github-for-unity/Unity/releases); -it will be saved as a file with the extension `.unitypackage`. -To install it, open Unity, then open the project you want to version control, and then double click on the downloaded package. -Alternatively, import the package by clicking Assets, Import Package, Custom Package, then select the downloaded package. - -#### Log files - -##### macOS - -The extension log file can be found at `~/Library/Logs/GitHubUnity/github-unity.log` - -##### Windows - -The extension log file can be found at `%LOCALAPPDATA%\GitHubUnity\github-unity.log` - ## Building and Contributing -The [CONTRIBUTING.md](CONTRIBUTING.md) document will help you get setup and familiar with the source. The [documentation](docs/) folder also contains more resources relevant to the project. - Please read the [How to Build](docs/contributing/how-to-build.md) document for information on how to build GitHub for Unity. -If you're looking for something to work on, check out the [up-for-grabs](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) label. - - -## I have a problem with GitHub for Unity - -First, please search the [open issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen) -and [closed issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aclosed) -to see if your issue hasn't already been reported (it may also be fixed). - -If you can't find an issue that matches what you're seeing, open a [new issue](https://github.com/github-for-unity/Unity/issues/new) -and fill out the template to provide us with enough information to investigate -further. - -## Quick Guide to GitHub for Unity - -### Opening the GitHub window - -You can access the GitHub window by going to Windows -> GitHub. The window opens by default next to the Inspector window. - -### Initialize Repository - -![Initialize repository screenshot](https://user-images.githubusercontent.com/10103121/37807041-bb4446a6-2e19-11e8-9fff-a431309b8515.png) - -If the current Unity project is not in a Git repository, the GitHub for Unity extension will offer to initialize the repository for you. This will: - -- Initialize a git repository at the Unity project root via `git init` -- Initialize git-lfs via `git lfs install` -- Set up a `.gitignore` file at the Unity project root. -- Set up a `.gitattributes` file at the Unity project root with a large list of known binary filetypes (images, audio, etc) that should be tracked by LFS -- Configure the project to serialize meta files as text -- Create an initial commit with the `.gitignore` and `.gitattributes` file. - -### Authentication - -To set up credentials in Git so you can push and pull, you can sign in to GitHub by going to `Window` -> `GitHub` -> `Account` -> `Sign in`. You only have to sign in successfully once, your credentials will remain on the system for all Git operations in Unity and outside of it. If you've already signed in once but the Account dropdown still says `Sign in`, ignore it, it's a bug. - -![Authentication screenshot](https://user-images.githubusercontent.com/121322/27644895-8f22f904-5bd9-11e7-8a93-e6bfe0c24a74.png) - -### Publish a new repository - -1. Go to [github.com](https://github.com) and create a new empty repository - do not add a license, readme or other files during the creation process. -2. Copy the **https** URL shown in the creation page -3. In Unity, go to `Windows` -> `GitHub` -> `Settings` and paste the url into the `Remote` textbox. -3. Click `Save repository`. -4. Go to the `History` tab and click `Push`. - -### Commiting your work - Changes tab - -You can see which files have been changed and commit them through the Changes tab. `.meta` files will show up in relation to their files on the tree, so you can select a file for comitting and automatically have their `.meta` - -![Changes tab screenshot](https://user-images.githubusercontent.com/121322/27644933-ab00af72-5bd9-11e7-84c3-edec495f87f5.png) - -### Pushing/pulling your work - History tab - -The history tab includes a `Push` button to push your work to the server. Make sure you have a remote url configured in the `Settings` tab so that you can push and pull your work. - -To receive updates from the server by clicking on the `Pull` button. You cannot pull if you have local changes, so commit your changes before pulling. - -![History tab screenshot](https://user-images.githubusercontent.com/121322/27644965-c1109bba-5bd9-11e7-9257-4fa38f5c67d1.png) - -### Branches tab - -![Branches tab screenshot](https://user-images.githubusercontent.com/121322/27644978-cd3c5622-5bd9-11e7-9dcb-6ae5d5c7dc8a.png) +The [CONTRIBUTING.md](CONTRIBUTING.md) document will help you get setup and familiar with the source. The [documentation](docs/) folder also contains more resources relevant to the project. -### Settings tab +If you're looking for something to work on, check out the [up-for-grabs](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen+is%3Aissue+label%3Aup-for-grabs) label. -You can configure your user data in the Settings tab, along with the path to the Git installation. +## How to use -Locked files will appear in a list in the Settings tab. You can see who has locked a file and release file locks after you've pushed your work. +The [quick guide to GitHub for Unity](docs/using/quick-guide.md) -![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) +More [in-depth information](docs/readme.md) ## More Resources @@ -188,6 +41,6 @@ The MIT license grant is not for GitHub's trademarks, which include the logo designs. GitHub reserves all trademark and copyright rights in and to all GitHub trademarks. GitHub's logos include, for instance, the stylized Invertocat designs that include "logo" in the file title in the following -folder: [IconsAndLogos](https://github.com/github-for-unity/Unity/tree/master/src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos). +folder: [IconsAndLogos](src/UnityExtension/Assets/Editor/GitHub.Unity/IconsAndLogos). Copyright 2015 - 2018 GitHub, Inc. diff --git a/docs/contributing/how-to-build.md b/docs/contributing/how-to-build.md index db494a224..0a8aba41d 100644 --- a/docs/contributing/how-to-build.md +++ b/docs/contributing/how-to-build.md @@ -13,7 +13,7 @@ This repository is LFS-enabled. To clone it, you should use a git client that su ### MacOS -- Mono 4.x required. +- Mono 4.x required. You can install it via brew with `brew tap shana/mono && brew install mono@4.8` - Mono 5.x will not work - `UnityEngine.dll` and `UnityEditor.dll`. - If you've installed Unity in the default location of `/Applications/Unity`, the build will be able to reference these DLLs automatically. Otherwise, you'll need to copy these DLLs from `[Unity installation path]/Unity.app/Contents/Managed` into the `lib` directory in order for the build to work @@ -35,12 +35,25 @@ git submodule deinit script ### Important pre-build steps -To be able to authenticate in GitHub for Unity, you'll need to: +The build needs to reference `UnityEngine.dll` and `UnityEditor.dll`. These DLLs are included with Unity. If you've installed Unity in the default location, the build will be able to find them automatically. If not, copy these DLLs from `[your Unity installation path]\Unity\Editor\Data\Managed` into the `lib` directory in order for the build to work. + +#### Developer OAuth app + +Because GitHub for Unity uses OAuth web application flow to interact with the GitHub API and perform actions on behalf of a user, it needs to be bundled with a Client ID and Secret. + +For external contributors, we have bundled a developer OAuth application in the source so that you can complete the sign in flow locally without needing to configure your own application. + +These are listed in `src/GitHub.Api/Application/ApplicationInfo.cs` + +DO NOT TRUST THIS CLIENT ID AND SECRET! THIS IS ONLY FOR TESTING PURPOSES!! + +The limitation with this developer application is that this will not work with GitHub Enterprise. You will see sign-in will fail on the OAuth callback due to the credentials not being present there. + +To provide your own Client ID and Client Secret: - [Register a new developer application](https://github.com/settings/developers) in your profile. - Copy [common/ApplicationInfo_Local.cs-example](../../common/ApplicationInfo_Local.cs-example) to `common/ApplicationInfo_Local.cs` and fill out the clientId/clientSecret fields for your application. -The build needs to reference `UnityEngine.dll` and `UnityEditor.dll`. These DLLs are included with Unity. If you've installed Unity in the default location, the build will be able to find them automatically. If not, copy these DLLs from `[your Unity installation path]\Unity\Editor\Data\Managed` into the `lib` directory in order for the build to work. ### Visual Studio @@ -56,13 +69,12 @@ Once you've built the solution for the first time, you can open `src/UnityExtens The build also creates a Unity test project called `GitHubExtension` inside a directory called `github-unity-test` next to your local clone. For instance, if the repository is located at `c:\Projects\Unity` the test project will be at `c:\Projects\github-unity-test\GitHubExtension`. You can use this project to test binary builds of the extension in a clean environment (all needed DLLs will be copied to it every time you build). -Note: some files might be locked by Unity if have one of the build output projects open when you compile from VS or the command line. This is expected and shouldn't cause issues with your builds. +Note: some files might be locked by Unity if have one of the build output projects open when you compile from VS or the command line. This is expected and shouldn't cause issues with your builds. ## Solution organization The `GitHub.Unity.sln` solution includes several projects: -- dotnet-httpclient35 and octokit: external dependencies for threading and github api support, respectively. These are the submodules. - packaging: empty projects with build rules that copy DLLs to various locations for testing - Tests: unit and integration test projects - GitHub.Logging: A logging helper library diff --git a/docs/readme.md b/docs/readme.md index ae4be69a9..98251a291 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -30,6 +30,11 @@ Details about how the team is organizing and shipping GitHub for Unity: ## Using +[Quick Guide](using/quick-guide.md) + +These documents contain more details on how to use the GitHub for Unity plugin: +- **[Installing and Updating the GitHub for Unity package](using/how-to-install-and-update.md)** + These documents contain more details on how to use the GitHub for Unity plugin: -- **[Installing and Updating the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md)** -- **[Getting Started with the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md)** +- **[Installing and Updating the GitHub for Unity package](using/how-to-install-and-update.md)** +- **[Getting Started with the GitHub for Unity package](using/getting-started.md)** diff --git a/docs/using/quick-guide.md b/docs/using/quick-guide.md new file mode 100644 index 000000000..5f95500a1 --- /dev/null +++ b/docs/using/quick-guide.md @@ -0,0 +1,153 @@ +# Quick Guide + +## More resources + +These documents contain more details on how to use the GitHub for Unity plugin: +- **[Installing and Updating the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md)** +- **[Getting Started with the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md)** + +## Table of Contents + +[Installing GitHub for Unity](#installing-github-for-unity) + +- [Requirements](#requirements) + - [Git on macOS](#git-on-macos) + - [Git on Windows](#git-on-windows) +- [Installation](#installation) +- [Log files](#log-files) + - [Windows](#windows) + - [macOS](#macos) + +[Quick Guide to GitHub for Unity](#quick-guide-to-github-for-unity) + +- [Opening the GitHub window](#opening-the-github-window) +- [Initialize Repository](#initialize-repository) +- [Authentication](#authentication) +- [Publish a new repository](#publish-a-new-repository) +- [Commiting your work - Changes tab](#commiting-your-work---changes-tab) +- [Pushing/pulling your work - History tab](#pushingpulling-your-work---history-tab) +- [Branches tab](#branches-tab) +- [Settings tab](#settings-tab) + +## Installing GitHub for Unity + +### Requirements + +- Unity 5.4 or higher + - There's currently an blocker issue opened for 5.3 support, so we know it doesn't run there. Personal edition is fine. +- Git and Git LFS 2.x + +#### Git on macOS + +The current release has limited macOS support. macOS users will need to install the latest [Git](https://git-scm.com/downloads) and [Git LFS](https://git-lfs.github.com/) manually, and make sure these are on the path. You can configure the Git location in the Settings tab on the GitHub window. + +The easiest way of installing git and git lfs is to install [Homebrew](https://brew.sh/) and then do `brew install git git-lfs`. + +Make sure a Git user and email address are set in the `~/.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `~/.gitconfig` file and adding the following section, if it doesn't exist yet: + +``` +[user] + name = Your Name + email = Your Email +``` + +#### Git on Windows + +The GitHub for Unity extension ships with a bundle of Git and Git LFS, to ensure that you have the correct version. These will be installed into `%LOCALAPPDATA%\GitHubUnity` when the extension runs for the first time. + +Make sure a Git user and email address are set in the `%HOME%\.gitconfig` file before you initialize a repository for the first time. You can set these values by opening your `%HOME%\.gitconfig` file and adding the following section, if it doesn't exist yet: + +``` +[user] + name = Your Name + email = Your Email +``` + +Once the extension is installed, you can open a command line with the same Git and Git LFS version that the extension uses by going to `Window` -> `GitHub Command Line` in Unity. + +### Installation + +This extensions needs to be installed (and updated) for each Unity project that you want to version control. +First step is to download the latest package from [the releases page](https://github.com/github-for-unity/Unity/releases); +it will be saved as a file with the extension `.unitypackage`. +To install it, open Unity, then open the project you want to version control, and then double click on the downloaded package. +Alternatively, import the package by clicking Assets, Import Package, Custom Package, then select the downloaded package. + +#### Log files + +##### macOS + +The extension log file can be found at `~/Library/Logs/GitHubUnity/github-unity.log` + +##### Windows + +The extension log file can be found at `%LOCALAPPDATA%\GitHubUnity\github-unity.log` + +## I have a problem with GitHub for Unity + +First, please search the [open issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aopen) +and [closed issues](https://github.com/github-for-unity/Unity/issues?q=is%3Aclosed) +to see if your issue hasn't already been reported (it may also be fixed). + +If you can't find an issue that matches what you're seeing, open a [new issue](https://github.com/github-for-unity/Unity/issues/new) +and fill out the template to provide us with enough information to investigate +further. + +## Quick Guide to GitHub for Unity + +### Opening the GitHub window + +You can access the GitHub window by going to Windows -> GitHub. The window opens by default next to the Inspector window. + +### Initialize Repository + +![Initialize repository screenshot](https://user-images.githubusercontent.com/10103121/37807041-bb4446a6-2e19-11e8-9fff-a431309b8515.png) + +If the current Unity project is not in a Git repository, the GitHub for Unity extension will offer to initialize the repository for you. This will: + +- Initialize a git repository at the Unity project root via `git init` +- Initialize git-lfs via `git lfs install` +- Set up a `.gitignore` file at the Unity project root. +- Set up a `.gitattributes` file at the Unity project root with a large list of known binary filetypes (images, audio, etc) that should be tracked by LFS +- Configure the project to serialize meta files as text +- Create an initial commit with the `.gitignore` and `.gitattributes` file. + +### Authentication + +To set up credentials in Git so you can push and pull, you can sign in to GitHub by going to `Window` -> `GitHub` -> `Account` -> `Sign in`. You only have to sign in successfully once, your credentials will remain on the system for all Git operations in Unity and outside of it. If you've already signed in once but the Account dropdown still says `Sign in`, ignore it, it's a bug. + +![Authentication screenshot](https://user-images.githubusercontent.com/121322/27644895-8f22f904-5bd9-11e7-8a93-e6bfe0c24a74.png) + +### Publish a new repository + +1. Go to [github.com](https://github.com) and create a new empty repository - do not add a license, readme or other files during the creation process. +2. Copy the **https** URL shown in the creation page +3. In Unity, go to `Windows` -> `GitHub` -> `Settings` and paste the url into the `Remote` textbox. +4. Click `Save repository`. +5. Go to the `History` tab and click `Push`. + +### Commiting your work - Changes tab + +You can see which files have been changed and commit them through the Changes tab. `.meta` files will show up in relation to their files on the tree, so you can select a file for comitting and automatically have their `.meta` + +![Changes tab screenshot](https://user-images.githubusercontent.com/121322/27644933-ab00af72-5bd9-11e7-84c3-edec495f87f5.png) + +### Pushing/pulling your work - History tab + +The history tab includes a `Push` button to push your work to the server. Make sure you have a remote url configured in the `Settings` tab so that you can push and pull your work. + +To receive updates from the server by clicking on the `Pull` button. You cannot pull if you have local changes, so commit your changes before pulling. + +![History tab screenshot](https://user-images.githubusercontent.com/121322/27644965-c1109bba-5bd9-11e7-9257-4fa38f5c67d1.png) + +### Branches tab + +![Branches tab screenshot](https://user-images.githubusercontent.com/121322/27644978-cd3c5622-5bd9-11e7-9dcb-6ae5d5c7dc8a.png) + +### Settings tab + +You can configure your user data in the Settings tab, along with the path to the Git installation. + +Locked files will appear in a list in the Settings tab. You can see who has locked a file and release file locks after you've pushed your work. + +![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) From 631bf30eb16cd25febe2e9b474b94be5999e000b Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 27 Jul 2018 14:14:51 -0400 Subject: [PATCH 366/567] Update readme.md --- docs/readme.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/readme.md b/docs/readme.md index 98251a291..d73e8e840 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -32,9 +32,6 @@ Details about how the team is organizing and shipping GitHub for Unity: [Quick Guide](using/quick-guide.md) -These documents contain more details on how to use the GitHub for Unity plugin: -- **[Installing and Updating the GitHub for Unity package](using/how-to-install-and-update.md)** - These documents contain more details on how to use the GitHub for Unity plugin: - **[Installing and Updating the GitHub for Unity package](using/how-to-install-and-update.md)** - **[Getting Started with the GitHub for Unity package](using/getting-started.md)** From 84405f28e6e60702f04bc6ec1bbe51491b1e202e Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 18 Jul 2018 17:37:06 +0200 Subject: [PATCH 367/567] Package octorun and add script to automate creating these zips --- .gitignore | 1 - .gitmodules | 3 +++ create-octorun-zip.sh | 3 +++ src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- submodules/packaging | 1 + 6 files changed, 10 insertions(+), 4 deletions(-) create mode 100755 create-octorun-zip.sh create mode 160000 submodules/packaging diff --git a/.gitignore b/.gitignore index 67d4e53bc..7fc38bf13 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ _NCrunch_GitHub.Unity .DS_Store build/ TestResult.xml -submodules/ *.stackdump *.lastcodeanalysissucceeded \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index d9e14a3e9..8ecda521f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "script"] path = script url = git@github.com:github-for-unity/UnityBuildScripts +[submodule "submodules/packaging"] + path = submodules/packaging + url = https://github.com/github-for-unity/packaging diff --git a/create-octorun-zip.sh b/create-octorun-zip.sh new file mode 100755 index 000000000..e722d7cb2 --- /dev/null +++ b/create-octorun-zip.sh @@ -0,0 +1,3 @@ +#!/bin/sh -eu +DIR=$(pwd) +submodules/packaging/octorun/run.sh --path $DIR/octorun --out $DIR/src/GitHub.Api/Resources diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index d7c63b3f3..038892ca7 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60478c33970a7a8d952777729450a42fa02bf7832514b7d44ec116aa09cb7814 -size 219592 +oid sha256:2b860fd59e28e8e7431f2c6737483b56a6931ad0092f521733acb4b1d25e68fb +size 1365568 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 6f12f7867..3618fc6cb 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -3d4bea9ae4ca3c4497d6b349166a1e34 \ No newline at end of file +24b5c15073de99adedf4bdb16b2b139f \ No newline at end of file diff --git a/submodules/packaging b/submodules/packaging new file mode 160000 index 000000000..369486ccc --- /dev/null +++ b/submodules/packaging @@ -0,0 +1 @@ +Subproject commit 369486cccfc13b5749f611964fa554f1c1675664 From f497f7aa3d18242bb4a7ae1426184de964d267e3 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 30 Jul 2018 16:43:09 +0200 Subject: [PATCH 368/567] Fix authentication, again Fix signin/signout not available if there's no repo. Fix signout of the active connection. Fix mismatches between what's in the connections file to what we're trying to signout or load the keychain for. --- octorun/src/api.js | 4 +- octorun/src/authentication.js | 1 - octorun/src/bin/app-login.js | 1 - octorun/src/bin/app-organizations.js | 1 - octorun/src/bin/app-publish.js | 1 - octorun/src/bin/app-usage.js | 8 ++- octorun/src/bin/app-validate.js | 1 - octorun/src/configuration.js | 2 - script | 2 +- src/GitHub.Api/Application/ApiClient.cs | 43 +++++++-------- .../Application/ApplicationManagerBase.cs | 5 +- src/GitHub.Api/Authentication/IKeychain.cs | 2 +- src/GitHub.Api/Authentication/Keychain.cs | 25 ++++++--- src/GitHub.Api/Authentication/LoginManager.cs | 28 +++------- src/GitHub.Api/Git/GitCredentialManager.cs | 2 +- src/GitHub.Api/Primitives/UriString.cs | 3 +- src/GitHub.Api/Tasks/OctorunTask.cs | 45 +++++++--------- .../Services/AuthenticationService.cs | 7 ++- .../GitHub.Unity/UI/AuthenticationView.cs | 14 +---- .../Editor/GitHub.Unity/UI/PopupWindow.cs | 13 +---- .../Editor/GitHub.Unity/UI/PublishView.cs | 24 +++------ .../Assets/Editor/GitHub.Unity/UI/Window.cs | 54 +++++++++++++------ .../IntegrationTests/BaseIntegrationTest.cs | 13 +++-- .../IntegrationTestEnvironment.cs | 2 +- .../IntegrationTests/Metrics/MetricsTests.cs | 2 +- 25 files changed, 137 insertions(+), 166 deletions(-) diff --git a/octorun/src/api.js b/octorun/src/api.js index 1c8c37e42..5b1bc833e 100644 --- a/octorun/src/api.js +++ b/octorun/src/api.js @@ -6,8 +6,8 @@ function ApiWrapper() { throw "appName missing"; } - if (!config.user || !config.token) { - throw "user and/or token missing"; + if (!config.token) { + throw "token missing"; } this.octokit = octokitWrapper.createOctokit(config.appName); diff --git a/octorun/src/authentication.js b/octorun/src/authentication.js index adfe688cd..e2d70d952 100644 --- a/octorun/src/authentication.js +++ b/octorun/src/authentication.js @@ -1,4 +1,3 @@ -var endOfLine = require('os').EOL; var config = require("./configuration"); var octokitWrapper = require("./octokit"); diff --git a/octorun/src/bin/app-login.js b/octorun/src/bin/app-login.js index f3484c31b..6c2ab582a 100644 --- a/octorun/src/bin/app-login.js +++ b/octorun/src/bin/app-login.js @@ -1,7 +1,6 @@ var commander = require("commander"); var package = require('../../package.json'); var authentication = require('../authentication'); -var endOfLine = require('os').EOL; var output = require('../output'); commander diff --git a/octorun/src/bin/app-organizations.js b/octorun/src/bin/app-organizations.js index 480289aa1..c9d70a760 100644 --- a/octorun/src/bin/app-organizations.js +++ b/octorun/src/bin/app-organizations.js @@ -1,7 +1,6 @@ var commander = require("commander"); var package = require('../../package.json'); var ApiWrapper = require('../api'); -var endOfLine = require('os').EOL; var output = require('../output'); commander diff --git a/octorun/src/bin/app-publish.js b/octorun/src/bin/app-publish.js index 5fe602390..61d132326 100644 --- a/octorun/src/bin/app-publish.js +++ b/octorun/src/bin/app-publish.js @@ -1,7 +1,6 @@ var commander = require("commander"); var package = require('../../package.json') var ApiWrapper = require('../api') -var endOfLine = require('os').EOL; var output = require('../output'); commander diff --git a/octorun/src/bin/app-usage.js b/octorun/src/bin/app-usage.js index 9f6811d15..e8caf544f 100644 --- a/octorun/src/bin/app-usage.js +++ b/octorun/src/bin/app-usage.js @@ -1,9 +1,7 @@ -var commander = require("commander"); -var package = require('../../package.json') -var config = require("../configuration"); -var endOfLine = require('os').EOL; +var commander = require('commander'); +var package = require('../../package.json'); +var config = require('../configuration'); var fs = require('fs'); -var util = require('util'); var output = require('../output'); commander diff --git a/octorun/src/bin/app-validate.js b/octorun/src/bin/app-validate.js index 8ba643021..7fd53bbb9 100644 --- a/octorun/src/bin/app-validate.js +++ b/octorun/src/bin/app-validate.js @@ -1,6 +1,5 @@ var commander = require("commander"); var package = require('../../package.json'); -var endOfLine = require('os').EOL; var ApiWrapper = require('../api'); var output = require('../output'); diff --git a/octorun/src/configuration.js b/octorun/src/configuration.js index f9462acde..0fe3906fd 100644 --- a/octorun/src/configuration.js +++ b/octorun/src/configuration.js @@ -3,13 +3,11 @@ require("dotenv").config({silent: true}); var clientId = process.env.OCTOKIT_CLIENT_ID; var clientSecret = process.env.OCTOKIT_CLIENT_SECRET; var appName = process.env.OCTOKIT_USER_AGENT; -var user = process.env.OCTORUN_USER; var token = process.env.OCTORUN_TOKEN; module.exports = { clientId: clientId, clientSecret: clientSecret, appName: appName, - user: user, token: token }; \ No newline at end of file diff --git a/script b/script index 259dba7e8..0c618caaa 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 259dba7e8375a96a935b51e2169fe029cd70c039 +Subproject commit 0c618caaab7f163921aea7b60484b423e13cb818 diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 73055228b..58156dbb7 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -16,23 +16,25 @@ class ApiClient : IApiClient private readonly IKeychain keychain; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; - private readonly NPath nodeJsExecutablePath; - private readonly NPath octorunScriptPath; private readonly ILoginManager loginManager; + private readonly IEnvironment environment; - public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, NPath nodeJsExecutablePath, NPath octorunScriptPath) + public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment) { - Guard.ArgumentNotNull(hostUrl, nameof(hostUrl)); Guard.ArgumentNotNull(keychain, nameof(keychain)); - HostAddress = HostAddress.Create(hostUrl); - OriginalUrl = hostUrl; + var host = String.IsNullOrEmpty(hostUrl) + ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) + : new UriString(hostUrl.ToRepositoryUri() + .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + + HostAddress = HostAddress.Create(host); + OriginalUrl = host; this.keychain = keychain; this.processManager = processManager; this.taskManager = taskManager; - this.nodeJsExecutablePath = nodeJsExecutablePath; - this.octorunScriptPath = octorunScriptPath; - loginManager = new LoginManager(keychain, processManager, taskManager, nodeJsExecutablePath, octorunScriptPath); + this.environment = environment; + loginManager = new LoginManager(keychain, processManager, taskManager, environment); } public ITask Logout(UriString host) @@ -40,14 +42,15 @@ public ITask Logout(UriString host) return loginManager.Logout(host); } - public void CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null) + public void CreateRepository(string name, string description, bool isPrivate, + Action callback, string organization = null) { Guard.ArgumentNotNull(callback, "callback"); new FuncTask(taskManager.Token, () => { - var user = GetCurrentUser(); - var keychainAdapter = keychain.Connect(OriginalUrl); + // this validates the user, again + GetCurrentUser(); var command = new StringBuilder("publish -r \""); command.Append(name); @@ -72,8 +75,7 @@ public void CreateRepository(string name, string description, bool isPrivate, Ac command.Append(" -p"); } - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, command.ToString(), - user: user.Login, userToken: keychainAdapter.Credential.Token) + var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, command.ToString()) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -106,11 +108,8 @@ public void GetOrganizations(Action onSuccess, Action Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); new FuncTask(taskManager.Token, () => { - var user = GetCurrentUser(); - var keychainAdapter = keychain.Connect(OriginalUrl); - - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, "organizations", - user: user.Login, userToken: keychainAdapter.Credential.Token) + var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, + "organizations") .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -208,8 +207,7 @@ public void ContinueLogin(LoginResult loginResult, string code) private GitHubUser GetCurrentUser() { - //TODO: ONE_USER_LOGIN This assumes we only support one login - var keychainConnection = keychain.Connections.FirstOrDefault(); + var keychainConnection = keychain.Connections.FirstOrDefault(x => x.Host == OriginalUrl); if (keychainConnection == null) throw new KeychainEmptyException(); @@ -249,8 +247,7 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha { try { - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath, octorunScriptPath, "validate", - user: keychainConnection.Username, userToken: keychainAdapter.Credential.Token) + var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, "validate") .Configure(processManager); var ret = octorunTask.RunSynchronously(); diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index e009573d2..297c1c24d 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -53,9 +53,8 @@ protected void Initialize() #if ENABLE_METRICS var metricsService = new MetricsService(ProcessManager, TaskManager, - Environment.FileSystem, - Environment.NodeJsExecutablePath, - Environment.OctorunScriptPath); + Platform.Keychain, + Environment); UsageTracker.MetricsService = metricsService; #endif } diff --git a/src/GitHub.Api/Authentication/IKeychain.cs b/src/GitHub.Api/Authentication/IKeychain.cs index 92d5dc524..6eca4c998 100644 --- a/src/GitHub.Api/Authentication/IKeychain.cs +++ b/src/GitHub.Api/Authentication/IKeychain.cs @@ -6,7 +6,7 @@ namespace GitHub.Unity public interface IKeychain { IKeychainAdapter Connect(UriString host); - IKeychainAdapter Load(UriString host); + IKeychainAdapter Load(UriString host, bool dontClear = false); void Clear(UriString host, bool deleteFromCredentialManager); void Save(UriString host); void SetCredentials(ICredential credential); diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index 992cc26b3..07c59f94f 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -99,22 +99,30 @@ public IKeychainAdapter Connect(UriString host) return FindOrCreateAdapter(host); } - public IKeychainAdapter Load(UriString host) + public IKeychainAdapter Load(UriString host, bool dontClear = false) { Guard.ArgumentNotNull(host, nameof(host)); - var keychainAdapter = FindOrCreateAdapter(host); - var connection = GetConnection(host); - + var keychainAdapter = Connect(host) as KeychainAdapter; var keychainItem = credentialManager.Load(host); if (keychainItem == null) { - logger.Warning("Cannot load host from Credential Manager; removing from cache"); - Clear(host, false); + if (!dontClear) + { + logger.Warning("Cannot load host from Credential Manager; removing from cache"); + Clear(host, false); + } keychainAdapter = null; } else { + var connection = GetConnection(host); + if (connection.Username == null) + { + connection.Username = keychainItem.Username; + SaveConnectionsToDisk(); + } + if (keychainItem.Username != connection.Username) { logger.Warning("Keychain Username:\"{0}\" does not match cached Username:\"{1}\"; Hopefully it works", keychainItem.Username, connection.Username); @@ -262,11 +270,11 @@ private void RemoveCredential(UriString host, bool deleteFromCredentialManager) private Connection GetConnection(UriString host) { if (!connections.ContainsKey(host)) - throw new ArgumentException($"{host} is not found", nameof(host)); + return AddConnection(new Connection(host, null)); return connections[host]; } - private void AddConnection(Connection connection) + private Connection AddConnection(Connection connection) { // create new connection in the connection cache for this host if (connections.ContainsKey(connection.Host)) @@ -274,6 +282,7 @@ private void AddConnection(Connection connection) else connections.Add(connection.Host, connection); SaveConnectionsToDisk(); + return connection; } private void RemoveConnection(UriString host) diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 44a337a54..602230c6d 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -22,8 +22,7 @@ class LoginManager : ILoginManager private readonly IKeychain keychain; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; - private readonly NPath? nodeJsExecutablePath; - private readonly NPath? octorunScript; + private readonly IEnvironment environment; /// /// Initializes a new instance of the class. @@ -35,15 +34,14 @@ class LoginManager : ILoginManager /// public LoginManager( IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, - NPath? nodeJsExecutablePath = null, NPath? octorunScript = null) + IEnvironment environment) { Guard.ArgumentNotNull(keychain, nameof(keychain)); this.keychain = keychain; this.processManager = processManager; this.taskManager = taskManager; - this.nodeJsExecutablePath = nodeJsExecutablePath; - this.octorunScript = octorunScript; + this.environment = environment; } /// @@ -146,22 +144,12 @@ private LoginResultData TryLogin( string code = null ) { - if (!nodeJsExecutablePath.HasValue) - { - throw new InvalidOperationException("nodeJsExecutablePath must be set"); - } - - if (!octorunScript.HasValue) - { - throw new InvalidOperationException("octorunScript must be set"); - } - var hasTwoFactorCode = code != null; var arguments = hasTwoFactorCode ? "login --twoFactor" : "login"; - var loginTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, - arguments, ApplicationInfo.ClientId, ApplicationInfo.ClientSecret); - loginTask.Configure(processManager, workingDirectory: octorunScript.Value.Parent.Parent, withInput: true); + var loginTask = new OctorunTask(taskManager.Token, keychain, environment, + arguments); + loginTask.Configure(processManager, withInput: true); loginTask.OnStartProcess += proc => { proc.StandardInput.WriteLine(username); @@ -198,8 +186,8 @@ private string RetrieveUsername(LoginResultData loginResultData, string username return username; } - var octorunTask = new OctorunTask(taskManager.Token, nodeJsExecutablePath.Value, octorunScript.Value, "validate", - user: username, userToken: loginResultData.Token).Configure(processManager); + var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, "validate") + .Configure(processManager); var validateResult = octorunTask.RunSynchronously(); if (!validateResult.IsSuccess) diff --git a/src/GitHub.Api/Git/GitCredentialManager.cs b/src/GitHub.Api/Git/GitCredentialManager.cs index 25b946d1b..2dcb61eb7 100644 --- a/src/GitHub.Api/Git/GitCredentialManager.cs +++ b/src/GitHub.Api/Git/GitCredentialManager.cs @@ -60,7 +60,7 @@ public ICredential Load(UriString host) if (String.IsNullOrEmpty(kvpCreds)) { - Logger.Error("No credentials are stored"); + // we didn't find credentials, stop here return null; } diff --git a/src/GitHub.Api/Primitives/UriString.cs b/src/GitHub.Api/Primitives/UriString.cs index a60decfb7..17c725ca3 100644 --- a/src/GitHub.Api/Primitives/UriString.cs +++ b/src/GitHub.Api/Primitives/UriString.cs @@ -28,7 +28,8 @@ public class UriString : StringEquivalent, IEquatable public UriString(string uriString) : base(NormalizePath(uriString)) { if (uriString == null || uriString.Length == 0) return; - if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) + if (Uri.TryCreate(uriString, UriKind.Absolute, out url) + || Uri.TryCreate("https://" + uriString, UriKind.Absolute, out url)) { if (!url.IsFile) SetUri(url); diff --git a/src/GitHub.Api/Tasks/OctorunTask.cs b/src/GitHub.Api/Tasks/OctorunTask.cs index f64fcc0cc..9d32ebd4e 100644 --- a/src/GitHub.Api/Tasks/OctorunTask.cs +++ b/src/GitHub.Api/Tasks/OctorunTask.cs @@ -54,28 +54,32 @@ class OctorunTask : ProcessTask { private readonly string clientId; private readonly string clientSecret; - private readonly string user; private readonly string userToken; private readonly NPath pathToNodeJs; private readonly NPath pathToOctorunJs; private readonly string arguments; - public OctorunTask(CancellationToken token, NPath pathToNodeJs, NPath pathToOctorunJs, string arguments, - string clientId = null, - string clientSecret = null, - string user = null, - string userToken = null, + public OctorunTask(CancellationToken token, IKeychain keychain, IEnvironment environment, + string arguments, IOutputProcessor processor = null) : base(token, processor ?? new OctorunResultOutputProcessor()) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.user = user; - this.userToken = userToken; - this.pathToNodeJs = pathToNodeJs; - this.pathToOctorunJs = pathToOctorunJs; + this.clientId = ApplicationInfo.ClientId; + this.clientSecret = ApplicationInfo.ClientSecret; + this.pathToNodeJs = environment.NodeJsExecutablePath; + this.pathToOctorunJs = environment.OctorunScriptPath; this.arguments = $"\"{pathToOctorunJs}\" {arguments}"; + + var cloneUrl = environment.Repository?.CloneUrl; + var host = String.IsNullOrEmpty(cloneUrl) + ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) + : new UriString(cloneUrl.ToRepositoryUri() + .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + + var adapter = keychain.Load(host, true); + if (adapter != null) + userToken = adapter.Credential.Token; } public override void Configure(ProcessStartInfo psi) @@ -85,21 +89,8 @@ public override void Configure(ProcessStartInfo psi) psi.WorkingDirectory = pathToOctorunJs.Parent.Parent.Parent; psi.EnvironmentVariables.Add("OCTOKIT_USER_AGENT", $"{ApplicationInfo.ApplicationSafeName}/{ApplicationInfo.Version}"); - - if (clientId != null) - { - psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_ID", clientId); - } - - if (clientSecret != null) - { - psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_SECRET", clientSecret); - } - - if (user != null) - { - psi.EnvironmentVariables.Add("OCTORUN_USER", user); - } + psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_ID", clientId); + psi.EnvironmentVariables.Add("OCTOKIT_CLIENT_SECRET", clientSecret); if (userToken != null) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index bd215045e..5c6afae20 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -8,9 +8,12 @@ class AuthenticationService private LoginResult loginResultData; - public AuthenticationService(IProcessManager processManager, ITaskManager taskManager, UriString host, IKeychain keychain, NPath nodeJsExecutablePath, NPath octorunExecutablePath) + public AuthenticationService(UriString host, IKeychain keychain, + IProcessManager processManager, ITaskManager taskManager, + IEnvironment environment + ) { - client = new ApiClient(host, keychain, processManager, taskManager, nodeJsExecutablePath, octorunExecutablePath); + client = new ApiClient(host, keychain, processManager, taskManager, environment); } public void Login(string username, string password, Action twofaRequired, Action authResult) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index b9430852c..6082238cb 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -243,18 +243,8 @@ private AuthenticationService AuthenticationService { if (authenticationService == null) { - UriString host; - if (Repository != null && Repository.CloneUrl != null && Repository.CloneUrl.IsValidUri) - { - host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - AuthenticationService = new AuthenticationService(Manager.ProcessManager, Manager.TaskManager, host, Platform.Keychain, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); + UriString host = Repository != null ? Repository.CloneUrl : null; + AuthenticationService = new AuthenticationService(host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); } return authenticationService; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs index ea7cd5e6d..d7da7cfbe 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs @@ -175,17 +175,8 @@ public IApiClient Client if (client == null) { var repository = Environment.Repository; - UriString host; - if (repository != null && !string.IsNullOrEmpty(repository.CloneUrl)) - { - host = repository.CloneUrl.ToRepositoryUrl(); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - client = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); + UriString host = repository != null ? repository.CloneUrl : null; + client = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment); } return client; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index 65ef5fb71..0671bcf06 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -35,24 +35,15 @@ class PublishView : Subview [NonSerialized] private string error; [NonSerialized] private bool ownersNeedLoading; + private Connection Connection { get { return Platform.Keychain.Connections.First(); } } + public IApiClient Client { get { if (client == null) { - var repository = Environment.Repository; - UriString host; - if (repository != null && !string.IsNullOrEmpty(repository.CloneUrl)) - { - host = repository.CloneUrl.ToRepositoryUrl(); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - client = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); + client = new ApiClient(Connection.Host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment); } return client; @@ -89,13 +80,10 @@ public override void InitializeView(IView parent) private void LoadOwners() { - var keychainConnections = Platform.Keychain.Connections; - //TODO: ONE_USER_LOGIN This assumes only ever one user can login - isBusy = true; //TODO: ONE_USER_LOGIN This assumes only ever one user can login - username = keychainConnections.First().Username; + username = Connection.Username; Client.GetOrganizations(orgs => { @@ -129,7 +117,7 @@ public override void OnGUI() { GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); { - GUILayout.Label(PublishToGithubLabel, EditorStyles.boldLabel); + GUILayout.Label(PublishToGithubLabel, EditorStyles.boldLabel); } GUILayout.EndHorizontal(); @@ -202,7 +190,7 @@ private string GetPublishErrorMessage(Exception ex) { return PublishLimitPrivateRepositoriesError; } - + return ex.Message; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 896160fe9..90557f47f 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -213,7 +213,22 @@ private void ValidateCachedData(IRepository repository) private void MaybeUpdateData() { - connection = Platform.Keychain.Connections.FirstOrDefault(); + UriString host = null; + if (!HasRepository || String.IsNullOrEmpty(Repository.CloneUrl)) + { + var firstConnection = Platform.Keychain.Connections.FirstOrDefault(); + if (firstConnection != null) + host = firstConnection.Host; + else + host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); + } + else + { + host = new UriString(Repository.CloneUrl.ToRepositoryUri() + .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + } + + connection = Platform.Keychain.Connections.FirstOrDefault(x => x.Host.ToUriString() == host); if (repositoryProgressHasUpdate) { @@ -568,6 +583,24 @@ private void DoToolbarGUI() } GUILayout.FlexibleSpace(); + + if (!HasRepository) + { + GUILayout.FlexibleSpace(); + + if (connection == null) + { + if (GUILayout.Button("Sign in", EditorStyles.toolbarButton)) + SignIn(null); + } + else + { + if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) + { + DoAccountDropdown(); + } + } + } } EditorGUILayout.EndHorizontal(); } @@ -826,27 +859,14 @@ private void SignIn(object obj) private void GoToProfile(object obj) { - //TODO: ONE_USER_LOGIN This assumes only ever one user can login - var keychainConnection = Platform.Keychain.Connections.First(); - var uriString = new UriString(keychainConnection.Host).Combine(keychainConnection.Username); + var uriString = new UriString(connection.Host).Combine(connection.Username); Application.OpenURL(uriString); } private void SignOut(object obj) { - UriString host; - if (Repository != null && Repository.CloneUrl != null && Repository.CloneUrl.IsValidUri) - { - host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); - } - else - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - - var apiClient = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment.NodeJsExecutablePath, Environment.OctorunScriptPath); - apiClient.Logout(host).FinallyInUI((s, e) => Redraw()); + var loginManager = new LoginManager(Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + loginManager.Logout(connection.Host).FinallyInUI((s, e) => Redraw()); } public new void ShowNotification(GUIContent content) diff --git a/src/tests/IntegrationTests/BaseIntegrationTest.cs b/src/tests/IntegrationTests/BaseIntegrationTest.cs index 3041951e2..37b9a6c41 100644 --- a/src/tests/IntegrationTests/BaseIntegrationTest.cs +++ b/src/tests/IntegrationTests/BaseIntegrationTest.cs @@ -62,11 +62,14 @@ protected void InitializeEnvironment(NPath repoPath, cacheContainer.SetCacheInitializer(CacheType.GitUser, () => GitUserCache.Instance); cacheContainer.SetCacheInitializer(CacheType.RepositoryInfo, () => RepositoryInfoCache.Instance); - Environment = new IntegrationTestEnvironment(cacheContainer, - repoPath, - SolutionDirectory, - enableTrace: enableEnvironmentTrace, - initializeRepository: initializeRepository); + var environment = new IntegrationTestEnvironment(cacheContainer, + repoPath, + SolutionDirectory, + enableTrace: enableEnvironmentTrace, + initializeRepository: initializeRepository); + environment.NodeJsExecutablePath = TestApp; + environment.OctorunScriptPath = TestApp; + Environment = environment; } protected void InitializePlatform(NPath repoPath, diff --git a/src/tests/IntegrationTests/IntegrationTestEnvironment.cs b/src/tests/IntegrationTests/IntegrationTestEnvironment.cs index f85a7b227..004864ab7 100644 --- a/src/tests/IntegrationTests/IntegrationTestEnvironment.cs +++ b/src/tests/IntegrationTests/IntegrationTestEnvironment.cs @@ -100,7 +100,7 @@ public string GetSpecialFolder(Environment.SpecialFolder folder) public NPath GitLfsExecutablePath => defaultEnvironment.GitLfsExecutablePath; public GitInstaller.GitInstallationState GitInstallationState { get { return defaultEnvironment.GitInstallationState; } set { defaultEnvironment.GitInstallationState = value; } } - public NPath NodeJsExecutablePath => defaultEnvironment.NodeJsExecutablePath; + public NPath NodeJsExecutablePath { get; set; } public NPath OctorunScriptPath { get; set; } diff --git a/src/tests/IntegrationTests/Metrics/MetricsTests.cs b/src/tests/IntegrationTests/Metrics/MetricsTests.cs index 3d7fa1bdb..34aaa42ad 100644 --- a/src/tests/IntegrationTests/Metrics/MetricsTests.cs +++ b/src/tests/IntegrationTests/Metrics/MetricsTests.cs @@ -110,7 +110,7 @@ public void SubmissionWorks() storePath.WriteAllText(savedStore.ToJson(lowerCase: true)); settings.Get(Arg.Is(Constants.MetricsKey), Arg.Any()).Returns(true); - var metricsService = new MetricsService(ProcessManager, TaskManager, Environment.FileSystem, TestApp, TestApp); + var metricsService = new MetricsService(ProcessManager, TaskManager, Platform.Keychain, Environment); usageTracker.MetricsService = metricsService; var method = usageTracker.GetType().GetMethod("SendUsage", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); method.Invoke(usageTracker, null); From c3ae9a02f35b33c84e42497ff7ef97113c905ace Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 30 Jul 2018 16:44:10 +0200 Subject: [PATCH 369/567] Update octorun zips and version --- octorun/version | 2 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/octorun/version b/octorun/version index 48417770b..820d50296 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -7f160da1 \ No newline at end of file +f497f7aa3d \ No newline at end of file diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 1a319c6c4..9c5364eb1 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -85,7 +85,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "7f160da1"; + public const string PackageVersion = "f497f7aa3d"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 038892ca7..791c35d52 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b860fd59e28e8e7431f2c6737483b56a6931ad0092f521733acb4b1d25e68fb -size 1365568 +oid sha256:c7287864b7d86835175494c3743832f47a10481ab7fb1c257f871b808744e448 +size 1399629 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 3618fc6cb..7e86214a0 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -24b5c15073de99adedf4bdb16b2b139f \ No newline at end of file +a41ad2fd5ceaacb20574a0fc2841e82d \ No newline at end of file From 43828e6ff8d95be387376ba68cec2d63b232d37e Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 30 Jul 2018 16:45:04 +0200 Subject: [PATCH 370/567] Clean up the login api a bit and fix some issues - Make sure we save the token before verifying the token, and save the new username after verifying it - Minimize how many times we query the cred manager --- src/GitHub.Api/Application/ApiClient.cs | 2 +- src/GitHub.Api/Authentication/Credential.cs | 2 +- .../Authentication/ICredentialManager.cs | 2 +- src/GitHub.Api/Authentication/IKeychain.cs | 6 +-- src/GitHub.Api/Authentication/Keychain.cs | 43 +++++-------------- .../Authentication/KeychainAdapter.cs | 7 ++- src/GitHub.Api/Authentication/LoginManager.cs | 23 +++++----- src/GitHub.Api/Tasks/OctorunTask.cs | 13 +++++- .../UnitTests/Authentication/KeychainTests.cs | 12 +++--- 9 files changed, 49 insertions(+), 61 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 58156dbb7..065c86c85 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -225,7 +225,7 @@ private GitHubUser GetCurrentUser() private IKeychainAdapter GetValidatedKeychainAdapter(Connection keychainConnection) { - var keychainAdapter = keychain.Load(keychainConnection.Host); + var keychainAdapter = keychain.LoadFromSystem(keychainConnection.Host); if (keychainAdapter == null) throw new KeychainEmptyException(); diff --git a/src/GitHub.Api/Authentication/Credential.cs b/src/GitHub.Api/Authentication/Credential.cs index 2e31f9838..86e76c458 100644 --- a/src/GitHub.Api/Authentication/Credential.cs +++ b/src/GitHub.Api/Authentication/Credential.cs @@ -16,7 +16,7 @@ public Credential(UriString host, string username, string token) this.Token = token; } - public void UpdateToken(string token, string username) + public void Update(string token, string username) { this.Token = token; this.Username = username; diff --git a/src/GitHub.Api/Authentication/ICredentialManager.cs b/src/GitHub.Api/Authentication/ICredentialManager.cs index 68bf53eb6..dde06bcc7 100644 --- a/src/GitHub.Api/Authentication/ICredentialManager.cs +++ b/src/GitHub.Api/Authentication/ICredentialManager.cs @@ -8,7 +8,7 @@ public interface ICredential : IDisposable UriString Host { get; } string Username { get; } string Token { get; } - void UpdateToken(string token, string username); + void Update(string token, string username); } public interface ICredentialManager diff --git a/src/GitHub.Api/Authentication/IKeychain.cs b/src/GitHub.Api/Authentication/IKeychain.cs index 6eca4c998..4a14e1e2f 100644 --- a/src/GitHub.Api/Authentication/IKeychain.cs +++ b/src/GitHub.Api/Authentication/IKeychain.cs @@ -6,15 +6,13 @@ namespace GitHub.Unity public interface IKeychain { IKeychainAdapter Connect(UriString host); - IKeychainAdapter Load(UriString host, bool dontClear = false); + IKeychainAdapter LoadFromSystem(UriString host); void Clear(UriString host, bool deleteFromCredentialManager); - void Save(UriString host); - void SetCredentials(ICredential credential); + void SaveToSystem(UriString host); void Initialize(); Connection[] Connections { get; } IList Hosts { get; } bool HasKeys { get; } - void SetToken(UriString host, string token, string username); event Action ConnectionsChanged; } diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index 07c59f94f..a32cc3c42 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -95,40 +95,35 @@ public Keychain(IEnvironment environment, ICredentialManager credentialManager) public IKeychainAdapter Connect(UriString host) { Guard.ArgumentNotNull(host, nameof(host)); - return FindOrCreateAdapter(host); } - public IKeychainAdapter Load(UriString host, bool dontClear = false) + public IKeychainAdapter LoadFromSystem(UriString host) { Guard.ArgumentNotNull(host, nameof(host)); var keychainAdapter = Connect(host) as KeychainAdapter; - var keychainItem = credentialManager.Load(host); - if (keychainItem == null) + var credential = credentialManager.Load(host); + if (credential == null) { - if (!dontClear) - { - logger.Warning("Cannot load host from Credential Manager; removing from cache"); - Clear(host, false); - } + logger.Warning("Cannot load host from Credential Manager; removing from cache"); + Clear(host, false); keychainAdapter = null; } else { + keychainAdapter.Set(credential); var connection = GetConnection(host); if (connection.Username == null) { - connection.Username = keychainItem.Username; + connection.Username = credential.Username; SaveConnectionsToDisk(); } - if (keychainItem.Username != connection.Username) + if (credential.Username != connection.Username) { - logger.Warning("Keychain Username:\"{0}\" does not match cached Username:\"{1}\"; Hopefully it works", keychainItem.Username, connection.Username); + logger.Warning("Keychain Username:\"{0}\" does not match cached Username:\"{1}\"; Hopefully it works", credential.Username, connection.Username); } - - keychainAdapter.Set(keychainItem); } return keychainAdapter; } @@ -159,7 +154,7 @@ public void Clear(UriString host, bool deleteFromCredentialManager) RemoveCredential(host, deleteFromCredentialManager); } - public void Save(UriString host) + public void SaveToSystem(UriString host) { Guard.ArgumentNotNull(host, nameof(host)); @@ -167,24 +162,6 @@ public void Save(UriString host) AddConnection(new Connection(host, keychainAdapter.Credential.Username)); } - public void SetCredentials(ICredential credential) - { - Guard.ArgumentNotNull(credential, nameof(credential)); - - var keychainAdapter = GetKeychainAdapter(credential.Host); - keychainAdapter.Set(credential); - } - - public void SetToken(UriString host, string token, string username) - { - Guard.ArgumentNotNull(host, nameof(host)); - Guard.ArgumentNotNull(token, nameof(token)); - Guard.ArgumentNotNull(username, nameof(username)); - - var keychainAdapter = GetKeychainAdapter(host); - keychainAdapter.UpdateToken(token, username); - } - private void LoadConnectionsFromDisk() { if (cachePath.FileExists()) diff --git a/src/GitHub.Api/Authentication/KeychainAdapter.cs b/src/GitHub.Api/Authentication/KeychainAdapter.cs index abbe9895e..9f5d30d5c 100644 --- a/src/GitHub.Api/Authentication/KeychainAdapter.cs +++ b/src/GitHub.Api/Authentication/KeychainAdapter.cs @@ -9,9 +9,9 @@ public void Set(ICredential credential) Credential = credential; } - public void UpdateToken(string token, string username) + public void Update(string token, string username) { - Credential.UpdateToken(token, username); + Credential.Update(token, username); } public void Clear() @@ -23,5 +23,8 @@ public void Clear() public interface IKeychainAdapter { ICredential Credential { get; } + void Set(ICredential credential); + void Update(string token, string username); + void Clear(); } } diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 602230c6d..9a67986af 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -56,8 +56,8 @@ public LoginResultData Login( // Start by saving the username and password, these will be used by the `IGitHubClient` // until an authorization token has been created and acquired: - keychain.Connect(host); - keychain.SetCredentials(new Credential(host, username, password)); + var keychainAdapter = keychain.Connect(host); + keychainAdapter.Set(new Credential(host, username, password)); try { @@ -69,16 +69,13 @@ public LoginResultData Login( throw new InvalidOperationException("Returned token is null or empty"); } - if (loginResultData.Code == LoginResultCodes.Success) - { - username = RetrieveUsername(loginResultData, username); - } - - keychain.SetToken(host, loginResultData.Token, username); + keychainAdapter.Update(loginResultData.Token, username); if (loginResultData.Code == LoginResultCodes.Success) { - keychain.Save(host); + username = RetrieveUsername(loginResultData, username); + keychainAdapter.Update(loginResultData.Token, username); + keychain.SaveToSystem(host); } return loginResultData; @@ -99,6 +96,9 @@ public LoginResultData ContinueLogin(LoginResultData loginResultData, string two { var host = loginResultData.Host; var keychainAdapter = keychain.Connect(host); + if (keychainAdapter.Credential == null) { + return new LoginResultData(LoginResultCodes.Failed, Localization.LoginFailed, host); + } var username = keychainAdapter.Credential.Username; var password = keychainAdapter.Credential.Token; try @@ -112,9 +112,10 @@ public LoginResultData ContinueLogin(LoginResultData loginResultData, string two throw new InvalidOperationException("Returned token is null or empty"); } + keychainAdapter.Update(loginResultData.Token, username); username = RetrieveUsername(loginResultData, username); - keychain.SetToken(host, loginResultData.Token, username); - keychain.Save(host); + keychainAdapter.Update(loginResultData.Token, username); + keychain.SaveToSystem(host); return loginResultData; } diff --git a/src/GitHub.Api/Tasks/OctorunTask.cs b/src/GitHub.Api/Tasks/OctorunTask.cs index 9d32ebd4e..b094bd6e7 100644 --- a/src/GitHub.Api/Tasks/OctorunTask.cs +++ b/src/GitHub.Api/Tasks/OctorunTask.cs @@ -77,9 +77,18 @@ public OctorunTask(CancellationToken token, IKeychain keychain, IEnvironment env : new UriString(cloneUrl.ToRepositoryUri() .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); - var adapter = keychain.Load(host, true); - if (adapter != null) + var adapter = keychain.Connect(host); + if (adapter.Credential?.Token != null) + { userToken = adapter.Credential.Token; + } + else + { + // use a cached adapter if there is one filled out + adapter = keychain.LoadFromSystem(host); + if (adapter != null) + userToken = adapter.Credential.Token; + } } public override void Configure(ProcessStartInfo psi) diff --git a/src/tests/UnitTests/Authentication/KeychainTests.cs b/src/tests/UnitTests/Authentication/KeychainTests.cs index 4c5a1e3a0..5e077ef1f 100644 --- a/src/tests/UnitTests/Authentication/KeychainTests.cs +++ b/src/tests/UnitTests/Authentication/KeychainTests.cs @@ -176,7 +176,7 @@ public void ShouldLoadFromConnectionManager() fileSystem.DidNotReceive().WriteAllLines(Args.String, Arg.Any()); var uriString = keychain.Hosts.FirstOrDefault(); - var keychainAdapter = keychain.Load(uriString); + var keychainAdapter = keychain.LoadFromSystem(uriString); keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(token); keychainAdapter.Credential.Host.Should().Be(hostUri); @@ -222,7 +222,7 @@ public void ShouldDeleteFromCacheWhenLoadReturnsNullFromConnectionManager() fileSystem.ClearReceivedCalls(); var uriString = keychain.Hosts.FirstOrDefault(); - var keychainAdapter = keychain.Load(uriString); + var keychainAdapter = keychain.LoadFromSystem(uriString); keychainAdapter.Should().BeNull(); fileSystem.DidNotReceive().FileExists(Args.String); @@ -281,21 +281,21 @@ public void ShouldConnectSetCredentialsTokenAndSave() keychainAdapter.Credential.Should().BeNull(); - keychain.SetCredentials(new Credential(hostUri, username, password)); + keychainAdapter.Set(new Credential(hostUri, username, password)); keychainAdapter.Credential.Should().NotBeNull(); keychainAdapter.Credential.Host.Should().Be(hostUri); keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(password); - keychain.SetToken(hostUri, token, username); + keychainAdapter.Update(token, username); keychainAdapter.Credential.Should().NotBeNull(); keychainAdapter.Credential.Host.Should().Be(hostUri); keychainAdapter.Credential.Username.Should().Be(username); keychainAdapter.Credential.Token.Should().Be(token); - keychain.Save(hostUri); + keychain.SaveToSystem(hostUri); fileSystem.DidNotReceive().FileExists(Args.String); fileSystem.DidNotReceive().FileDelete(Args.String); @@ -352,7 +352,7 @@ public void ShouldConnectSetCredentialsAndClear() keychainAdapter.Credential.Should().BeNull(); - keychain.SetCredentials(new Credential(hostUri, username, password)); + keychainAdapter.Set(new Credential(hostUri, username, password)); keychainAdapter.Credential.Should().NotBeNull(); keychainAdapter.Credential.Host.Should().Be(hostUri); From d054a610b00ae9a7746692c57039a447a28fc81f Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 30 Jul 2018 16:51:46 +0200 Subject: [PATCH 371/567] Bump packaging submodule to pick up small fix --- submodules/packaging | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/packaging b/submodules/packaging index 369486ccc..43af07792 160000 --- a/submodules/packaging +++ b/submodules/packaging @@ -1 +1 @@ -Subproject commit 369486cccfc13b5749f611964fa554f1c1675664 +Subproject commit 43af077928bf3cfa06cfaac507b801efc950d475 From 9d2f1dfb01f24771457f3644049206d965aa696e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 30 Jul 2018 12:58:20 -0400 Subject: [PATCH 372/567] Correcting progress delta computation --- src/GitHub.Api/Helpers/Progress.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/GitHub.Api/Helpers/Progress.cs b/src/GitHub.Api/Helpers/Progress.cs index c7ae0a003..312cd43da 100644 --- a/src/GitHub.Api/Helpers/Progress.cs +++ b/src/GitHub.Api/Helpers/Progress.cs @@ -72,8 +72,7 @@ public void UpdateProgress(long value, long total, string message = null) float fTotal = Total; float fValue = Value; Percentage = fValue / fTotal; - float delta = fValue / fTotal - previousValue / fTotal; - delta = delta * 100f / fTotal; + var delta = (fValue / fTotal - previousValue / fTotal) * 100f; if (Value != previousValue && (fValue == 0f || delta > 1f || fValue == fTotal)) { // signal progress in 1% increments or if we don't know what the total is From 675714afdabd86a0ccbcf51837d36705ee9425c8 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 31 Jul 2018 14:46:08 -0400 Subject: [PATCH 373/567] Preventing queue execution if no items are added --- src/GitHub.Api/Tasks/ActionTask.cs | 47 +++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index 344704561..e1c5db110 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -31,16 +31,30 @@ public ITask Queue(ITask task) public override void RunSynchronously() { - foreach (var task in queuedTasks) - task.Start(); - base.RunSynchronously(); + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) + task.Start(); + base.RunSynchronously(); + } + else + { + aggregateTask.TrySetResult(true); + } } protected override void Schedule() { - foreach (var task in queuedTasks) - task.Start(); - base.Schedule(); + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) + task.Start(); + base.Schedule(); + } + else + { + aggregateTask.TrySetResult(true); + } } private void InvokeFinishOnlyOnSuccess(ITask task, bool success, Exception ex) @@ -115,16 +129,29 @@ public ITask Queue(ITask task) public override List RunSynchronously() { - foreach (var task in queuedTasks) + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) task.Start(); - return base.RunSynchronously(); + return base.RunSynchronously(); + } + + aggregateTask.TrySetResult(new List()); + return Result; } protected override void Schedule() { - foreach (var task in queuedTasks) + if (queuedTasks.Any()) + { + foreach (var task in queuedTasks) task.Start(); - base.Schedule(); + base.Schedule(); + } + else + { + aggregateTask.TrySetResult(new List()); + } } private void InvokeFinishOnlyOnSuccess(ITask task, TTaskResult result, bool success, Exception ex) From d1dd6695d78bf482f1f36c5e417c5a717f125137 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 31 Jul 2018 16:34:00 -0400 Subject: [PATCH 374/567] Correcting initialization string --- src/GitHub.Api/Application/ApplicationManagerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index e009573d2..53370df38 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -138,7 +138,7 @@ public void Run() RestartRepository(); } - progress.UpdateProgress(100, 100, "Initialization failed"); + progress.UpdateProgress(100, 100, "Initialized"); } catch (Exception ex) { From 0812397cf166e5a686cbc9f48cb85989e3e0c079 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 31 Jul 2018 16:41:08 -0400 Subject: [PATCH 375/567] Ignoring a flaky test --- .../IntegrationTests/Process/ProcessManagerIntegrationTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs b/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs index 454a81ee2..9ab837f95 100644 --- a/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs +++ b/src/tests/IntegrationTests/Process/ProcessManagerIntegrationTests.cs @@ -13,6 +13,7 @@ namespace IntegrationTests class ProcessManagerIntegrationTests : BaseGitEnvironmentTest { [Test] + [Category("DoNotRunOnAppVeyor")] public async Task BranchListTest() { InitializePlatformAndEnvironment(TestRepoMasterCleanUnsynchronized); From ab0f819d2c5504381a2c805b82fc449cfd6425ca Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 1 Aug 2018 09:57:41 -0400 Subject: [PATCH 376/567] Updating resharper settings to 2018.1.3 --- GitHub.Unity.sln.DotSettings | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/GitHub.Unity.sln.DotSettings b/GitHub.Unity.sln.DotSettings index 31c5e56f1..2166c735a 100644 --- a/GitHub.Unity.sln.DotSettings +++ b/GitHub.Unity.sln.DotSettings @@ -22,8 +22,11 @@ END_OF_LINE 1 1 + False + False False True + NEVER False True False @@ -339,8 +342,13 @@ SSH <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + True + True + True + True True True + True True True True From 801d03307a583f7e8a99434775a4a5b68a4ebc03 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 1 Aug 2018 10:29:42 -0400 Subject: [PATCH 377/567] Making sure to call base methods appropriately --- src/GitHub.Api/Tasks/ActionTask.cs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/GitHub.Api/Tasks/ActionTask.cs b/src/GitHub.Api/Tasks/ActionTask.cs index e1c5db110..73692baea 100644 --- a/src/GitHub.Api/Tasks/ActionTask.cs +++ b/src/GitHub.Api/Tasks/ActionTask.cs @@ -35,12 +35,13 @@ public override void RunSynchronously() { foreach (var task in queuedTasks) task.Start(); - base.RunSynchronously(); } else { aggregateTask.TrySetResult(true); } + + base.RunSynchronously(); } protected override void Schedule() @@ -49,12 +50,13 @@ protected override void Schedule() { foreach (var task in queuedTasks) task.Start(); - base.Schedule(); } else { aggregateTask.TrySetResult(true); } + + base.Schedule(); } private void InvokeFinishOnlyOnSuccess(ITask task, bool success, Exception ex) @@ -132,12 +134,14 @@ public override List RunSynchronously() if (queuedTasks.Any()) { foreach (var task in queuedTasks) - task.Start(); - return base.RunSynchronously(); + task.Start(); + } + else + { + aggregateTask.TrySetResult(new List()); } - aggregateTask.TrySetResult(new List()); - return Result; + return base.RunSynchronously(); } protected override void Schedule() @@ -145,13 +149,14 @@ protected override void Schedule() if (queuedTasks.Any()) { foreach (var task in queuedTasks) - task.Start(); - base.Schedule(); + task.Start(); } else { aggregateTask.TrySetResult(new List()); } + + base.Schedule(); } private void InvokeFinishOnlyOnSuccess(ITask task, TTaskResult result, bool success, Exception ex) From cc76f560b4fcf2a31d6e7e2dda95b16a934431ac Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 1 Aug 2018 14:48:18 -0400 Subject: [PATCH 378/567] Updating gitignore --- unity/PackageProject/.gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unity/PackageProject/.gitignore b/unity/PackageProject/.gitignore index 01498ea52..02fb6cbd5 100644 --- a/unity/PackageProject/.gitignore +++ b/unity/PackageProject/.gitignore @@ -15,4 +15,6 @@ Library/ // These files come from lib/ Assets/Plugins/GitHub/Editor/x64/ -Assets/Plugins/GitHub/Editor/x86/ \ No newline at end of file +Assets/Plugins/GitHub/Editor/x86/ +Assets/Plugins/GitHub/Editor/libsfw.bundle.meta +Assets/Plugins/GitHub/Editor/libsfw.so.meta \ No newline at end of file From f42dfc0ebba05db086739a8f1ca9eecd3463a132 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 1 Aug 2018 14:52:22 -0400 Subject: [PATCH 379/567] Removing files --- .../Assets/Plugins/GitHub/Editor/libsfw.bundle.meta | 8 ++++---- .../Assets/Plugins/GitHub/Editor/libsfw.so.meta | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta index 907426232..bb4f8fabf 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta @@ -34,7 +34,7 @@ PluginImporter: OS: OSX data: first: - Any: + Any: second: enabled: 0 settings: {} @@ -101,6 +101,6 @@ PluginImporter: enabled: 0 settings: CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta index f5ba9573e..af2f46c79 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta @@ -34,7 +34,7 @@ PluginImporter: OS: Linux data: first: - Any: + Any: second: enabled: 0 settings: {} @@ -101,6 +101,6 @@ PluginImporter: enabled: 0 settings: CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: From 2c0944d0f6c80aa453f88f500ee2fe9ede2b3251 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 2 Aug 2018 09:16:28 -0400 Subject: [PATCH 380/567] Removing files as they are duplicates --- .../Plugins/GitHub/Editor/libsfw.bundle.meta | 106 ------------------ .../Plugins/GitHub/Editor/libsfw.so.meta | 106 ------------------ 2 files changed, 212 deletions(-) delete mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta delete mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta deleted file mode 100644 index bb4f8fabf..000000000 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.bundle.meta +++ /dev/null @@ -1,106 +0,0 @@ -fileFormatVersion: 2 -guid: 636d33ae594884e7d80b569f429d245d -timeCreated: 1503667182 -licenseType: Free -PluginImporter: - serializedVersion: 2 - iconMap: {} - executionOrder: {} - isPreloaded: 0 - isOverridable: 0 - platformData: - data: - first: - '': Any - second: - enabled: 0 - settings: - Exclude Editor: 0 - Exclude Linux: 1 - Exclude Linux64: 1 - Exclude LinuxUniversal: 1 - Exclude OSXIntel: 1 - Exclude OSXIntel64: 1 - Exclude OSXUniversal: 1 - Exclude Win: 1 - Exclude Win64: 1 - data: - first: - '': Editor - second: - enabled: 0 - settings: - CPU: AnyCPU - OS: OSX - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - data: - first: - Facebook: Win - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Facebook: Win64 - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: Linux - second: - enabled: 0 - settings: - CPU: x86 - data: - first: - Standalone: Linux64 - second: - enabled: 0 - settings: - CPU: x86_64 - data: - first: - Standalone: OSXIntel - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: OSXIntel64 - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: Win - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: Win64 - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta deleted file mode 100644 index af2f46c79..000000000 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/libsfw.so.meta +++ /dev/null @@ -1,106 +0,0 @@ -fileFormatVersion: 2 -guid: 21206c65839f84d0e9ae14bc1fdc68db -timeCreated: 1503931807 -licenseType: Pro -PluginImporter: - serializedVersion: 2 - iconMap: {} - executionOrder: {} - isPreloaded: 0 - isOverridable: 0 - platformData: - data: - first: - '': Any - second: - enabled: 0 - settings: - Exclude Editor: 0 - Exclude Linux: 1 - Exclude Linux64: 1 - Exclude LinuxUniversal: 1 - Exclude OSXIntel: 1 - Exclude OSXIntel64: 1 - Exclude OSXUniversal: 1 - Exclude Win: 1 - Exclude Win64: 1 - data: - first: - '': Editor - second: - enabled: 0 - settings: - CPU: AnyCPU - OS: Linux - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - data: - first: - Facebook: Win - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Facebook: Win64 - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: Linux - second: - enabled: 0 - settings: - CPU: x86 - data: - first: - Standalone: Linux64 - second: - enabled: 0 - settings: - CPU: x86_64 - data: - first: - Standalone: OSXIntel - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: OSXIntel64 - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: Win - second: - enabled: 0 - settings: - CPU: AnyCPU - data: - first: - Standalone: Win64 - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: From 99282dbb2a43760596e68eeaf575fbff9bd221e6 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 2 Aug 2018 15:33:56 -0400 Subject: [PATCH 381/567] Bump version to 1.0.2 --- common/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 276bb3268..04ce44d45 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -32,8 +32,8 @@ namespace System { internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics - internal const string VersionForAssembly = "1.0.1"; + internal const string VersionForAssembly = "1.0.2"; // Actual real version - internal const string Version = "1.0.1"; + internal const string Version = "1.0.2"; } } From 2a42d7c53fd2e475939b8b161147cbef3063442c Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 9 Aug 2018 10:17:02 -0400 Subject: [PATCH 382/567] Silly style --- src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index 8896c1068..206843df9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -494,7 +494,6 @@ public static GUIStyle ToolbarButtonStyle toolbarButtonStyle = new GUIStyle(EditorStyles.toolbarButton); toolbarButtonStyle.name = "HistoryToolbarButtonStyle"; toolbarButtonStyle.richText = true; - toolbarButtonStyle.wordWrap = true; } return toolbarButtonStyle; } From d488a27158c5501bbeb636da8fd72e5280908062 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 13 Aug 2018 10:12:51 -0400 Subject: [PATCH 383/567] Scaling the image by pixelsPerPoint --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index c3cc0fb03..d228a361e 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -283,6 +283,7 @@ private static void OnProjectWindowItemGUI(string guid, Rect itemRect) { var scale = itemRect.height / 90f; var size = new Vector2(texture.width * scale, texture.height * scale); + size = size / EditorGUIUtility.pixelsPerPoint; var offset = new Vector2(itemRect.width * Mathf.Min(.4f * scale, .2f), itemRect.height * Mathf.Min(.2f * scale, .2f)); rect = new Rect(itemRect.center.x - size.x * .5f + offset.x, itemRect.center.y - size.y * .5f + offset.y, size.x, size.y); } From f0bbf9755950acd4af8b1251ba8a6d39124ce0f3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 13 Aug 2018 11:26:56 -0400 Subject: [PATCH 384/567] Correctly specifying the Unity merge tool --- src/GitHub.Api/Resources/.gitattributes | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Resources/.gitattributes b/src/GitHub.Api/Resources/.gitattributes index df7ad1b69..7782ad6fe 100644 --- a/src/GitHub.Api/Resources/.gitattributes +++ b/src/GitHub.Api/Resources/.gitattributes @@ -1,10 +1,10 @@ * text=auto # Unity files -*.meta -text -merge=unityamlmerge -*.unity -text -merge=unityamlmerge -*.asset -text -merge=unityamlmerge -*.prefab -text -merge=unityamlmerge +*.meta -text merge=unityamlmerge diff +*.unity -text merge=unityamlmerge diff +*.asset -text merge=unityamlmerge diff +*.prefab -text merge=unityamlmerge diff # Image formats *.psd filter=lfs diff=lfs merge=lfs -text From 12558c81985c785cdb0df87cf561b31f4cf1ea98 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 15 Aug 2018 10:25:12 -0400 Subject: [PATCH 385/567] Adding documentation for IGitClient --- src/GitHub.Api/Git/GitClient.cs | 268 ++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 8dc75d6b6..396a88e78 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -6,40 +6,275 @@ namespace GitHub.Unity { + /// + /// Client that provides access to git functionality + /// public interface IGitClient { + /// + /// Executes `git init` to initialize a git repo. + /// + /// A custom output processor instance + /// String output of git command ITask Init(IOutputProcessor processor = null); + + /// + /// Executes `git lfs install` to install LFS hooks. + /// + /// A custom output processor instance + /// String output of git command ITask LfsInstall(IOutputProcessor processor = null); + + /// + /// Executes `git rev-list` to determine the ahead/behind status between two refs. + /// + /// Ref to compare + /// Ref to compare against + /// A custom output processor instance + /// output ITask AheadBehindStatus(string gitRef, string otherRef, IOutputProcessor processor = null); + + /// + /// Executes `git status` to determine the working directory status. + /// + /// A custom output processor instance + /// output ITask Status(IOutputProcessor processor = null); + + /// + /// Executes `git config get` to get a configuration value. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null); + + /// + /// Executes `git config set` to set a configuration value. + /// + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask SetConfig(string key, string value, GitConfigSource configSource, IOutputProcessor processor = null); + + /// + /// Executes two `git config get` commands to get the git user and email. + /// + /// output ITask GetConfigUserAndEmail(); + + /// + /// Executes `git lfs locks` to get a list of lfs locks from the git lfs server. + /// + /// + /// A custom output processor instance + /// of output ITask> ListLocks(bool local, BaseOutputListProcessor processor = null); + + /// + /// Executes `git pull` to perform a pull operation. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask Pull(string remote, string branch, IOutputProcessor processor = null); + + /// + /// Executes `git push` to perform a push operation. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask Push(string remote, string branch, IOutputProcessor processor = null); + + /// + /// Executes `git revert` to perform a revert operation. + /// + /// + /// A custom output processor instance + /// String output of git command ITask Revert(string changeset, IOutputProcessor processor = null); + + /// + /// Executes `git fetch` to perform a fetch operation. + /// + /// + /// A custom output processor instance + /// String output of git command ITask Fetch(string remote, IOutputProcessor processor = null); + + /// + /// Executes `git checkout` to switch branches. + /// + /// + /// A custom output processor instance + /// String output of git command ITask SwitchBranch(string branch, IOutputProcessor processor = null); + + /// + /// Executes `git branch -d` to delete a branch. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask DeleteBranch(string branch, bool deleteUnmerged = false, IOutputProcessor processor = null); + + /// + /// Executes `git branch` to create a branch. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask CreateBranch(string branch, string baseBranch, IOutputProcessor processor = null); + + /// + /// Executes `git remote add` to add a git remote. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask RemoteAdd(string remote, string url, IOutputProcessor processor = null); + + /// + /// Executes `git remote rm` to remove a git remote. + /// + /// + /// A custom output processor instance + /// String output of git command ITask RemoteRemove(string remote, IOutputProcessor processor = null); + + /// + /// Executes `git remote set-url` to change the url of a git remote. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask RemoteChange(string remote, string url, IOutputProcessor processor = null); + + /// + /// Executes `git commit` to perform a commit operation. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask Commit(string message, string body, IOutputProcessor processor = null); + + /// + /// Executes at least one `git add` command to add the list of files to the git index. + /// + /// + /// A custom output processor instance + /// String output of git command ITask Add(IList files, IOutputProcessor processor = null); + + /// + /// Executes `git add -A` to add all files to the git index. + /// + /// A custom output processor instance + /// String output of git command ITask AddAll(IOutputProcessor processor = null); + + /// + /// Executes at least one `git checkout` command to discard changes to the list of files. + /// + /// + /// A custom output processor instance + /// String output of git command ITask Discard(IList files, IOutputProcessor processor = null); + + /// + /// Executes `git checkout -- .` to discard all changes in the working directory. + /// + /// A custom output processor instance + /// String output of git command ITask DiscardAll(IOutputProcessor processor = null); + + /// + /// Executes `git reset HEAD` command to remove files from the git index. + /// + /// + /// A custom output processor instance + /// String output of git command ITask Remove(IList files, IOutputProcessor processor = null); + + /// + /// Executes at least one `git add` command to add the list of files to the git index. Followed by a `git commit` command to commit the changes. + /// + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask AddAndCommit(IList files, string message, string body, IOutputProcessor processor = null); + + /// + /// Executes `git lfs lock` to lock a file. + /// + /// + /// A custom output processor instance + /// String output of git command ITask Lock(NPath file, IOutputProcessor processor = null); + + /// + /// Executes `git lfs unlock` to unlock a file. + /// + /// + /// + /// A custom output processor instance + /// String output of git command ITask Unlock(NPath file, bool force, IOutputProcessor processor = null); + + /// + /// Executes `git log` to get the history of the current branch. + /// + /// A custom output processor instance + /// of output ITask> Log(BaseOutputListProcessor processor = null); + + /// + /// Executes `git --version` to get the git version. + /// + /// A custom output processor instance + /// output ITask Version(IOutputProcessor processor = null); + + /// + /// Executes `git lfs version` to get the git lfs version. + /// + /// A custom output processor instance + /// output ITask LfsVersion(IOutputProcessor processor = null); + + /// + /// Executes `git count-objects` to get the size of the git repo in kilobytes. + /// + /// A custom output processor instance + /// output ITask CountObjects(IOutputProcessor processor = null); + + /// + /// Executes two `git set config` commands to set the git name and email. + /// + /// + /// + /// output ITask SetConfigNameAndEmail(string username, string email); + + /// + /// Executes `git rev-parse --short HEAD` to get the current commit sha of the current branch. + /// + /// A custom output processor instance + /// String output of git command ITask GetHead(IOutputProcessor processor = null); } @@ -58,66 +293,77 @@ public GitClient(IEnvironment environment, IProcessManager processManager, Cance this.cancellationToken = cancellationToken; } + /// public ITask Init(IOutputProcessor processor = null) { return new GitInitTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask LfsInstall(IOutputProcessor processor = null) { return new GitLfsInstallTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask Status(IOutputProcessor processor = null) { return new GitStatusTask(new GitObjectFactory(environment), cancellationToken, processor) .Configure(processManager); } + /// public ITask AheadBehindStatus(string gitRef, string otherRef, IOutputProcessor processor = null) { return new GitAheadBehindStatusTask(gitRef, otherRef, cancellationToken, processor) .Configure(processManager); } + /// public ITask> Log(BaseOutputListProcessor processor = null) { return new GitLogTask(new GitObjectFactory(environment), cancellationToken, processor) .Configure(processManager); } + /// public ITask Version(IOutputProcessor processor = null) { return new GitVersionTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask LfsVersion(IOutputProcessor processor = null) { return new GitLfsVersionTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask CountObjects(IOutputProcessor processor = null) { return new GitCountObjectsTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null) { return new GitConfigGetTask(key, configSource, cancellationToken, processor) .Configure(processManager); } + /// public ITask SetConfig(string key, string value, GitConfigSource configSource, IOutputProcessor processor = null) { return new GitConfigSetTask(key, value, configSource, cancellationToken, processor) .Configure(processManager); } + /// public ITask GetConfigUserAndEmail() { string username = null; @@ -141,6 +387,7 @@ public ITask GetConfigUserAndEmail() }); } + /// public ITask SetConfigNameAndEmail(string username, string email) { return SetConfig(UserNameConfigKey, username, GitConfigSource.User) @@ -148,18 +395,21 @@ public ITask SetConfigNameAndEmail(string username, string email) .Then(b => new GitUser(username, email)); } + /// public ITask> ListLocks(bool local, BaseOutputListProcessor processor = null) { return new GitListLocksTask(local, cancellationToken, processor) .Configure(processManager, environment.GitLfsExecutablePath); } + /// public ITask Pull(string remote, string branch, IOutputProcessor processor = null) { return new GitPullTask(remote, branch, cancellationToken, processor) .Configure(processManager); } + /// public ITask Push(string remote, string branch, IOutputProcessor processor = null) { @@ -167,12 +417,14 @@ public ITask Push(string remote, string branch, .Configure(processManager); } + /// public ITask Revert(string changeset, IOutputProcessor processor = null) { return new GitRevertTask(changeset, cancellationToken, processor) .Configure(processManager); } + /// public ITask Fetch(string remote, IOutputProcessor processor = null) { @@ -180,12 +432,14 @@ public ITask Fetch(string remote, .Configure(processManager); } + /// public ITask SwitchBranch(string branch, IOutputProcessor processor = null) { return new GitSwitchBranchesTask(branch, cancellationToken, processor) .Configure(processManager); } + /// public ITask DeleteBranch(string branch, bool deleteUnmerged = false, IOutputProcessor processor = null) { @@ -193,6 +447,7 @@ public ITask DeleteBranch(string branch, bool deleteUnmerged = false, .Configure(processManager); } + /// public ITask CreateBranch(string branch, string baseBranch, IOutputProcessor processor = null) { @@ -200,6 +455,7 @@ public ITask CreateBranch(string branch, string baseBranch, .Configure(processManager); } + /// public ITask RemoteAdd(string remote, string url, IOutputProcessor processor = null) { @@ -207,6 +463,7 @@ public ITask RemoteAdd(string remote, string url, .Configure(processManager); } + /// public ITask RemoteRemove(string remote, IOutputProcessor processor = null) { @@ -214,6 +471,7 @@ public ITask RemoteRemove(string remote, .Configure(processManager); } + /// public ITask RemoteChange(string remote, string url, IOutputProcessor processor = null) { @@ -221,6 +479,7 @@ public ITask RemoteChange(string remote, string url, .Configure(processManager); } + /// public ITask Commit(string message, string body, IOutputProcessor processor = null) { @@ -228,12 +487,14 @@ public ITask Commit(string message, string body, .Configure(processManager); } + /// public ITask AddAll(IOutputProcessor processor = null) { return new GitAddTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask Add(IList files, IOutputProcessor processor = null) { @@ -255,6 +516,7 @@ public ITask Add(IList files, return last; } + /// public ITask Discard( IList files, IOutputProcessor processor = null) { @@ -276,12 +538,14 @@ public ITask Discard( IList files, return last; } + /// public ITask DiscardAll(IOutputProcessor processor = null) { return new GitCheckoutTask(cancellationToken, processor) .Configure(processManager); } + /// public ITask Remove(IList files, IOutputProcessor processor = null) { @@ -289,6 +553,7 @@ public ITask Remove(IList files, .Configure(processManager); } + /// public ITask AddAndCommit(IList files, string message, string body, IOutputProcessor processor = null) { @@ -297,6 +562,7 @@ public ITask AddAndCommit(IList files, string message, string bo .Configure(processManager)); } + /// public ITask Lock(NPath file, IOutputProcessor processor = null) { @@ -304,6 +570,7 @@ public ITask Lock(NPath file, .Configure(processManager, environment.GitLfsExecutablePath); } + /// public ITask Unlock(NPath file, bool force, IOutputProcessor processor = null) { @@ -311,6 +578,7 @@ public ITask Unlock(NPath file, bool force, .Configure(processManager, environment.GitLfsExecutablePath); } + /// public ITask GetHead(IOutputProcessor processor = null) { return new FirstNonNullLineProcessTask(cancellationToken, "rev-parse --short HEAD") { Name = "Getting current head..." } From f61bacd744fb87bd4e7a4a02690c01320431fcdf Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 15 Aug 2018 10:40:29 -0400 Subject: [PATCH 386/567] Adding parameters --- src/GitHub.Api/Git/GitClient.cs | 68 ++++++++++++++++----------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 396a88e78..e7fddbf05 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -44,8 +44,8 @@ public interface IGitClient /// /// Executes `git config get` to get a configuration value. /// - /// - /// + /// The configuration key to get + /// The config source (unspecified, local,user,global) to use /// A custom output processor instance /// String output of git command ITask GetConfig(string key, GitConfigSource configSource, IOutputProcessor processor = null); @@ -53,9 +53,9 @@ public interface IGitClient /// /// Executes `git config set` to set a configuration value. /// - /// - /// - /// + /// The configuration key to set + /// The value to set + /// The config source (unspecified, local,user,global) to use /// A custom output processor instance /// String output of git command ITask SetConfig(string key, string value, GitConfigSource configSource, IOutputProcessor processor = null); @@ -77,8 +77,8 @@ public interface IGitClient /// /// Executes `git pull` to perform a pull operation. /// - /// - /// + /// The remote to pull from + /// The branch to pull /// A custom output processor instance /// String output of git command ITask Pull(string remote, string branch, IOutputProcessor processor = null); @@ -86,8 +86,8 @@ public interface IGitClient /// /// Executes `git push` to perform a push operation. /// - /// - /// + /// The remote to push to + /// The branch to push /// A custom output processor instance /// String output of git command ITask Push(string remote, string branch, IOutputProcessor processor = null); @@ -95,7 +95,7 @@ public interface IGitClient /// /// Executes `git revert` to perform a revert operation. /// - /// + /// The changeset to revert /// A custom output processor instance /// String output of git command ITask Revert(string changeset, IOutputProcessor processor = null); @@ -103,7 +103,7 @@ public interface IGitClient /// /// Executes `git fetch` to perform a fetch operation. /// - /// + /// The remote to fetch from /// A custom output processor instance /// String output of git command ITask Fetch(string remote, IOutputProcessor processor = null); @@ -111,7 +111,7 @@ public interface IGitClient /// /// Executes `git checkout` to switch branches. /// - /// + /// The branch to checkout /// A custom output processor instance /// String output of git command ITask SwitchBranch(string branch, IOutputProcessor processor = null); @@ -119,8 +119,8 @@ public interface IGitClient /// /// Executes `git branch -d` to delete a branch. /// - /// - /// + /// The branch to delete + /// The flag to indicate the branch should be deleted even if not merged /// A custom output processor instance /// String output of git command ITask DeleteBranch(string branch, bool deleteUnmerged = false, IOutputProcessor processor = null); @@ -128,8 +128,8 @@ public interface IGitClient /// /// Executes `git branch` to create a branch. /// - /// - /// + /// The name of branch to create + /// The name of branch to create from /// A custom output processor instance /// String output of git command ITask CreateBranch(string branch, string baseBranch, IOutputProcessor processor = null); @@ -137,8 +137,8 @@ public interface IGitClient /// /// Executes `git remote add` to add a git remote. /// - /// - /// + /// The remote to add + /// The url of the remote /// A custom output processor instance /// String output of git command ITask RemoteAdd(string remote, string url, IOutputProcessor processor = null); @@ -146,7 +146,7 @@ public interface IGitClient /// /// Executes `git remote rm` to remove a git remote. /// - /// + /// The remote to remove /// A custom output processor instance /// String output of git command ITask RemoteRemove(string remote, IOutputProcessor processor = null); @@ -154,8 +154,8 @@ public interface IGitClient /// /// Executes `git remote set-url` to change the url of a git remote. /// - /// - /// + /// The remote to change + /// The url to change to /// A custom output processor instance /// String output of git command ITask RemoteChange(string remote, string url, IOutputProcessor processor = null); @@ -163,8 +163,8 @@ public interface IGitClient /// /// Executes `git commit` to perform a commit operation. /// - /// - /// + /// The commit message summary + /// The commit message body /// A custom output processor instance /// String output of git command ITask Commit(string message, string body, IOutputProcessor processor = null); @@ -172,7 +172,7 @@ public interface IGitClient /// /// Executes at least one `git add` command to add the list of files to the git index. /// - /// + /// The file to add /// A custom output processor instance /// String output of git command ITask Add(IList files, IOutputProcessor processor = null); @@ -187,7 +187,7 @@ public interface IGitClient /// /// Executes at least one `git checkout` command to discard changes to the list of files. /// - /// + /// The files to discard /// A custom output processor instance /// String output of git command ITask Discard(IList files, IOutputProcessor processor = null); @@ -202,7 +202,7 @@ public interface IGitClient /// /// Executes `git reset HEAD` command to remove files from the git index. /// - /// + /// The files to remove /// A custom output processor instance /// String output of git command ITask Remove(IList files, IOutputProcessor processor = null); @@ -210,9 +210,9 @@ public interface IGitClient /// /// Executes at least one `git add` command to add the list of files to the git index. Followed by a `git commit` command to commit the changes. /// - /// - /// - /// + /// The files to add and commit + /// The commit message summary + /// The commit message body /// A custom output processor instance /// String output of git command ITask AddAndCommit(IList files, string message, string body, IOutputProcessor processor = null); @@ -220,7 +220,7 @@ public interface IGitClient /// /// Executes `git lfs lock` to lock a file. /// - /// + /// The file to lock /// A custom output processor instance /// String output of git command ITask Lock(NPath file, IOutputProcessor processor = null); @@ -228,8 +228,8 @@ public interface IGitClient /// /// Executes `git lfs unlock` to unlock a file. /// - /// - /// + /// The file to unlock + /// If force should be used /// A custom output processor instance /// String output of git command ITask Unlock(NPath file, bool force, IOutputProcessor processor = null); @@ -265,8 +265,8 @@ public interface IGitClient /// /// Executes two `git set config` commands to set the git name and email. /// - /// - /// + /// The username to set + /// The email to set /// output ITask SetConfigNameAndEmail(string username, string email); From d1eaa94bed837c64a8be803a664bda0dff1d52a5 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 15 Aug 2018 10:44:48 -0400 Subject: [PATCH 387/567] Making GitClient public --- src/GitHub.Api/Git/GitClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index e7fddbf05..c345a47a7 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -278,7 +278,7 @@ public interface IGitClient ITask GetHead(IOutputProcessor processor = null); } - class GitClient : IGitClient + public class GitClient : IGitClient { private const string UserNameConfigKey = "user.name"; private const string UserEmailConfigKey = "user.email"; From 82e6d9ba2e2a7498d9335073b6d067552c960c9c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 13:18:01 +0200 Subject: [PATCH 388/567] Adding .net 4.5 builds --- GitHub.Unity.sln | 19 ++ common/properties.props | 2 +- src/GitHub.Api/GitHub.Api.45.csproj | 308 ++++++++++++++++++ .../Assets/Editor/GitHub.Unity/EntryPoint.cs | 4 - .../Editor/GitHub.Unity/ExtensionLoader | 13 + .../CopyLibrariesToDevelopmentFolder.csproj | 8 +- .../CopyLibrariesToPackageProject.csproj | 9 + .../GitHub/Editor/AsyncBridge.Net35.dll.meta | 35 +- .../GitHub/Editor/GitHub.Api.45.dll.meta | 34 ++ .../GitHub/Editor/GitHub.Api.dll.mdb.meta | 2 +- .../Plugins/GitHub/Editor/GitHub.Api.dll.meta | 35 +- .../GitHub/Editor/GitHub.Logging.dll.mdb.meta | 2 +- .../GitHub/Editor/GitHub.Logging.dll.meta | 35 +- .../GitHub/Editor/GitHub.Unity.45.dll.meta | 34 ++ .../GitHub/Editor/GitHub.Unity.dll.meta | 2 +- .../Editor/ICSharpCode.SharpZipLib.dll.meta | 35 +- .../Plugins/GitHub/Editor/Mono.Posix.dll.meta | 2 +- .../ReadOnlyCollectionsInterfaces.dll.meta | 35 +- .../GitHub/Editor/System.Threading.dll.meta | 35 +- .../Plugins/GitHub/Editor/sfw.net.dll.meta | 35 +- 20 files changed, 583 insertions(+), 101 deletions(-) create mode 100644 src/GitHub.Api/GitHub.Api.45.csproj create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.meta create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.meta diff --git a/GitHub.Unity.sln b/GitHub.Unity.sln index 0197b47fc..367026b8e 100644 --- a/GitHub.Unity.sln +++ b/GitHub.Unity.sln @@ -5,8 +5,12 @@ VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Unity", "src\UnityExtension\Assets\Editor\GitHub.Unity\GitHub.Unity.csproj", "{ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Unity.45", "src\UnityExtension\Assets\Editor\GitHub.Unity\GitHub.Unity.45.csproj", "{ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Api", "src\GitHub.Api\GitHub.Api.csproj", "{B389ADAF-62CC-486E-85B4-2D8B078DF763}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Api.45", "src\GitHub.Api\GitHub.Api.45.csproj", "{B389ADAF-62CC-486E-85B4-2D8B078DF76B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Logging", "src\GitHub.Logging\GitHub.Logging.csproj", "{BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CopyLibrariesToDevelopmentFolder", "src\packaging\CopyLibrariesToDevelopmentFolder\CopyLibrariesToDevelopmentFolder.csproj", "{44257C81-EE4A-4817-9AF4-A26C02AA6DD4}" @@ -46,6 +50,13 @@ Global {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.dev|Any CPU.Build.0 = dev|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ADD7A18B-DD2A-4C22-A2C1-488964EFF30A}.Release|Any CPU.Build.0 = Release|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.dev|Any CPU.ActiveCfg = dev|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.dev|Any CPU.Build.0 = dev|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B}.Release|Any CPU.Build.0 = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Debug|Any CPU.Build.0 = Debug|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU @@ -54,6 +65,14 @@ Global {B389ADAF-62CC-486E-85B4-2D8B078DF763}.dev|Any CPU.Build.0 = dev|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.ActiveCfg = Release|Any CPU {B389ADAF-62CC-486E-85B4-2D8B078DF763}.Release|Any CPU.Build.0 = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.dev|Any CPU.ActiveCfg = dev|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.dev|Any CPU.Build.0 = dev|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B}.Release|Any CPU.Build.0 = Release|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB6A8EDA-15D8-471B-A6ED-EE551E0B3BA0}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/common/properties.props b/common/properties.props index 977f217b3..8316329ed 100644 --- a/common/properties.props +++ b/common/properties.props @@ -3,7 +3,7 @@ Internal - ENABLE_METRICS + ENABLE_METRICS $(BuildDefs);ENABLE_MONO $(SolutionDir)script\lib\ diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj new file mode 100644 index 000000000..78269c5b4 --- /dev/null +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -0,0 +1,308 @@ + + + + + Debug + AnyCPU + {B389ADAF-62CC-486E-85B4-2D8B078DF76B} + Library + Properties + GitHub.Unity + GitHub.Api.45 + v4.5 + 512 + + 6 + + + ..\UnityExtension\Assets\Editor\build + + + + true + full + false + DEBUG;TRACE;$(BuildDefs) + prompt + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs) + prompt + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) + prompt + 4 + false + false + true + ..\..\common\codeanalysis-debug.ruleset + + + Debug + + + + $(SolutionDir)lib\ICSharpCode.SharpZipLib.dll + + + $(SolutionDir)lib\Mono.Posix.dll + + + $(SolutionDir)\lib\sfw\sfw.net.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Localization.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Metrics\MetricsService.cs + + + Properties\ApplicationInfo_Local.cs + + + + + + + Properties\ApplicationInfo_Local.cs-example + + + Properties\ApplicationInfo_Local.cs + + + + + + + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} + GitHub.Logging + + + + + + + + + + + + + + + + + + + + + + + PublicResXFileCodeGenerator + Localization.Designer.cs + Designer + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 3d99d48f9..01bcdc2f1 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -1,9 +1,5 @@ using GitHub.Logging; using System; -using System.IO; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; using UnityEditor; using UnityEngine; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader new file mode 100644 index 000000000..56291fa6a --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader @@ -0,0 +1,13 @@ +using System; +using UnityEditor; +using UnityEngine; +using GitHub.Logging; + +namespace GitHub.Unity +{ + [InitializeOnLoad] + public class EntryPoint : ScriptableObject + { + + } +} \ No newline at end of file diff --git a/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj b/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj index c02403ad2..8e79d6159 100644 --- a/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj +++ b/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj @@ -28,11 +28,18 @@ prompt 4 + + + {b389adaf-62cc-486e-85b4-2d8b078df763} GitHub.Api + + {b389adaf-62cc-486e-85b4-2d8b078df76B} + GitHub.Api + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging @@ -41,7 +48,6 @@ $(SolutionDir)\lib\sfw\sfw.net.dll True - diff --git a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj index 179d6dbe3..cc951852d 100644 --- a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj +++ b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj @@ -34,6 +34,10 @@ {b389adaf-62cc-486e-85b4-2d8b078df763} GitHub.Api + + {b389adaf-62cc-486e-85b4-2d8b078df76b} + GitHub.Api + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging @@ -43,6 +47,11 @@ GitHub.Unity True + + {add7a18b-dd2a-4c22-a2c1-488964eff30b} + GitHub.Unity + True + $(SolutionDir)\packages\AsyncBridge.Net35.0.2.3333.0\lib\net35-Client\AsyncBridge.Net35.dll True diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta index 68688858e..1c1d85763 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: d516f2a1bec6a9645a084ef8c9237132 timeCreated: 1491391262 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.meta new file mode 100644 index 000000000..3b1911219 --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: c743ae24ee231884887054d20ccdd0ab +timeCreated: 1534504082 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.mdb.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.mdb.meta index ce6d7087f..9b74d73ee 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.mdb.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.mdb.meta @@ -1,7 +1,7 @@ fileFormatVersion: 2 guid: c5c83d14802e712408f23409f3c59e26 timeCreated: 1493304320 -licenseType: Pro +licenseType: Free DefaultImporter: userData: assetBundleName: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta index aae704762..d12a12326 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: c743ae24ee231884887054d20ccdd0ae timeCreated: 1491391261 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.mdb.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.mdb.meta index 8ce5af844..d51a20eaf 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.mdb.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.mdb.meta @@ -1,7 +1,7 @@ fileFormatVersion: 2 guid: 23c8bee69b591054094d32918f98facd timeCreated: 1493304320 -licenseType: Pro +licenseType: Free DefaultImporter: userData: assetBundleName: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.meta index 3d04e1950..46ab48b16 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Logging.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: 15ca2bebf173f2d4484686a03a45b56d timeCreated: 1491391259 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.meta new file mode 100644 index 000000000..1fcd625de --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: 68c7e4565cde54155bb78d8e935f1ddb +timeCreated: 1534504082 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta index 2187c3a65..a70aca527 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta @@ -19,7 +19,7 @@ PluginImporter: first: Editor: Editor second: - enabled: 1 + enabled: 0 settings: DefaultValueInitialized: true data: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta index 8abfa3131..cb9cd75f4 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: ecfb28d906a32914d956497c8d3b3395 timeCreated: 1493304328 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/Mono.Posix.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/Mono.Posix.dll.meta index 7f984fd53..7a71232e5 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/Mono.Posix.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/Mono.Posix.dll.meta @@ -19,7 +19,7 @@ PluginImporter: first: Editor: Editor second: - enabled: 1 + enabled: 0 settings: DefaultValueInitialized: true data: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta index ad958912e..98b231bec 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: 48c22d5d7479fcb49ab3be0cdd2ccec0 timeCreated: 1491391260 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta index 9f8232768..ea6a32d4c 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: 790749ba7e4b18141953e39cb13f1b79 timeCreated: 1491392717 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/sfw.net.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/sfw.net.dll.meta index a749fc943..11b151c05 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/sfw.net.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/sfw.net.dll.meta @@ -1,25 +1,34 @@ fileFormatVersion: 2 guid: f9fc9b08ecd899944adf9860b4abd6b6 timeCreated: 1491392718 -licenseType: Pro +licenseType: Free PluginImporter: - serializedVersion: 1 + serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - Any: - enabled: 0 - settings: {} - Editor: - enabled: 1 - settings: - DefaultValueInitialized: true - WindowsStoreApps: - enabled: 0 - settings: - CPU: AnyCPU + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU userData: assetBundleName: assetBundleVariant: From 47513a9f81fc5855b06fa3d72af24752402d6957 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 15:11:42 +0200 Subject: [PATCH 389/567] The meta file for the loader --- unity/PackageProject/.gitignore | 1 + .../GitHub/Editor/ExtensionLoader.cs.meta | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/ExtensionLoader.cs.meta diff --git a/unity/PackageProject/.gitignore b/unity/PackageProject/.gitignore index 02fb6cbd5..aaecd01aa 100644 --- a/unity/PackageProject/.gitignore +++ b/unity/PackageProject/.gitignore @@ -9,6 +9,7 @@ *.dylib *.so *.bundle +*.cs ProjectVersion.txt Library/ diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ExtensionLoader.cs.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ExtensionLoader.cs.meta new file mode 100644 index 000000000..2060c1819 --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ExtensionLoader.cs.meta @@ -0,0 +1,34 @@ +fileFormatVersion: 2 +guid: dae2ecee8a704dd59797e26554ff8606 +timeCreated: 1534504082 +licenseType: Free +PluginImporter: + serializedVersion: 2 + iconMap: {} + executionOrder: {} + isPreloaded: 0 + isOverridable: 0 + platformData: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: From b67daee7c5f7211a3b52cb4723f29f8d5438a7e7 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 15:52:11 +0200 Subject: [PATCH 390/567] Add support for 2018 4.6 profile --- GitHub.Unity.sln | 13 +++ common/packaging.targets | 13 +-- src/GitHub.Api/GitHub.Api.45.csproj | 34 ++------ src/GitHub.Api/Helpers/TaskHelpers.cs | 10 ++- src/GitHub.Api/Properties/AssemblyInfo.cs | 3 +- .../Extensions/ExceptionExtensions.cs | 2 +- .../Editor/GitHub.Unity/ExtensionLoader | 13 --- .../ExtensionLoader/ExtensionLoader.asmdef | 8 ++ .../ExtensionLoader/ExtensionLoader.cs | 82 +++++++++++++++++++ .../GitHub.Unity/Properties/AssemblyInfo.cs | 4 +- .../CopyLibrariesToDevelopmentFolder.csproj | 7 +- .../CopyLibrariesToPackageProject.csproj | 19 +++-- 12 files changed, 145 insertions(+), 63 deletions(-) delete mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs diff --git a/GitHub.Unity.sln b/GitHub.Unity.sln index 367026b8e..c9da69500 100644 --- a/GitHub.Unity.sln +++ b/GitHub.Unity.sln @@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebServer", "src\tests\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityTests", "src\UnityExtension\Assets\Editor\UnityTests\UnityTests.csproj", "{462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtensionLoader", "src\UnityExtension\Assets\Editor\GitHub.Unity\ExtensionLoader\ExtensionLoader.csproj", "{6B0EAB30-511A-44C1-87FE-D9AB7E34D115}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +153,14 @@ Global {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.dev|Any CPU.Build.0 = Debug|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5}.Release|Any CPU.Build.0 = Release|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.dev|Any CPU.ActiveCfg = dev|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.dev|Any CPU.Build.0 = dev|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -166,4 +176,7 @@ Global {3DD3451C-30FA-4294-A3A9-1E080342F867} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} {462CDBD4-0DDA-4854-1B13-CFDACBFB66F5} = {D17F1B4C-42DC-4E78-BCEF-9F239A084C4D} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {66BD4D50-3779-4912-9596-2C838BF24911} + EndGlobalSection EndGlobal diff --git a/common/packaging.targets b/common/packaging.targets index 77f866e20..06609f4e6 100644 --- a/common/packaging.targets +++ b/common/packaging.targets @@ -7,8 +7,9 @@ @@ -25,13 +26,13 @@ Condition="!$([System.String]::Copy('%(Filename)').Contains('deleteme')) and !$([System.String]::Copy('%(Extension)').Contains('xml')) and !$([System.String]::Copy('%(Extension)').Contains('pdb')) and !$([System.String]::Copy('%(Extension)').Contains('dll.mdb'))" /> - + - + diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj index 78269c5b4..dd7be03ae 100644 --- a/src/GitHub.Api/GitHub.Api.45.csproj +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -1,4 +1,4 @@ - + @@ -15,14 +15,14 @@ 6 - ..\UnityExtension\Assets\Editor\build + ..\UnityExtension\Assets\Editor\build\ true full false - DEBUG;TRACE;$(BuildDefs) + DEBUG;TRACE;$(BuildDefs);NET_4_6 prompt 4 false @@ -32,7 +32,7 @@ pdbonly true - TRACE;$(BuildDefs) + TRACE;$(BuildDefs);NET_4_6 prompt 4 Release @@ -44,7 +44,7 @@ true full false - TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs);NET_4_6 prompt 4 false @@ -264,15 +264,10 @@ - - - - - PublicResXFileCodeGenerator Localization.Designer.cs @@ -285,24 +280,5 @@ --> - - - - - - - \ No newline at end of file diff --git a/src/GitHub.Api/Helpers/TaskHelpers.cs b/src/GitHub.Api/Helpers/TaskHelpers.cs index 33481c029..fb5695329 100644 --- a/src/GitHub.Api/Helpers/TaskHelpers.cs +++ b/src/GitHub.Api/Helpers/TaskHelpers.cs @@ -8,14 +8,18 @@ static class TaskHelpers { public static Task GetCompletedTask(T result) { +#if NET_4_6 + return Task.FromResult(result); +#else return TaskEx.FromResult(result); +#endif } public static Task ToTask(this Exception exception) { - TaskCompletionSource completionSource = new TaskCompletionSource(); - completionSource.TrySetException(exception); - return completionSource.Task; + TaskCompletionSource completionSource = new TaskCompletionSource(); + completionSource.TrySetException(exception); + return completionSource.Task; } } diff --git a/src/GitHub.Api/Properties/AssemblyInfo.cs b/src/GitHub.Api/Properties/AssemblyInfo.cs index 1c8fb5107..ecf892a01 100644 --- a/src/GitHub.Api/Properties/AssemblyInfo.cs +++ b/src/GitHub.Api/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("GitHub.Api")] -[assembly: AssemblyDescription("GitHub Api")] +[assembly: AssemblyDescription("GitHub for Unity API")] [assembly: Guid("4B424108-D0E8-4BF9-9B0C-4FB49E532AB9")] [assembly: InternalsVisibleTo("GitHub.Unity")] +[assembly: InternalsVisibleTo("GitHub.Unity.45")] diff --git a/src/GitHub.Logging/Extensions/ExceptionExtensions.cs b/src/GitHub.Logging/Extensions/ExceptionExtensions.cs index b22772799..9c4c0f71c 100644 --- a/src/GitHub.Logging/Extensions/ExceptionExtensions.cs +++ b/src/GitHub.Logging/Extensions/ExceptionExtensions.cs @@ -3,7 +3,7 @@ namespace GitHub.Logging { - static class ExceptionExtensions + public static class ExceptionExtensions { public static string GetExceptionMessage(this Exception ex) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader deleted file mode 100644 index 56291fa6a..000000000 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using UnityEditor; -using UnityEngine; -using GitHub.Logging; - -namespace GitHub.Unity -{ - [InitializeOnLoad] - public class EntryPoint : ScriptableObject - { - - } -} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef new file mode 100644 index 000000000..c400f84eb --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef @@ -0,0 +1,8 @@ +{ + "name": "ExtensionLoader", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [] +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs new file mode 100644 index 000000000..c32e52047 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs @@ -0,0 +1,82 @@ +using UnityEditor; +using UnityEngine; +using System.IO; +using System; + +namespace GitHub.Unity +{ + [InitializeOnLoad] + public class ExtensionLoader : ScriptableSingleton + { + [SerializeField] private bool initialized = true; + + public bool Initialized + { + get + { + return initialized; + } + set + { + initialized = value; + Save(true); + } + } + + private static bool inSourceMode = false; + private const string sourceModePath = "Assets/Editor/build/"; + private const string realPath = "Assets/Plugins/GitHub/Editor/"; + + private static string[] assemblies20 = { "System.Threading.dll", "AsyncBridge.Net35.dll", "ReadOnlyCollectionsInterfaces.dll", "GitHub.Api.dll", "GitHub.Unity.dll" }; + private static string[] assemblies45 = { "GitHub.Api.45.dll", "GitHub.Unity.45.dll" }; + + static ExtensionLoader() + { + EditorApplication.update += Initialize; + } + + private static void Initialize() + { + EditorApplication.update -= Initialize; + //if (!ExtensionLoader.instance.Initialized) + { + var scriptPath = Path.Combine(Application.dataPath, "Editor" + Path.DirectorySeparatorChar + "GitHub.Unity" + Path.DirectorySeparatorChar + "EntryPoint.cs"); + inSourceMode = File.Exists(scriptPath); + ToggleAssemblies(); + //ExtensionLoader.instance.Initialized = true; + } + + } + + private static void ToggleAssemblies() + { + var path = inSourceMode ? sourceModePath : realPath; +#if NET_4_6 + ToggleAssemblies(path, assemblies20, false); + ToggleAssemblies(path, assemblies45, true); +#else + ToggleAssemblies(path, assemblies45, false); + ToggleAssemblies(path, assemblies20, true); +#endif + } + + private static void ToggleAssemblies(string path, string[] assemblies, bool enable) + { + foreach (var file in assemblies) + { + var filepath = path + file; + PluginImporter importer = AssetImporter.GetAtPath(filepath) as PluginImporter; + if (importer == null) + { + Debug.LogFormat("GitHub for Unity: Could not find importer for {0}. Some functionality may fail.", filepath); + continue; + } + if (importer.GetCompatibleWithEditor() != enable) + { + importer.SetCompatibleWithEditor(enable); + importer.SaveAndReimport(); + } + } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs index effeac2f0..a320c4438 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Properties/AssemblyInfo.cs @@ -2,6 +2,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -[assembly: AssemblyTitle("GitHub.Api")] -[assembly: AssemblyDescription("GitHub Api")] +[assembly: AssemblyTitle("GitHub.Unity")] +[assembly: AssemblyDescription("GitHub for Unity")] [assembly: Guid("add7a18b-dd2a-4c22-a2c1-488964eff30a")] diff --git a/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj b/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj index 8e79d6159..40067b0aa 100644 --- a/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj +++ b/src/packaging/CopyLibrariesToDevelopmentFolder/CopyLibrariesToDevelopmentFolder.csproj @@ -9,9 +9,10 @@ Properties deleteme deleteme - v3.5 + v4.5 512 $(SolutionDir)src\UnityExtension\Assets\Editor\build + true @@ -20,6 +21,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -27,6 +29,7 @@ TRACE prompt 4 + false @@ -38,7 +41,7 @@ {b389adaf-62cc-486e-85b4-2d8b078df76B} - GitHub.Api + GitHub.Api.45 {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} diff --git a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj index cc951852d..abd019e66 100644 --- a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj +++ b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj @@ -9,9 +9,10 @@ Properties deleteme deleteme - v3.5 + v4.5 512 build + Release @@ -20,6 +21,7 @@ TRACE prompt 4 + false Debug @@ -28,6 +30,7 @@ TRACE prompt 4 + false @@ -36,12 +39,16 @@ {b389adaf-62cc-486e-85b4-2d8b078df76b} - GitHub.Api + GitHub.Api.45 {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging + + {add7a18b-dd2a-4c22-a2c1-488964eff30b} + GitHub.Unity.45 + {add7a18b-dd2a-4c22-a2c1-488964eff30a} GitHub.Unity @@ -65,16 +72,16 @@ $(SolutionDir)\lib\sfw\sfw.net.dll True - - $(SolutionDir)\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll - True - + + ExtensionLoader.cs + PreserveNewest + From 2fe11a453a8a612301a4459f36bdc4cbc200a4d1 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 16:13:50 +0200 Subject: [PATCH 391/567] Add comment about always running the profile detector --- .../Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs index c32e52047..8d41c3ef2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs @@ -38,6 +38,9 @@ static ExtensionLoader() private static void Initialize() { EditorApplication.update -= Initialize; + + // we're always doing this right now because if the plugin gets updated all the meta files will be disabled and we need to re-enable them + // we should probably detect if our assets change and re-run this instead of doing it every time //if (!ExtensionLoader.instance.Initialized) { var scriptPath = Path.Combine(Application.dataPath, "Editor" + Path.DirectorySeparatorChar + "GitHub.Unity" + Path.DirectorySeparatorChar + "EntryPoint.cs"); From bb03bf1d7c4cb1e8297f22fd56588e6d78c6b664 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 16:19:12 +0200 Subject: [PATCH 392/567] Bump version to 1.0.3 --- common/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 04ce44d45..2a2d4185b 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -32,8 +32,8 @@ namespace System { internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics - internal const string VersionForAssembly = "1.0.2"; + internal const string VersionForAssembly = "1.0.3"; // Actual real version - internal const string Version = "1.0.2"; + internal const string Version = "1.0.3"; } } From f62212827149f39b061c4b754514c77bd17aa5dc Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 16:26:03 +0200 Subject: [PATCH 393/567] Ooops these kinda need to be there too --- .../ExtensionLoader/ExtensionLoader.csproj | 77 ++++++ .../GitHub.Unity/GitHub.Unity.45.csproj | 226 ++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj new file mode 100644 index 000000000..50ebe9826 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {6B0EAB30-511A-44C1-87FE-D9AB7E34D115} + Library + Properties + GitHub.Unity + ExtensionLoader + v3.5 + 512 + ..\..\..\..\obj\ + ..\..\..\..\obj\ + + + + true + full + false + DEBUG;TRACE;$(BuildDefs) + prompt + 4 + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs) + prompt + 4 + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs) + prompt + 4 + 4 + false + false + true + + + + + + $(UnityDir)Managed\UnityEditor.dll + False + + + $(UnityDir)Managed\UnityEngine.dll + False + + + + + + + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj new file mode 100644 index 000000000..0f2f0f5ad --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj @@ -0,0 +1,226 @@ + + + + + Debug + AnyCPU + {ADD7A18B-DD2A-4C22-A2C1-488964EFF30B} + Library + Properties + GitHub.Unity + GitHub.Unity.45 + v4.5 + 512 + $(SolutionDir)\unity\TestProject\Assets\Plugins\GitHub\Editor + ..\..\..\obj\ + + + + true + full + false + DEBUG;TRACE;$(BuildDefs);NET_4_6 + prompt + 4 + 4 + false + false + true + + + pdbonly + true + TRACE;$(BuildDefs);NET_4_6 + prompt + 4 + 4 + Release + false + false + true + + + true + full + false + TRACE;DEBUG;DEVELOPER_BUILD;$(BuildDefs);NET_4_6 + prompt + 4 + 4 + false + false + true + + + + + + + + + $(UnityDir)Managed\UnityEditor.dll + False + + + $(UnityDir)Managed\UnityEngine.dll + False + + + + + {b389adaf-62cc-486e-85b4-2d8b078df76B} + GitHub.Api.45 + + + {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} + GitHub.Logging + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eula.txt + PreserveNewest + + + credits.txt + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 8a467f9f0027a7a65c51ccfe3de5f95d4baf8cf1 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 16:40:54 +0200 Subject: [PATCH 394/567] Need to copy some stuff manually --- .../CopyLibrariesToPackageProject.csproj | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj index abd019e66..35f9b738c 100644 --- a/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj +++ b/src/packaging/CopyLibrariesToPackageProject/CopyLibrariesToPackageProject.csproj @@ -59,14 +59,6 @@ GitHub.Unity True - - $(SolutionDir)\packages\AsyncBridge.Net35.0.2.3333.0\lib\net35-Client\AsyncBridge.Net35.dll - True - - - $(SolutionDir)\packages\ReadOnlyCollectionInterfaces.1.0.0\lib\NET20\ReadOnlyCollectionsInterfaces.dll - True - $(SolutionDir)\lib\sfw\sfw.net.dll @@ -82,6 +74,18 @@ ExtensionLoader.cs PreserveNewest + + ReadOnlyCollectionsInterfaces.dll + PreserveNewest + + + AsyncBridge.Net35.dll + PreserveNewest + + + System.Threading.dll + PreserveNewest + From c40b281a9dd429699f29d7afbdeac719619184d4 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 16:43:37 +0200 Subject: [PATCH 395/567] Add some more meta files --- .../Plugins/GitHub/Editor/GitHub.Api.45.dll.mdb.meta | 8 ++++++++ .../Plugins/GitHub/Editor/GitHub.Unity.45.dll.mdb.meta | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.mdb.meta create mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.mdb.meta diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.mdb.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.mdb.meta new file mode 100644 index 000000000..d484c54df --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.45.dll.mdb.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 181f65fb096cedd4493bc9971257d8b1 +timeCreated: 1534516893 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.mdb.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.mdb.meta new file mode 100644 index 000000000..bc30ebed8 --- /dev/null +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.45.dll.mdb.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 877ef3585573ce44eab899298be23158 +timeCreated: 1534516893 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: From dd5b3d3c4924d701a319fcc1634b6c01e2a85172 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 17 Aug 2018 17:00:17 +0200 Subject: [PATCH 396/567] Reimport any changed assets all at once --- src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs | 1 + .../Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs index 01bcdc2f1..b1d83ca2c 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/EntryPoint.cs @@ -1,5 +1,6 @@ using GitHub.Logging; using System; +using System.IO; using UnityEditor; using UnityEngine; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs index 8d41c3ef2..770a49ec5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs @@ -47,6 +47,7 @@ private static void Initialize() inSourceMode = File.Exists(scriptPath); ToggleAssemblies(); //ExtensionLoader.instance.Initialized = true; + AssetDatabase.SaveAssets(); } } From 4bfcee13554ef84b8466cc4e4ef5ddd07626a1dc Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 20 Aug 2018 12:55:51 -0400 Subject: [PATCH 397/567] Making more classes public --- src/GitHub.Api/OutputProcessors/ProcessManager.cs | 2 +- src/GitHub.Api/Platform/ProcessEnvironment.cs | 2 +- src/GitHub.Api/Tasks/TaskManager.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/OutputProcessors/ProcessManager.cs b/src/GitHub.Api/OutputProcessors/ProcessManager.cs index f67c6a6c2..3cf767a91 100644 --- a/src/GitHub.Api/OutputProcessors/ProcessManager.cs +++ b/src/GitHub.Api/OutputProcessors/ProcessManager.cs @@ -7,7 +7,7 @@ namespace GitHub.Unity { - class ProcessManager : IProcessManager + public class ProcessManager : IProcessManager { private static readonly ILogging logger = LogHelper.GetLogger(); diff --git a/src/GitHub.Api/Platform/ProcessEnvironment.cs b/src/GitHub.Api/Platform/ProcessEnvironment.cs index b1609faa3..685a07b60 100644 --- a/src/GitHub.Api/Platform/ProcessEnvironment.cs +++ b/src/GitHub.Api/Platform/ProcessEnvironment.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { - class ProcessEnvironment : IProcessEnvironment + public class ProcessEnvironment : IProcessEnvironment { protected IEnvironment Environment { get; private set; } protected ILogging Logger { get; private set; } diff --git a/src/GitHub.Api/Tasks/TaskManager.cs b/src/GitHub.Api/Tasks/TaskManager.cs index 5bff94012..51ad03d4e 100644 --- a/src/GitHub.Api/Tasks/TaskManager.cs +++ b/src/GitHub.Api/Tasks/TaskManager.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { - class TaskManager : ITaskManager + public class TaskManager : ITaskManager { private static readonly ILogging logger = LogHelper.GetLogger(); From 849e2b4072dcc26fa57b069faa44fb85744158b6 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 22 Aug 2018 10:23:57 -0400 Subject: [PATCH 398/567] Allowing the User to be constructed without a CacheContainer --- src/GitHub.Api/Git/Repository.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Git/Repository.cs b/src/GitHub.Api/Git/Repository.cs index 38b499f5e..61935cccc 100644 --- a/src/GitHub.Api/Git/Repository.cs +++ b/src/GitHub.Api/Git/Repository.cs @@ -423,9 +423,12 @@ public class User : IUser public User(ICacheContainer cacheContainer) { - this.cacheContainer = cacheContainer; - cacheContainer.CacheInvalidated += (type) => { if (type == CacheType.GitUser) GitUserCacheOnCacheInvalidated(); }; - cacheContainer.CacheUpdated += (type, dt) => { if (type == CacheType.GitUser) CacheHasBeenUpdated(dt); }; + if (cacheContainer != null) + { + this.cacheContainer = cacheContainer; + cacheContainer.CacheInvalidated += (type) => { if (type == CacheType.GitUser) GitUserCacheOnCacheInvalidated(); }; + cacheContainer.CacheUpdated += (type, dt) => { if (type == CacheType.GitUser) CacheHasBeenUpdated(dt); }; + } } public void CheckAndRaiseEventsIfCacheNewer(CacheType cacheType, CacheUpdateEvent cacheUpdateEvent) => cacheContainer.CheckAndRaiseEventsIfCacheNewer(CacheType.GitUser, cacheUpdateEvent); From 7d1c6f01153da998c84709e61cfb5b3808bfd331 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 22 Aug 2018 10:24:16 -0400 Subject: [PATCH 399/567] Creating the TaskManager instance on usage of the property --- src/GitHub.Api/Tasks/TaskManager.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/Tasks/TaskManager.cs b/src/GitHub.Api/Tasks/TaskManager.cs index 51ad03d4e..7a3e80749 100644 --- a/src/GitHub.Api/Tasks/TaskManager.cs +++ b/src/GitHub.Api/Tasks/TaskManager.cs @@ -17,7 +17,19 @@ public class TaskManager : ITaskManager public CancellationToken Token { get { return cts.Token; } } private static ITaskManager instance; - public static ITaskManager Instance => instance; + public static ITaskManager Instance + { + get + { + if (instance == null) + { + instance = new TaskManager(); + } + + return instance; + } + } + private ProgressReporter progressReporter = new ProgressReporter(); public event Action OnProgress From 8b737687075c74d5eb6ed468474d56d296cb96cd Mon Sep 17 00:00:00 2001 From: dragonfyre23 <9019960+dragonfyre23@users.noreply.github.com> Date: Wed, 22 Aug 2018 21:38:55 -0400 Subject: [PATCH 400/567] Update getting-started.md Removed unfinished 4th step for # setting up a new repository Deleted # connecting to an existing repository I'm not sure when this case would arise as the proper workflow should have the unity project with plugin in a repository, never the project alone. Added steps for # Connecting to an Existing Repository that already has the GitHub for Unity package These are based on what worked for me today. --- docs/using/getting-started.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/using/getting-started.md b/docs/using/getting-started.md index 11f2b008f..ef323e95d 100644 --- a/docs/using/getting-started.md +++ b/docs/using/getting-started.md @@ -19,8 +19,14 @@ And you should see the GitHub spinner: - History: A history of commits with title, time stamp, and commit author - Branches: A list of local and remote branches with the ability to create new branches, switch branches, or checkout remote branches - Settings: your git configuration (pulled from your local git credentials if they have been previously set), your repository configuration (you can manually put the URL to any remote repository here instead of using the Publish button to publish to GitHub), a list of locked files, your git installation details, and general settings to help us better help you if you get stuck -4. You can - -# Connecting to an Existing Repository # Connecting to an Existing Repository that already has the GitHub for Unity package +If you have an existing Unity project which already has the GitHub for Unity plugin installed and is connected to a remote repository (with the initial commit), you can access the project from another machine. +1. Clone the repository on the second machine (either through command line or with GitHub Desktop https://desktop.github.com/). +2. Open Unity Editor and click Open. +3. Browse to the location of the cloned repository and select the parent directory. +4. When the project opens the plugin should be enabled. If you don't see the GitHub tab, enable it as above. +5. Enter your credentials and verify that the system git location and remote origin link are correct. +6. Submit and push the initial commit. +For further questions see Issue#891 + From ba68eb09168b0c005ff5b0eb104453004179fed6 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 27 Aug 2018 10:20:18 -0400 Subject: [PATCH 401/567] Adding documentation on how to use the api --- docs/using/using-the-api.md | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/using/using-the-api.md diff --git a/docs/using/using-the-api.md b/docs/using/using-the-api.md new file mode 100644 index 000000000..ab75cab55 --- /dev/null +++ b/docs/using/using-the-api.md @@ -0,0 +1,75 @@ +# Using the API + +GitHub for Unity provides access to a git client to help users create their own tools to assist in their workflow. + +Users can separate the user interface from the API by removing `GitHub.Unity.dll`. All other libraries are required by the API. + +## Creating an instance of `GitClient` +```cs +var defaultEnvironment = new DefaultEnvironment(); +defaultEnvironment.Initialize(null, NPath.Default, NPath.Default, NPath.Default, Application.dataPath.ToNPath()); + +var processEnvironment = new ProcessEnvironment(defaultEnvironment); +var processManager = new ProcessManager(defaultEnvironment, processEnvironment, TaskManager.Instance.Token); + +var gitClient = new GitClient(defaultEnvironment, processManager, TaskManager.Instance.Token); +``` + +## Full Example +This example creates a window that has a single button which commits all changes. +```cs +using System; +using System.Globalization; +using GitHub.Unity; +using UnityEditor; +using UnityEngine; + +public class CustomGitEditor : EditorWindow +{ + [MenuItem("Window/Custom Git")] + public static void ShowWindow() + { + EditorWindow.GetWindow(typeof(CustomGitEditor)); + } + + [NonSerialized] private GitClient gitClient; + + public void OnEnable() + { + InitGitClient(); + } + + private void InitGitClient() + { + if (gitClient != null) return; + + Debug.Log("Init GitClient"); + + var defaultEnvironment = new DefaultEnvironment(); + defaultEnvironment.Initialize(null, NPath.Default, NPath.Default, + NPath.Default, Application.dataPath.ToNPath()); + + var processEnvironment = new ProcessEnvironment(defaultEnvironment); + var processManager = new ProcessManager(defaultEnvironment, processEnvironment, TaskManager.Instance.Token); + + gitClient = new GitClient(defaultEnvironment, processManager, TaskManager.Instance.Token); + } + + void OnGUI() + { + GUILayout.Label("Custom Git Window", EditorStyles.boldLabel); + + if (GUILayout.Button("Commit Stuff")) + { + var message = DateTime.Now.ToString(CultureInfo.InvariantCulture); + var body = string.Empty; + + gitClient.AddAll() + .Then(gitClient.Commit(message, body)) + .Start(); + } + } +} +``` + + From c71161a92c7b22c79996252960c6bb70a10794be Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 27 Aug 2018 10:31:22 -0400 Subject: [PATCH 402/567] Adding more internal documentation links --- docs/using/quick-guide.md | 13 ++++++++++++- docs/using/working-with-changes.md | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/using/quick-guide.md b/docs/using/quick-guide.md index 5f95500a1..5164e1832 100644 --- a/docs/using/quick-guide.md +++ b/docs/using/quick-guide.md @@ -5,6 +5,10 @@ These documents contain more details on how to use the GitHub for Unity plugin: - **[Installing and Updating the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/how-to-install-and-update.md)** - **[Getting Started with the GitHub for Unity package](https://github.com/github-for-unity/Unity/blob/master/docs/using/getting-started.md)** +- **[Authenticating to GitHub](https://github.com/github-for-unity/Unity/blob/master/docs/using/authenticating-to-github.md)** +- **[Managing Branches](https://github.com/github-for-unity/Unity/blob/master/docs/using/managing-branches.md)** +- **[Locking Files](https://github.com/github-for-unity/Unity/blob/master/docs/using/locking-files.md)** +- **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md)** ## Table of Contents @@ -118,6 +122,8 @@ To set up credentials in Git so you can push and pull, you can sign in to GitHub ![Authentication screenshot](https://user-images.githubusercontent.com/121322/27644895-8f22f904-5bd9-11e7-8a93-e6bfe0c24a74.png) +For more information on Authentication: - **[Authenticating to GitHub](https://github.com/github-for-unity/Unity/blob/master/docs/using/authenticating-to-github.md)** + ### Publish a new repository 1. Go to [github.com](https://github.com) and create a new empty repository - do not add a license, readme or other files during the creation process. @@ -132,6 +138,8 @@ You can see which files have been changed and commit them through the Changes ta ![Changes tab screenshot](https://user-images.githubusercontent.com/121322/27644933-ab00af72-5bd9-11e7-84c3-edec495f87f5.png) +For more information on working with changes: - **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md#commit-changes)** + ### Pushing/pulling your work - History tab The history tab includes a `Push` button to push your work to the server. Make sure you have a remote url configured in the `Settings` tab so that you can push and pull your work. @@ -140,6 +148,9 @@ To receive updates from the server by clicking on the `Pull` button. You cannot ![History tab screenshot](https://user-images.githubusercontent.com/121322/27644965-c1109bba-5bd9-11e7-9257-4fa38f5c67d1.png) + +For more information on working with changes: - **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md#pulling-changes)** + ### Branches tab ![Branches tab screenshot](https://user-images.githubusercontent.com/121322/27644978-cd3c5622-5bd9-11e7-9dcb-6ae5d5c7dc8a.png) @@ -150,4 +161,4 @@ You can configure your user data in the Settings tab, along with the path to the Locked files will appear in a list in the Settings tab. You can see who has locked a file and release file locks after you've pushed your work. -![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) +![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) \ No newline at end of file diff --git a/docs/using/working-with-changes.md b/docs/using/working-with-changes.md index 7b8487861..228e4bd3f 100644 --- a/docs/using/working-with-changes.md +++ b/docs/using/working-with-changes.md @@ -1,6 +1,6 @@ # Working with changes -## Commit changes to GitHub +## Commit changes All changes made to a repository will show up under the **Changes** view. From f919a7a3bbb56d07b6561136d8973eb4130a59da Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 27 Aug 2018 10:46:16 -0400 Subject: [PATCH 403/567] Linking to documentation --- docs/using/quick-guide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/using/quick-guide.md b/docs/using/quick-guide.md index 5164e1832..f089be9d6 100644 --- a/docs/using/quick-guide.md +++ b/docs/using/quick-guide.md @@ -9,6 +9,7 @@ These documents contain more details on how to use the GitHub for Unity plugin: - **[Managing Branches](https://github.com/github-for-unity/Unity/blob/master/docs/using/managing-branches.md)** - **[Locking Files](https://github.com/github-for-unity/Unity/blob/master/docs/using/locking-files.md)** - **[Working with Changes](https://github.com/github-for-unity/Unity/blob/master/docs/using/working-with-changes.md)** +- **[Using the Api](https://github.com/github-for-unity/Unity/blob/master/docs/using/using-the-api.md)** ## Table of Contents From 84e0c0481059de73d8c073f4f6b20522891cf189 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 27 Aug 2018 10:51:18 -0400 Subject: [PATCH 404/567] Adding documentation to update GitHub for Unity --- docs/using/how-to-install-and-update.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md index 5f357cf79..610f2a722 100644 --- a/docs/using/how-to-install-and-update.md +++ b/docs/using/how-to-install-and-update.md @@ -43,4 +43,6 @@ Once you've downloaded the package file, you can quickly install it within Unity screen shot 2018-05-18 at 7 13 34 am # Updating the GitHub for Unity Package -_COMING SOON_ + +- If you are running Unity and wish to update GitHub for Unity (unless explicitly stated), be sure that the files in `x64` and `x86` are not selected. +- Otherwise, it's best to stop Unity and delete GitHub for Unity from your project. Startup Unity and run the package installer like normal. Allowing it to restore everything. \ No newline at end of file From d921f9e7e04ca2840dbd6cc453bd8e9a6016b701 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 27 Aug 2018 10:54:13 -0400 Subject: [PATCH 405/567] Adding an image --- docs/using/how-to-install-and-update.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/using/how-to-install-and-update.md b/docs/using/how-to-install-and-update.md index 610f2a722..fb9dd93d8 100644 --- a/docs/using/how-to-install-and-update.md +++ b/docs/using/how-to-install-and-update.md @@ -45,4 +45,7 @@ Once you've downloaded the package file, you can quickly install it within Unity # Updating the GitHub for Unity Package - If you are running Unity and wish to update GitHub for Unity (unless explicitly stated), be sure that the files in `x64` and `x86` are not selected. -- Otherwise, it's best to stop Unity and delete GitHub for Unity from your project. Startup Unity and run the package installer like normal. Allowing it to restore everything. \ No newline at end of file + + ![image](https://user-images.githubusercontent.com/417571/44666907-6e6d5a80-a9e7-11e8-8f97-b3b52250a75d.png) + +- Otherwise, it's best to stop Unity and delete GitHub for Unity from your project. Startup Unity and run the package installer like normal. Allowing it to restore everything. From bcc888eda6d28bbc7b8df27ffb17df711269323e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 27 Aug 2018 11:05:31 -0400 Subject: [PATCH 406/567] Updating documentation on cloning an existing repository? --- docs/using/getting-started.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/using/getting-started.md b/docs/using/getting-started.md index ef323e95d..4c5892a96 100644 --- a/docs/using/getting-started.md +++ b/docs/using/getting-started.md @@ -20,13 +20,9 @@ And you should see the GitHub spinner: - Branches: A list of local and remote branches with the ability to create new branches, switch branches, or checkout remote branches - Settings: your git configuration (pulled from your local git credentials if they have been previously set), your repository configuration (you can manually put the URL to any remote repository here instead of using the Publish button to publish to GitHub), a list of locked files, your git installation details, and general settings to help us better help you if you get stuck -# Connecting to an Existing Repository that already has the GitHub for Unity package -If you have an existing Unity project which already has the GitHub for Unity plugin installed and is connected to a remote repository (with the initial commit), you can access the project from another machine. -1. Clone the repository on the second machine (either through command line or with GitHub Desktop https://desktop.github.com/). -2. Open Unity Editor and click Open. -3. Browse to the location of the cloned repository and select the parent directory. -4. When the project opens the plugin should be enabled. If you don't see the GitHub tab, enable it as above. -5. Enter your credentials and verify that the system git location and remote origin link are correct. -6. Submit and push the initial commit. -For further questions see Issue#891 - +# Cloning an Existing Repository +GitHub for Unity does not have the functionality to clone projects (yet!). +1. Clone the repository (either through command line or with GitHub Desktop https://desktop.github.com/). +2. Open the project in Unity. +3. Install GitHub for Unity if it is not already installed. +4. The GitHub plugin should load with all functionality enabled. \ No newline at end of file From 8bb5cb31196f66a8c0c41de0ba66e4b5d0665d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Here=C3=B1=C3=BA?= Date: Fri, 31 Aug 2018 13:17:40 -0300 Subject: [PATCH 407/567] Minor formattin (proposals) --- docs/using/quick-guide.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/using/quick-guide.md b/docs/using/quick-guide.md index f089be9d6..2011ee40f 100644 --- a/docs/using/quick-guide.md +++ b/docs/using/quick-guide.md @@ -39,12 +39,12 @@ These documents contain more details on how to use the GitHub for Unity plugin: ### Requirements - Unity 5.4 or higher - - There's currently an blocker issue opened for 5.3 support, so we know it doesn't run there. Personal edition is fine. + - There's currently a blocker issue opened for 5.3 support, so we know it doesn't run there. Personal edition is fine. - Git and Git LFS 2.x #### Git on macOS -The current release has limited macOS support. macOS users will need to install the latest [Git](https://git-scm.com/downloads) and [Git LFS](https://git-lfs.github.com/) manually, and make sure these are on the path. You can configure the Git location in the Settings tab on the GitHub window. +The current release has limited macOS support. macOS users will need to install the latest [Git](https://git-scm.com/downloads) and [Git LFS](https://git-lfs.github.com/) manually, and make sure these are on the path. You can configure the Git location in the `Settings` tab on the GitHub window. The easiest way of installing git and git lfs is to install [Homebrew](https://brew.sh/) and then do `brew install git git-lfs`. @@ -73,10 +73,9 @@ Once the extension is installed, you can open a command line with the same Git a ### Installation This extensions needs to be installed (and updated) for each Unity project that you want to version control. -First step is to download the latest package from [the releases page](https://github.com/github-for-unity/Unity/releases); -it will be saved as a file with the extension `.unitypackage`. +First step is to download the latest package from [the releases page](https://github.com/github-for-unity/Unity/releases); it will be saved as a file with the extension `.unitypackage`. To install it, open Unity, then open the project you want to version control, and then double click on the downloaded package. -Alternatively, import the package by clicking Assets, Import Package, Custom Package, then select the downloaded package. +Alternatively, import the package by clicking `Assets`, `Import Package`, `Custom Package`, then select the downloaded package. #### Log files @@ -102,7 +101,7 @@ further. ### Opening the GitHub window -You can access the GitHub window by going to Windows -> GitHub. The window opens by default next to the Inspector window. +You can access the GitHub window by going to `Windows` -> `GitHub`. The window opens by default next to the Inspector window. ### Initialize Repository @@ -135,7 +134,7 @@ For more information on Authentication: - **[Authenticating to GitHub](https://g ### Commiting your work - Changes tab -You can see which files have been changed and commit them through the Changes tab. `.meta` files will show up in relation to their files on the tree, so you can select a file for comitting and automatically have their `.meta` +You can see which files have been changed and commit them through the `Changes` tab. `.meta` files will show up in relation to their files on the tree, so you can select a file for comitting and automatically have their `.meta` ![Changes tab screenshot](https://user-images.githubusercontent.com/121322/27644933-ab00af72-5bd9-11e7-84c3-edec495f87f5.png) @@ -158,8 +157,8 @@ For more information on working with changes: - **[Working with Changes](https:/ ### Settings tab -You can configure your user data in the Settings tab, along with the path to the Git installation. +You can configure your user data in the `Settings` tab, along with the path to the Git installation. Locked files will appear in a list in the Settings tab. You can see who has locked a file and release file locks after you've pushed your work. -![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) \ No newline at end of file +![Settings tab screenshot](https://user-images.githubusercontent.com/121322/27644993-d9d325a0-5bd9-11e7-86f5-beee00e9e8b8.png) From 25897f8a845cd259e3597a27a867872134a61430 Mon Sep 17 00:00:00 2001 From: konh Date: Mon, 3 Sep 2018 21:25:39 +0700 Subject: [PATCH 408/567] Spool files in GitClient.Remove() method to prevent possible issues when trying to remove lots of files; --- src/GitHub.Api/Git/GitClient.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index c345a47a7..d3bc60351 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -200,7 +200,7 @@ public interface IGitClient ITask DiscardAll(IOutputProcessor processor = null); /// - /// Executes `git reset HEAD` command to remove files from the git index. + /// Executes at least one `git reset HEAD` command to remove files from the git index. /// /// The files to remove /// A custom output processor instance @@ -549,8 +549,22 @@ public ITask DiscardAll(IOutputProcessor processor = null) public ITask Remove(IList files, IOutputProcessor processor = null) { - return new GitRemoveFromIndexTask(files, cancellationToken, processor) - .Configure(processManager); + GitRemoveFromIndexTask last = null; + foreach (var batch in files.Spool(5000)) + { + var current = new GitRemoveFromIndexTask(batch, cancellationToken, processor).Configure(processManager); + if (last == null) + { + last = current; + } + else + { + last.Then(current); + last = current; + } + } + + return last; } /// From b2b22da1861acf0324e3dba8b3e79483c2d7e694 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 17 Sep 2018 14:47:47 -0400 Subject: [PATCH 409/567] Correcting statement to check status of initialization --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index d228a361e..95cdd8043 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -26,7 +26,7 @@ class ProjectWindowInterface : AssetPostprocessor private static CacheUpdateEvent lastRepositoryStatusChangedEvent; private static CacheUpdateEvent lastLocksChangedEvent; private static IRepository Repository { get { return manager != null ? manager.Environment.Repository : null; } } - private static bool IsInitialized { get { return Repository != null && Repository.CurrentRemote.HasValue; } } + private static bool IsInitialized { get { return Repository != null; } } public static void Initialize(IApplicationManager theManager) { From 09102c31ac092720e5390f774eff8217c51b5336 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 17 Sep 2018 15:12:18 -0400 Subject: [PATCH 410/567] Refreshing the asset database on status entry changed events --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 95cdd8043..8a6291439 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -67,6 +67,7 @@ private static void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdat if (!lastRepositoryStatusChangedEvent.Equals(cacheUpdateEvent)) { lastRepositoryStatusChangedEvent = cacheUpdateEvent; + AssetDatabase.Refresh(); entries.Clear(); entries.AddRange(Repository.CurrentChanges); OnStatusUpdate(); From c346bb11f8894189798e4c97470b054e548c9991 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 25 Sep 2018 21:21:21 +0200 Subject: [PATCH 411/567] Bump version to 1.1.0 --- common/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index 2a2d4185b..aae47c51d 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -32,8 +32,8 @@ namespace System { internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics - internal const string VersionForAssembly = "1.0.3"; + internal const string VersionForAssembly = "1.1.0"; // Actual real version - internal const string Version = "1.0.3"; + internal const string Version = "1.1.0"; } } From ee015351bd5f7c1932a1e2db99ad7dc6d14cdebb Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 19:39:11 +0200 Subject: [PATCH 412/567] Nuget 2 is borked? --- nuget.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget.config b/nuget.config index 1a5bd9cf3..bad252e01 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,7 @@ - + From b3297dadfe519de0235f4799ae050f5ee902627f Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 19:31:30 +0200 Subject: [PATCH 413/567] Update octorun packager to also update the version file --- create-octorun-zip.sh | 2 +- submodules/packaging | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/create-octorun-zip.sh b/create-octorun-zip.sh index e722d7cb2..4eb568d33 100755 --- a/create-octorun-zip.sh +++ b/create-octorun-zip.sh @@ -1,3 +1,3 @@ #!/bin/sh -eu DIR=$(pwd) -submodules/packaging/octorun/run.sh --path $DIR/octorun --out $DIR/src/GitHub.Api/Resources +submodules/packaging/octorun/run.sh --path $DIR/octorun --out $DIR/src/GitHub.Api/Resources --source $DIR/src/GitHub.Api/Installer diff --git a/submodules/packaging b/submodules/packaging index 43af07792..07785297f 160000 --- a/submodules/packaging +++ b/submodules/packaging @@ -1 +1 @@ -Subproject commit 43af077928bf3cfa06cfaac507b801efc950d475 +Subproject commit 07785297f6e1ec67dfdef0abffe5e0b44e12a9b2 From 2619aa4dd925ed9323bae7424bfcf57cad297b83 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 21 Sep 2018 15:30:01 +0200 Subject: [PATCH 414/567] Create unitypackage packages in CI --- appveyor.yml | 63 +++++++++++++++++++------------- common/packaging.targets | 2 +- lib/pdb2mdb.exe | 3 ++ unity/PackageProject/preview.png | 3 ++ 4 files changed, 44 insertions(+), 27 deletions(-) create mode 100644 lib/pdb2mdb.exe create mode 100755 unity/PackageProject/preview.png diff --git a/appveyor.yml b/appveyor.yml index 3736c7189..4e278d01d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,35 +1,26 @@ -version: '{build}.{branch}' +version: '1.1.0.{build}' environment: GHFU_KEY: secure: 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 -clone_script: -- ps: >- - if(-not $env:appveyor_pull_request_number) { - git lfs clone -q -n --branch=$env:appveyor_repo_branch https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder - git checkout -qf $env:appveyor_repo_commit - } else { - git lfs clone -q -n https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder - git fetch -q origin +refs/pull/$env:appveyor_pull_request_number/merge: - git lfs fetch origin FETCH_HEAD - git checkout -qf FETCH_HEAD - } - - Set-Location $env:appveyor_build_folder + matrix: + - node_version: '8' install: -- ps: >- - git submodule sync +- ps: | + $full_build = Test-Path env:GHFU_KEY + git submodule sync git submodule init - $full_build = Test-Path env:GHFU_KEY - if ($full_build) { + $env:BUILD_TYPE="full" $fileContent = "-----BEGIN RSA PRIVATE KEY-----`n" $fileContent += $env:GHFU_KEY.Replace(' ', "`n") $fileContent += "`n-----END RSA PRIVATE KEY-----`n" Set-Content c:\users\appveyor\.ssh\id_rsa $fileContent + Install-Product node $env:node_version } else { + $env:BUILD_TYPE="partial" git submodule deinit script $destdir = Join-Path $env:appveyor_build_folder 'lib' $destfile = Join-Path $destdir 'deps.zip' @@ -40,8 +31,8 @@ install: } git submodule update - nuget restore GitHub.Unity.sln +- if %BUILD_TYPE%==full cd submodules\packaging\unitypackage && node .\yarn.js install --prefer-offline assembly_info: patch: false @@ -60,10 +51,30 @@ test: categories: except: - DoNotRunOnAppVeyor -artifacts: -- path: unity\PackageProject - type: zip - name: github-for-unity-packageproject -- path: build\*.log -on_failure: - - ps: Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } +on_success: +- ps: | + if ($full_build) { + $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" + Get-ChildItem -Recurse "$($sourcedir)\*.pdb" | foreach { $_.fullname.substring(0, $_.fullname.length - $_.extension.length) } | foreach { Write-Output "Generating $($_).mdb"; & 'lib\pdb2mdb.exe' "$($_).dll" } + } +- if %BUILD_TYPE%==full cd %appveyor_build_folder%\submodules\packaging\unitypackage && node yarn.js start --path %appveyor_build_folder%\unity\PackageProject --out %appveyor_build_folder% --file github-for-unity-%appveyor_build_version% +- ps: | + if ($full_build) { + Set-Location $env:appveyor_build_folder + $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" + $zipfile="$($env:appveyor_build_folder)\github-for-unity-$($env:appveyor_build_version).zip" + $packagefile="$($env:appveyor_build_folder)\github-for-unity-$($env:appveyor_build_version).unitypackage" + $commitfile="$sourcedir\commit" + + Add-Content $commitfile $appveyor_repo_commit + + Write-Output "Zipping $sourcedir to $zipfile" + 7z a $zipfile $sourcedir + + Write-Output "Uploading $zipfile" + Push-AppveyorArtifact $zipfile + Push-AppveyorArtifact $packagefile + Push-AppveyorArtifact "$($packagefile).md5" + } +on_finish: +- ps: Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/common/packaging.targets b/common/packaging.targets index 06609f4e6..a5ddd41a5 100644 --- a/common/packaging.targets +++ b/common/packaging.targets @@ -23,7 +23,7 @@ + Condition="!$([System.String]::Copy('%(Filename)').Contains('deleteme')) and !$([System.String]::Copy('%(Extension)').Contains('xml'))" /> diff --git a/lib/pdb2mdb.exe b/lib/pdb2mdb.exe new file mode 100644 index 000000000..72547bf3c --- /dev/null +++ b/lib/pdb2mdb.exe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a72425f98bdb5923704946c4010485d471ce9e35611264f394637fb185424619 +size 369664 diff --git a/unity/PackageProject/preview.png b/unity/PackageProject/preview.png new file mode 100755 index 000000000..926bb3196 --- /dev/null +++ b/unity/PackageProject/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82a8bda13a627b1bdb25d6cfd2abeedbc190b8ab5ba8a67a785e929f6967fccb +size 9704 From 907833bdd6b65afd4226888a269676c658c2bc22 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Tue, 25 Sep 2018 21:44:23 +0200 Subject: [PATCH 415/567] Some more CI logging --- appveyor.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4e278d01d..e7809c0eb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,10 +6,23 @@ environment: - node_version: '8' install: -- ps: | +- ps: >- $full_build = Test-Path env:GHFU_KEY + $package = $full_build + + $message = "Building " + + if ($package) { $message += "and packaging "} + + if ($full_build) { $message += "(full build)" } else { $message += "(partial build)" } + + $message += " version " + $env:APPVEYOR_BUILD_NUMBER + " " + + Write-Host $message + git submodule sync + git submodule init if ($full_build) { @@ -31,7 +44,9 @@ install: } git submodule update + nuget restore GitHub.Unity.sln + - if %BUILD_TYPE%==full cd submodules\packaging\unitypackage && node .\yarn.js install --prefer-offline assembly_info: @@ -53,13 +68,13 @@ test: - DoNotRunOnAppVeyor on_success: - ps: | - if ($full_build) { + if ($package) { $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" Get-ChildItem -Recurse "$($sourcedir)\*.pdb" | foreach { $_.fullname.substring(0, $_.fullname.length - $_.extension.length) } | foreach { Write-Output "Generating $($_).mdb"; & 'lib\pdb2mdb.exe' "$($_).dll" } } - if %BUILD_TYPE%==full cd %appveyor_build_folder%\submodules\packaging\unitypackage && node yarn.js start --path %appveyor_build_folder%\unity\PackageProject --out %appveyor_build_folder% --file github-for-unity-%appveyor_build_version% - ps: | - if ($full_build) { + if ($package) { Set-Location $env:appveyor_build_folder $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" $zipfile="$($env:appveyor_build_folder)\github-for-unity-$($env:appveyor_build_version).zip" From bd66a20a7d476e84541e8dccebe9cd177b6eb328 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 19:11:57 +0200 Subject: [PATCH 416/567] Send the username along with the metrics --- octorun/src/bin/app-usage.js | 3 -- src/GitHub.Api/Application/ApiClient.cs | 2 +- .../Application/ApplicationManagerBase.cs | 2 +- src/GitHub.Api/Metrics/UsageModel.cs | 1 + src/GitHub.Api/Metrics/UsageTracker.cs | 28 ++++++++++++++++++- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/octorun/src/bin/app-usage.js b/octorun/src/bin/app-usage.js index e8caf544f..0fb32b691 100644 --- a/octorun/src/bin/app-usage.js +++ b/octorun/src/bin/app-usage.js @@ -44,9 +44,6 @@ if (fileContents && host) { 'Content-Type': 'application/json' } }; - if (config.token) { - options.headers['Authorization'] = 'token ' + config.token; - } var req = https.request(options, function (res) { var success = res.statusCode == 200; diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 065c86c85..01d189def 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -205,7 +205,7 @@ public void ContinueLogin(LoginResult loginResult, string code) .Start(); } - private GitHubUser GetCurrentUser() + public GitHubUser GetCurrentUser() { var keychainConnection = keychain.Connections.FirstOrDefault(x => x.Host == OriginalUrl); if (keychainConnection == null) diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 76f8d59a1..80f9fe666 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -48,7 +48,7 @@ protected void Initialize() ApplicationConfiguration.GitTimeout = UserSettings.Get(Constants.GitTimeoutKey, ApplicationConfiguration.GitTimeout); Platform.Initialize(ProcessManager, TaskManager); progress.OnProgress += progressReporter.UpdateProgress; - UsageTracker = new UsageTracker(TaskManager, GitClient, ProcessManager, UserSettings, Environment, InstanceId.ToString()); + UsageTracker = new UsageTracker(TaskManager, GitClient, ProcessManager, UserSettings, Environment, Platform.Keychain, InstanceId.ToString()); #if ENABLE_METRICS var metricsService = new MetricsService(ProcessManager, diff --git a/src/GitHub.Api/Metrics/UsageModel.cs b/src/GitHub.Api/Metrics/UsageModel.cs index 123146465..91a02c1d2 100644 --- a/src/GitHub.Api/Metrics/UsageModel.cs +++ b/src/GitHub.Api/Metrics/UsageModel.cs @@ -22,6 +22,7 @@ public class Dimensions public string UnityVersion { get; set; } public string Lang { get; set; } public string CurrentLang { get; set; } + public string GitHubUser { get; set; } } public class Measures diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 08c515b21..6c96803bf 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -94,6 +94,11 @@ private void SendUsage() return; } + var username = GetUsername(); + if (!String.IsNullOrEmpty(username)) { + extractReports.ForEach(x => x.Dimensions.GitHubUser = username); + } + try { MetricsService.PostUsage(extractReports); @@ -316,6 +321,11 @@ public virtual void UpdateLfsDiskUsage(int kilobytes) } } + protected virtual string GetUsername() + { + return ""; + } + public bool Enabled { get @@ -344,7 +354,9 @@ class UsageTracker : UsageTrackerSync { public UsageTracker(ITaskManager taskManager, IGitClient gitClient, IProcessManager processManager, ISettings userSettings, - IEnvironment environment, string instanceId) + IEnvironment environment, + IKeychain keychain, + string instanceId) : base(userSettings, new UsageLoader(environment.UserCachePath.Combine(Constants.UsageFile)), environment.UnityVersion, instanceId) @@ -353,6 +365,7 @@ public UsageTracker(ITaskManager taskManager, IGitClient gitClient, IProcessMana Environment = environment; GitClient = gitClient; ProcessManager = processManager; + Keychain = keychain; } protected override void CaptureRepoSize() @@ -377,6 +390,18 @@ protected override void CaptureRepoSize() catch {} } + protected override string GetUsername() + { + string username = ""; + try { + var apiClient = new ApiClient("", Keychain, ProcessManager, TaskManager, Environment); + var user = apiClient.GetCurrentUser(); + username = user.Login; + } catch { + } + return username; + } + public override void IncrementApplicationMenuMenuItemCommandLine() => TaskManager.Run(base.IncrementApplicationMenuMenuItemCommandLine); public override void IncrementAuthenticationViewButtonAuthentication() => TaskManager.Run(base.IncrementAuthenticationViewButtonAuthentication); public override void IncrementBranchesViewButtonCheckoutLocalBranch() => TaskManager.Run(base.IncrementBranchesViewButtonCheckoutLocalBranch); @@ -400,6 +425,7 @@ protected override void CaptureRepoSize() protected IEnvironment Environment { get; } protected IGitClient GitClient { get; } public IProcessManager ProcessManager { get; } + protected IKeychain Keychain { get; } } interface IUsageLoader From 902910f4f32ff3ab412fe4adfb3928ed455c72cf Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 20:00:40 +0200 Subject: [PATCH 417/567] Update octorun package for commit bd66a20a --- octorun/version | 2 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/octorun/version b/octorun/version index 820d50296..94ac57c5f 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -f497f7aa3d \ No newline at end of file +bd66a20a \ No newline at end of file diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 9c5364eb1..e1f92252b 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -85,7 +85,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "f497f7aa3d"; + public const string PackageVersion = "bd66a20a"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 791c35d52..5284a6d79 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7287864b7d86835175494c3743832f47a10481ab7fb1c257f871b808744e448 -size 1399629 +oid sha256:95a82d30f9e3a3f1eaec7d4a382983748d35320b87a2ec91134beb5d75738af3 +size 1399526 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 7e86214a0..3f4ad43f2 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -a41ad2fd5ceaacb20574a0fc2841e82d \ No newline at end of file +f6865e64072e9b65fa31ac9087fe1363 \ No newline at end of file From c623b519ff1f5ecc775f4d788c52cd2aace31620 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 18:34:46 +0200 Subject: [PATCH 418/567] Grab package version from SolutionInfo. Configure deployment artifacts. --- appveyor.yml | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index e7809c0eb..185eed79d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,16 +11,6 @@ install: $package = $full_build - $message = "Building " - - if ($package) { $message += "and packaging "} - - if ($full_build) { $message += "(full build)" } else { $message += "(partial build)" } - - $message += " version " + $env:APPVEYOR_BUILD_NUMBER + " " - - Write-Host $message - git submodule sync git submodule init @@ -46,8 +36,24 @@ install: git submodule update nuget restore GitHub.Unity.sln - - if %BUILD_TYPE%==full cd submodules\packaging\unitypackage && node .\yarn.js install --prefer-offline +- ps: >- + + Set-Location $env:appveyor_build_folder + + $version = Get-Content "$($env:appveyor_build_folder)\common\SolutionInfo.cs" | %{ $regex = "const string Version = `"([^`"]*)`""; if ($_ -match $regex) { $matches[1] } } + + $env:package_version="$($version).$($env:APPVEYOR_BUILD_NUMBER)" + + $message = "Building " + + if ($package) { $message += "and packaging "} + + if ($full_build) { $message += "(full build)" } else { $message += "(partial build)" } + + $message += " version " + $env:package_version + " " + + Write-Host $message assembly_info: patch: false @@ -72,13 +78,12 @@ on_success: $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" Get-ChildItem -Recurse "$($sourcedir)\*.pdb" | foreach { $_.fullname.substring(0, $_.fullname.length - $_.extension.length) } | foreach { Write-Output "Generating $($_).mdb"; & 'lib\pdb2mdb.exe' "$($_).dll" } } -- if %BUILD_TYPE%==full cd %appveyor_build_folder%\submodules\packaging\unitypackage && node yarn.js start --path %appveyor_build_folder%\unity\PackageProject --out %appveyor_build_folder% --file github-for-unity-%appveyor_build_version% +- if %BUILD_TYPE%==full cd %appveyor_build_folder%\submodules\packaging\unitypackage && node yarn.js start --path %appveyor_build_folder%\unity\PackageProject --out %appveyor_build_folder% --file github-for-unity-%package_version% - ps: | if ($package) { - Set-Location $env:appveyor_build_folder $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" - $zipfile="$($env:appveyor_build_folder)\github-for-unity-$($env:appveyor_build_version).zip" - $packagefile="$($env:appveyor_build_folder)\github-for-unity-$($env:appveyor_build_version).unitypackage" + $zipfile="$($env:appveyor_build_folder)\github-for-unity-$($env:package_version).zip" + $packagefile="$($env:appveyor_build_folder)\github-for-unity-$($env:package_version).unitypackage" $commitfile="$sourcedir\commit" Add-Content $commitfile $appveyor_repo_commit @@ -88,8 +93,8 @@ on_success: Write-Output "Uploading $zipfile" Push-AppveyorArtifact $zipfile - Push-AppveyorArtifact $packagefile - Push-AppveyorArtifact "$($packagefile).md5" + Push-AppveyorArtifact $packagefile -DeploymentName package + Push-AppveyorArtifact "$($packagefile).md5" -DeploymentName package } on_finish: - ps: Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } From c972dbec0d6c962d078830eb40af49af0a618688 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 20:51:19 +0200 Subject: [PATCH 419/567] appveyor: reset location on every script block, it might get changed by previous blocks --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 185eed79d..c9e397168 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -74,12 +74,14 @@ test: - DoNotRunOnAppVeyor on_success: - ps: | + Set-Location $env:appveyor_build_folder if ($package) { $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" Get-ChildItem -Recurse "$($sourcedir)\*.pdb" | foreach { $_.fullname.substring(0, $_.fullname.length - $_.extension.length) } | foreach { Write-Output "Generating $($_).mdb"; & 'lib\pdb2mdb.exe' "$($_).dll" } } - if %BUILD_TYPE%==full cd %appveyor_build_folder%\submodules\packaging\unitypackage && node yarn.js start --path %appveyor_build_folder%\unity\PackageProject --out %appveyor_build_folder% --file github-for-unity-%package_version% - ps: | + Set-Location $env:appveyor_build_folder if ($package) { $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" $zipfile="$($env:appveyor_build_folder)\github-for-unity-$($env:package_version).zip" @@ -97,4 +99,6 @@ on_success: Push-AppveyorArtifact "$($packagefile).md5" -DeploymentName package } on_finish: -- ps: Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } +- ps: | + Set-Location $env:appveyor_build_folder + Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } From 2cf3f61a61cca81a5c26d23f4ca11d1d3064b0bf Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Thu, 27 Sep 2018 20:53:03 +0200 Subject: [PATCH 420/567] appveyor: name things better and group artifacts together --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c9e397168..dca763b72 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -84,7 +84,7 @@ on_success: Set-Location $env:appveyor_build_folder if ($package) { $sourcedir="$($env:appveyor_build_folder)\unity\PackageProject" - $zipfile="$($env:appveyor_build_folder)\github-for-unity-$($env:package_version).zip" + $zipfile="$($env:appveyor_build_folder)\PackageProject-$($env:package_version).zip" $packagefile="$($env:appveyor_build_folder)\github-for-unity-$($env:package_version).unitypackage" $commitfile="$sourcedir\commit" @@ -94,11 +94,11 @@ on_success: 7z a $zipfile $sourcedir Write-Output "Uploading $zipfile" - Push-AppveyorArtifact $zipfile + Push-AppveyorArtifact $zipfile -DeploymentName source Push-AppveyorArtifact $packagefile -DeploymentName package Push-AppveyorArtifact "$($packagefile).md5" -DeploymentName package } on_finish: - ps: | Set-Location $env:appveyor_build_folder - Get-ChildItem build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + Get-ChildItem $env:appveyor_build_folder\build\*.log | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name -DeploymentName logs } From 4395ae9280e1c45fa6233747f3acc64f5c530c31 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 28 Sep 2018 18:50:46 +0200 Subject: [PATCH 421/567] Disable the extension loader if the extension is disabled --- .../Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs index 770a49ec5..a04601fa9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs @@ -32,6 +32,10 @@ public bool Initialized static ExtensionLoader() { + if (Environment.GetEnvironmentVariable("GITHUB_UNITY_DISABLE") == "1") + { + return; + } EditorApplication.update += Initialize; } From 76c55e2da29fb4c28a5263eb09d765ecc89bec20 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 2 Oct 2018 16:23:48 -0400 Subject: [PATCH 422/567] Fixing the enable/disable of context menus --- .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 8a6291439..0a0106267 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -89,6 +89,8 @@ private static bool ContextMenu_CanLock() { if (!EnsureInitialized()) return false; + if(!Repository.CurrentRemote.HasValue) + return false; if (isBusy) return false; return Selection.objects.Any(IsObjectUnlocked); @@ -99,6 +101,8 @@ private static bool ContextMenu_CanUnlock() { if (!EnsureInitialized()) return false; + if (!Repository.CurrentRemote.HasValue) + return false; if (isBusy) return false; return Selection.objects.Any(IsObjectLocked); @@ -109,6 +113,8 @@ private static bool ContextMenu_CanUnlockForce() { if (!EnsureInitialized()) return false; + if (!Repository.CurrentRemote.HasValue) + return false; if (isBusy) return false; return Selection.objects.Any(IsObjectLocked); From 26e43c50744d983ff25317e305e62b24e19ff1d3 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 08:43:53 -0400 Subject: [PATCH 423/567] Fixing project files --- src/GitHub.Api/GitHub.Api.45.csproj | 2 +- src/GitHub.Api/GitHub.Api.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj index dd7be03ae..651dd9345 100644 --- a/src/GitHub.Api/GitHub.Api.45.csproj +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -35,7 +35,7 @@ TRACE;$(BuildDefs);NET_4_6 prompt 4 - Release + Release false false true diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 5c9305279..98fd5af53 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -35,7 +35,7 @@ TRACE;$(BuildDefs) prompt 4 - Release + Release false false true From edaec52e0c482a48ef0f861f74fbeb69730c296b Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 09:31:35 -0400 Subject: [PATCH 424/567] Making these changes easier to test --- script | 2 +- src/GitHub.Api/Metrics/UsageTracker.cs | 9 ++++++++- src/tests/TestWebServer/HttpServer.cs | 13 +++++++++++++ src/tests/TestWebServer/TestWebServer.csproj | 4 ++++ src/tests/TestWebServer/packages.config | 4 ++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/tests/TestWebServer/packages.config diff --git a/script b/script index 0c618caaa..38269e987 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 0c618caaab7f163921aea7b60484b423e13cb818 +Subproject commit 38269e987adabd0f42dda353872a46a5e206caea diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index 6c96803bf..ca68471cc 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -8,6 +8,13 @@ namespace GitHub.Unity { class UsageTrackerSync : IUsageTracker { + +#if DEVELOPER_BUILD + protected internal const int MetrisReportTimeout = 30; +#else + protected internal const int MetrisReportTimeout = 3 * 60; +#endif + private static ILogging Logger { get; } = LogHelper.GetLogger(); private static object _lock = new object(); @@ -44,7 +51,7 @@ public UsageTrackerSync(ISettings userSettings, IUsageLoader usageLoader, Logger.Trace("userId:{0} instanceId:{1}", userId, instanceId); if (Enabled) - RunTimer(3 * 60); + RunTimer(MetrisReportTimeout); } private void RunTimer(int seconds) diff --git a/src/tests/TestWebServer/HttpServer.cs b/src/tests/TestWebServer/HttpServer.cs index 8f4765abb..1693b6da4 100644 --- a/src/tests/TestWebServer/HttpServer.cs +++ b/src/tests/TestWebServer/HttpServer.cs @@ -8,6 +8,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; +using Newtonsoft.Json; namespace TestWebServer { @@ -106,6 +107,18 @@ private void Process(HttpListenerContext context) if (context.Request.Url.AbsolutePath == "/api/usage/unity") { + var streamReader = new StreamReader(context.Request.InputStream); + string body = null; + using (streamReader) + { + body = streamReader.ReadToEnd(); + } + + var parsedJson = JsonConvert.DeserializeObject(body); + var formattedJson = JsonConvert.SerializeObject(parsedJson, Formatting.Indented); + + Logger.Info(formattedJson); + var json = new { result = "Cool unity usage" }.ToJson(); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.ContentLength64 = json.Length; diff --git a/src/tests/TestWebServer/TestWebServer.csproj b/src/tests/TestWebServer/TestWebServer.csproj index c8d6cffad..8cc2ce5f1 100644 --- a/src/tests/TestWebServer/TestWebServer.csproj +++ b/src/tests/TestWebServer/TestWebServer.csproj @@ -31,6 +31,9 @@ 4 + + ..\..\..\packages\Newtonsoft.Json.11.0.2\lib\net35\Newtonsoft.Json.dll + @@ -81,6 +84,7 @@ PreserveNewest + diff --git a/src/tests/TestWebServer/packages.config b/src/tests/TestWebServer/packages.config new file mode 100644 index 000000000..9eecb30ef --- /dev/null +++ b/src/tests/TestWebServer/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 4b0769c5400009b78a9d85fd15b247d3c5c219f2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 11:23:12 -0400 Subject: [PATCH 425/567] Adding a test that proves the error --- .../UnitTests/IO/GitObjectFactoryTests.cs | 50 +++++++++++++++++++ src/tests/UnitTests/UnitTests.csproj | 1 + 2 files changed, 51 insertions(+) create mode 100644 src/tests/UnitTests/IO/GitObjectFactoryTests.cs diff --git a/src/tests/UnitTests/IO/GitObjectFactoryTests.cs b/src/tests/UnitTests/IO/GitObjectFactoryTests.cs new file mode 100644 index 000000000..d306a0111 --- /dev/null +++ b/src/tests/UnitTests/IO/GitObjectFactoryTests.cs @@ -0,0 +1,50 @@ +using GitHub.Unity; +using NCrunch.Framework; +using NSubstitute; +using NUnit.Framework; +using TestUtils; + +namespace UnitTests +{ + [TestFixture, Isolated] + class GitObjectFactoryTests + { + private static readonly SubstituteFactory SubstituteFactory = new SubstituteFactory(); + + [Test] + public void ShouldParseNormalFile() + { + NPath.FileSystem = SubstituteFactory.CreateFileSystem(new CreateFileSystemOptions() { + CurrentDirectory = @"c:\Projects\UnityProject" + }); + + var environment = NSubstitute.Substitute.For(); + environment.RepositoryPath.Returns(@"c:\Projects\UnityProject".ToNPath()); + environment.UnityProjectPath.Returns(@"c:\Projects\UnityProject".ToNPath()); + + var gitObjectFactory = new GitObjectFactory(environment); + var gitStatusEntry = gitObjectFactory.CreateGitStatusEntry("hello.txt", GitFileStatus.Deleted); + + Assert.AreEqual(@"c:\Projects\UnityProject\hello.txt", gitStatusEntry.FullPath); + } + + + [Test] + public void ShouldParseOddFile() + { + NPath.FileSystem = SubstituteFactory.CreateFileSystem(new CreateFileSystemOptions() + { + CurrentDirectory = @"c:\Projects\UnityProject" + }); + + var environment = NSubstitute.Substitute.For(); + environment.RepositoryPath.Returns(@"c:\Projects\UnityProject".ToNPath()); + environment.UnityProjectPath.Returns(@"c:\Projects\UnityProject".ToNPath()); + + var gitObjectFactory = new GitObjectFactory(environment); + var gitStatusEntry = gitObjectFactory.CreateGitStatusEntry("c:UsersOculusGoVideo.mp4", GitFileStatus.Deleted); + + Assert.AreEqual(@"c:\Projects\UnityProject\c:UsersOculusGoVideo.mp4", gitStatusEntry.FullPath); + } + } +} \ No newline at end of file diff --git a/src/tests/UnitTests/UnitTests.csproj b/src/tests/UnitTests/UnitTests.csproj index bff3c8d57..e172e1b74 100644 --- a/src/tests/UnitTests/UnitTests.csproj +++ b/src/tests/UnitTests/UnitTests.csproj @@ -76,6 +76,7 @@ + From 3a9593c3766e76dd3d6d7ec6d3b4aceac8441202 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 11:24:41 -0400 Subject: [PATCH 426/567] Fixing the issue --- src/GitHub.Api/IO/NiceIO.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/IO/NiceIO.cs b/src/GitHub.Api/IO/NiceIO.cs index bf008660c..c8605de6a 100644 --- a/src/GitHub.Api/IO/NiceIO.cs +++ b/src/GitHub.Api/IO/NiceIO.cs @@ -113,7 +113,7 @@ private static bool HasNonDotDotLastElement(List stack) private static string ParseDriveLetter(string path, out string driveLetter) { - if (path.Length >= 2 && path[1] == ':') + if (path.Length >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\')) { driveLetter = path[0].ToString(); return path.Substring(2); From 4b08403bf9eb37978036ffa190eb476aabe83f8e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 11:28:57 -0400 Subject: [PATCH 427/567] Cleanup --- src/tests/UnitTests/IO/GitObjectFactoryTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/UnitTests/IO/GitObjectFactoryTests.cs b/src/tests/UnitTests/IO/GitObjectFactoryTests.cs index d306a0111..35c98d5cd 100644 --- a/src/tests/UnitTests/IO/GitObjectFactoryTests.cs +++ b/src/tests/UnitTests/IO/GitObjectFactoryTests.cs @@ -18,7 +18,7 @@ public void ShouldParseNormalFile() CurrentDirectory = @"c:\Projects\UnityProject" }); - var environment = NSubstitute.Substitute.For(); + var environment = Substitute.For(); environment.RepositoryPath.Returns(@"c:\Projects\UnityProject".ToNPath()); environment.UnityProjectPath.Returns(@"c:\Projects\UnityProject".ToNPath()); @@ -37,7 +37,7 @@ public void ShouldParseOddFile() CurrentDirectory = @"c:\Projects\UnityProject" }); - var environment = NSubstitute.Substitute.For(); + var environment = Substitute.For(); environment.RepositoryPath.Returns(@"c:\Projects\UnityProject".ToNPath()); environment.UnityProjectPath.Returns(@"c:\Projects\UnityProject".ToNPath()); @@ -47,4 +47,4 @@ public void ShouldParseOddFile() Assert.AreEqual(@"c:\Projects\UnityProject\c:UsersOculusGoVideo.mp4", gitStatusEntry.FullPath); } } -} \ No newline at end of file +} From bc914a911c64c43dc5e6586abeccfad1748b30e2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 16:07:26 -0400 Subject: [PATCH 428/567] Adding test to prove the error --- src/GitHub.Api/UI/TreeBase.cs | 1 - src/tests/UnitTests/UI/TreeBaseTests.cs | 73 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 915abe89f..771ae4290 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -332,7 +332,6 @@ private List GetLeafNodes(TNode node, int idx) return results; } - private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) { while (true) diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index ec0c03467..6ba2d7419 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -185,6 +185,11 @@ public override TestTreeNode SelectedNode } } + public new void ToggleNodeChecked(int idx, TestTreeNode node) + { + base.ToggleNodeChecked(idx, node); + } + protected override List Nodes { get @@ -308,6 +313,8 @@ protected override bool PromoteMetaFiles return TestTreeListener.PromoteMetaFiles; } } + + } [TestFixture] @@ -606,6 +613,72 @@ public void ShouldPopulateTreeWithSingleEntryWithMetaInPath() } }); } + + + [Test] + public void ShouldCheckParentOfMetaFile() + { + var testTree = new TestTree(true); + var testTreeListener = testTree.TestTreeListener; + + testTreeListener.GetCollapsedFolders().Returns(new string[0]); + testTreeListener.SelectedNode.Returns((TestTreeNode)null); + testTreeListener.GetCheckedFiles().Returns(new string[0]); + testTreeListener.Nodes.Returns(new List()); + testTreeListener.PathSeparator.Returns(@"\"); + testTreeListener.DisplayRootNode.Returns(true); + testTreeListener.IsSelectable.Returns(false); + testTreeListener.Title.Returns("Test Tree"); + testTreeListener.PromoteMetaFiles.Returns(true); + + var testTreeData = new[] { + new TestTreeData { + Path = "Folder\\Default Scene.unity" + }, + new TestTreeData { + Path = "Folder\\Default Scene.unity.meta" + } + }; + testTree.Load(testTreeData); + + testTree.CreatedTreeNodes.ShouldAllBeEquivalentTo(new[] { + new TestTreeNode { + Path = "Test Tree", + Label = "Test Tree", + IsFolder = true + }, + new TestTreeNode { + Path = "Folder", + Label = "Folder", + Level = 1, + IsFolder = true + }, + new TestTreeNode { + Path = "Folder\\Default Scene.unity", + Label = "Default Scene.unity", + Level = 2, + TreeData = testTreeData[0], + IsContainer = true + }, + new TestTreeNode { + Path = "Folder\\Default Scene.unity.meta", + Label = "Default Scene.unity.meta", + Level = 3, + TreeData = testTreeData[1] + } + }); + + var sceneNode = testTree.CreatedTreeNodes[2]; + var sceneMetaNode = testTree.CreatedTreeNodes[3]; + + testTree.ToggleNodeChecked(3, sceneMetaNode); + + Assert.AreEqual(CheckState.Checked, sceneNode.CheckState); + Assert.AreEqual(CheckState.Checked, sceneMetaNode.CheckState); + + testTreeListener.Received(2).AddCheckedNode(Arg.Any()); + } + [Test] public void ShouldPopulateTreeWithSingleEntryWithNonPromotedMetaInPath() { From ecbdffe05b9cb0a331e99ba4374ab1c76ee47b4d Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 16:16:50 -0400 Subject: [PATCH 429/567] Functionality to set parent node from meta --- src/GitHub.Api/UI/TreeBase.cs | 95 +++++++++++-------------- src/tests/UnitTests/UI/TreeBaseTests.cs | 3 + 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 771ae4290..1db09de94 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -183,23 +183,9 @@ protected bool PromoteNode(TNode previouslyAddedNode, string nextLabel) public void SetCheckStateOnAll(bool isChecked) { - var nodeCheckState = isChecked ? CheckState.Checked : CheckState.Empty; foreach (var node in Nodes) { - var wasChecked = node.CheckState == CheckState.Checked; - node.CheckState = nodeCheckState; - - if (!node.IsFolder) - { - if (isChecked && !wasChecked) - { - AddCheckedNode(node); - } - else if (!isChecked && wasChecked) - { - RemoveCheckedNode(node); - } - } + SetCheckStateOnNode(node, isChecked ? CheckState.Checked : CheckState.Empty); } } @@ -250,39 +236,32 @@ protected void ToggleNodeVisibility(int idx, TNode node) protected void ToggleNodeChecked(int idx, TNode node) { + CheckState checkState; var isChecked = false; - switch (node.CheckState) { case CheckState.Mixed: case CheckState.Empty: - node.CheckState = CheckState.Checked; + checkState = CheckState.Checked; isChecked = true; break; case CheckState.Checked: - node.CheckState = CheckState.Empty; + checkState = CheckState.Empty; break; - } - if (!node.IsFolder) - { - if (isChecked) - { - AddCheckedNode(node); - } - else - { - RemoveCheckedNode(node); - } + default: + throw new ArgumentOutOfRangeException("Unknown CheckState"); } + SetCheckStateOnNode(node, checkState); + if (node.IsFolderOrContainer) { ToggleChildrenChecked(idx, node, isChecked); } - ToggleParentFoldersChecked(idx, node, isChecked); + ToggleParentFolderAndContainersChecked(idx, node, isChecked); } private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) @@ -290,20 +269,9 @@ private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) for (var i = idx + 1; i < Nodes.Count && node.Level < Nodes[i].Level; i++) { var childNode = Nodes[i]; - var wasChecked = childNode.CheckState == CheckState.Checked; - childNode.CheckState = isChecked ? CheckState.Checked : CheckState.Empty; - if (!childNode.IsFolder) - { - if (isChecked && !wasChecked) - { - AddCheckedNode(childNode); - } - else if (!isChecked && wasChecked) - { - RemoveCheckedNode(childNode); - } - } + var wasChecked = childNode.CheckState == CheckState.Checked; + SetCheckStateOnNode(node, isChecked ? CheckState.Checked : CheckState.Empty); if (childNode.IsFolderOrContainer) { @@ -332,7 +300,29 @@ private List GetLeafNodes(TNode node, int idx) return results; } - private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) + private void SetCheckStateOnNode(TNode node, CheckState nodeCheckState) + { + var isChecked = nodeCheckState == CheckState.Checked + || nodeCheckState == CheckState.Mixed; + + var wasChecked = node.CheckState == CheckState.Checked; + + node.CheckState = nodeCheckState; + + if (!node.IsFolder) + { + if (isChecked && !wasChecked) + { + AddCheckedNode(node); + } + else if (!isChecked && wasChecked) + { + RemoveCheckedNode(node); + } + } + } + + private void ToggleParentFolderAndContainersChecked(int idx, TNode node, bool isChecked) { while (true) { @@ -383,14 +373,15 @@ private void ToggleParentFoldersChecked(int idx, TNode node, bool isChecked) var parentIndex = firstSiblingIndex - 1; var parentNode = Nodes[parentIndex]; - if (siblingsInSameState) - { - parentNode.CheckState = isChecked ? CheckState.Checked : CheckState.Empty; - } - else - { - parentNode.CheckState = CheckState.Mixed; - } + + var parentNodeState = + siblingsInSameState + ? isChecked + ? CheckState.Checked + : CheckState.Empty + : CheckState.Mixed; + + SetCheckStateOnNode(parentNode, parentNodeState); idx = parentIndex; node = parentNode; diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index 6ba2d7419..434e54884 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -671,6 +671,9 @@ public void ShouldCheckParentOfMetaFile() var sceneNode = testTree.CreatedTreeNodes[2]; var sceneMetaNode = testTree.CreatedTreeNodes[3]; + Assert.AreEqual(CheckState.Empty, sceneNode.CheckState); + Assert.AreEqual(CheckState.Empty, sceneMetaNode.CheckState); + testTree.ToggleNodeChecked(3, sceneMetaNode); Assert.AreEqual(CheckState.Checked, sceneNode.CheckState); From 5802234958060c1f19d181c64ad1ff31bc2126a5 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 16:26:04 -0400 Subject: [PATCH 430/567] Adding an overload --- src/GitHub.Api/UI/TreeBase.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 1db09de94..73692a8c9 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -185,7 +185,7 @@ public void SetCheckStateOnAll(bool isChecked) { foreach (var node in Nodes) { - SetCheckStateOnNode(node, isChecked ? CheckState.Checked : CheckState.Empty); + SetCheckStateOnNode(node, isChecked); } } @@ -271,7 +271,7 @@ private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) var childNode = Nodes[i]; var wasChecked = childNode.CheckState == CheckState.Checked; - SetCheckStateOnNode(node, isChecked ? CheckState.Checked : CheckState.Empty); + SetCheckStateOnNode(node, isChecked); if (childNode.IsFolderOrContainer) { @@ -300,6 +300,11 @@ private List GetLeafNodes(TNode node, int idx) return results; } + private void SetCheckStateOnNode(TNode node, bool isChecked) + { + SetCheckStateOnNode(node, isChecked ? CheckState.Checked : CheckState.Empty); + } + private void SetCheckStateOnNode(TNode node, CheckState nodeCheckState) { var isChecked = nodeCheckState == CheckState.Checked From 6d65f91bc91231400ea303c9f49e0d5516dc86dc Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 16:27:22 -0400 Subject: [PATCH 431/567] Cleanup --- src/tests/UnitTests/UI/TreeBaseTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index 434e54884..399d15846 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -313,8 +313,6 @@ protected override bool PromoteMetaFiles return TestTreeListener.PromoteMetaFiles; } } - - } [TestFixture] @@ -614,7 +612,6 @@ public void ShouldPopulateTreeWithSingleEntryWithMetaInPath() }); } - [Test] public void ShouldCheckParentOfMetaFile() { From d8d92c990278df1306754c22ed90de05193686ae Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 3 Oct 2018 16:36:05 -0400 Subject: [PATCH 432/567] Rename variables --- src/GitHub.Api/UI/TreeBase.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index 73692a8c9..d752af48d 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -300,19 +300,19 @@ private List GetLeafNodes(TNode node, int idx) return results; } - private void SetCheckStateOnNode(TNode node, bool isChecked) + private void SetCheckStateOnNode(TNode node, bool setChecked) { - SetCheckStateOnNode(node, isChecked ? CheckState.Checked : CheckState.Empty); + SetCheckStateOnNode(node, setChecked ? CheckState.Checked : CheckState.Empty); } - private void SetCheckStateOnNode(TNode node, CheckState nodeCheckState) + private void SetCheckStateOnNode(TNode node, CheckState setCheckState) { - var isChecked = nodeCheckState == CheckState.Checked - || nodeCheckState == CheckState.Mixed; + var isChecked = setCheckState == CheckState.Checked + || setCheckState == CheckState.Mixed; var wasChecked = node.CheckState == CheckState.Checked; - node.CheckState = nodeCheckState; + node.CheckState = setCheckState; if (!node.IsFolder) { From 4a8171d69da42b5913e72bd4d0b0f7e1c3183d26 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 4 Oct 2018 09:05:24 -0400 Subject: [PATCH 433/567] Adding an additional test and catching some bugs --- src/GitHub.Api/UI/TreeBase.cs | 12 ++- src/tests/UnitTests/UI/TreeBaseTests.cs | 114 ++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/src/GitHub.Api/UI/TreeBase.cs b/src/GitHub.Api/UI/TreeBase.cs index d752af48d..4804558ce 100644 --- a/src/GitHub.Api/UI/TreeBase.cs +++ b/src/GitHub.Api/UI/TreeBase.cs @@ -261,7 +261,7 @@ protected void ToggleNodeChecked(int idx, TNode node) ToggleChildrenChecked(idx, node, isChecked); } - ToggleParentFolderAndContainersChecked(idx, node, isChecked); + ToggleParentFolderAndContainersChecked(idx, node, checkState); } private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) @@ -270,8 +270,7 @@ private void ToggleChildrenChecked(int idx, TNode node, bool isChecked) { var childNode = Nodes[i]; - var wasChecked = childNode.CheckState == CheckState.Checked; - SetCheckStateOnNode(node, isChecked); + SetCheckStateOnNode(childNode, isChecked); if (childNode.IsFolderOrContainer) { @@ -327,8 +326,9 @@ private void SetCheckStateOnNode(TNode node, CheckState setCheckState) } } - private void ToggleParentFolderAndContainersChecked(int idx, TNode node, bool isChecked) + private void ToggleParentFolderAndContainersChecked(int idx, TNode node, CheckState checkState) { + var isChecked = checkState != CheckState.Empty; while (true) { if (node.Level > 0) @@ -381,9 +381,7 @@ private void ToggleParentFolderAndContainersChecked(int idx, TNode node, bool is var parentNodeState = siblingsInSameState - ? isChecked - ? CheckState.Checked - : CheckState.Empty + ? node.CheckState : CheckState.Mixed; SetCheckStateOnNode(parentNode, parentNodeState); diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index 399d15846..de37c9455 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -679,6 +679,120 @@ public void ShouldCheckParentOfMetaFile() testTreeListener.Received(2).AddCheckedNode(Arg.Any()); } + [Test] + public void ShouldRippleUncheckCorrectly() + { + var testTree = new TestTree(true); + var testTreeListener = testTree.TestTreeListener; + + testTreeListener.GetCollapsedFolders().Returns(new string[0]); + testTreeListener.SelectedNode.Returns((TestTreeNode)null); + testTreeListener.GetCheckedFiles().Returns(new string[0]); + testTreeListener.Nodes.Returns(new List()); + testTreeListener.PathSeparator.Returns(@"\"); + testTreeListener.DisplayRootNode.Returns(true); + testTreeListener.IsSelectable.Returns(false); + testTreeListener.Title.Returns("Test Tree"); + testTreeListener.PromoteMetaFiles.Returns(true); + + var testTreeData = new[] { + new TestTreeData { + Path = "Root\\Parent\\A.txt" + }, + new TestTreeData { + Path = "Root\\Parent\\B.txt" + }, + new TestTreeData { + Path = "Root\\Parent\\C.txt" + } + }; + + testTree.Load(testTreeData); + + testTree.CreatedTreeNodes.ShouldAllBeEquivalentTo(new[] { + new TestTreeNode { + Path = "Test Tree", + Label = "Test Tree", + IsFolder = true + }, + new TestTreeNode { + Path = "Root", + Label = "Root", + Level = 1, + IsFolder = true + }, + new TestTreeNode { + Path = "Root\\Parent", + Label = "Parent", + Level = 2, + IsFolder = true + }, + new TestTreeNode { + Path = "Root\\Parent\\A.txt", + Label = "A.txt", + Level = 3, + TreeData = testTreeData[0], + }, + new TestTreeNode { + Path = "Root\\Parent\\B.txt", + Label = "B.txt", + Level = 3, + TreeData = testTreeData[1], + }, + new TestTreeNode { + Path = "Root\\Parent\\C.txt", + Label = "C.txt", + Level = 3, + TreeData = testTreeData[2], + } + }); + + var rootNode = testTree.CreatedTreeNodes[1]; + var parentNode = testTree.CreatedTreeNodes[2]; + var aNode = testTree.CreatedTreeNodes[3]; + var bNode = testTree.CreatedTreeNodes[4]; + var cNode = testTree.CreatedTreeNodes[5]; + + Assert.AreEqual(CheckState.Empty, rootNode.CheckState); + Assert.AreEqual(CheckState.Empty, parentNode.CheckState); + Assert.AreEqual(CheckState.Empty, aNode.CheckState); + Assert.AreEqual(CheckState.Empty, bNode.CheckState); + Assert.AreEqual(CheckState.Empty, cNode.CheckState); + + testTree.ToggleNodeChecked(1, rootNode); + + Assert.AreEqual(CheckState.Checked, rootNode.CheckState); + Assert.AreEqual(CheckState.Checked, parentNode.CheckState); + Assert.AreEqual(CheckState.Checked, aNode.CheckState); + Assert.AreEqual(CheckState.Checked, bNode.CheckState); + Assert.AreEqual(CheckState.Checked, cNode.CheckState); + + testTreeListener.Received(3).AddCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + + testTree.ToggleNodeChecked(5, cNode); + + Assert.AreEqual(CheckState.Mixed, rootNode.CheckState); + Assert.AreEqual(CheckState.Mixed, parentNode.CheckState); + Assert.AreEqual(CheckState.Checked, aNode.CheckState); + Assert.AreEqual(CheckState.Checked, bNode.CheckState); + Assert.AreEqual(CheckState.Empty, cNode.CheckState); + + testTreeListener.Received(1).RemoveCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + + testTree.ToggleNodeChecked(5, cNode); + + Assert.AreEqual(CheckState.Checked, rootNode.CheckState); + Assert.AreEqual(CheckState.Checked, parentNode.CheckState); + Assert.AreEqual(CheckState.Checked, aNode.CheckState); + Assert.AreEqual(CheckState.Checked, bNode.CheckState); + Assert.AreEqual(CheckState.Checked, cNode.CheckState); + + testTreeListener.Received(1).AddCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); + } + [Test] public void ShouldPopulateTreeWithSingleEntryWithNonPromotedMetaInPath() { From ce52b22553ab22625d7b4bdb66658b701c3dfb81 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 4 Oct 2018 09:07:24 -0400 Subject: [PATCH 434/567] Adding comments --- src/tests/UnitTests/UI/TreeBaseTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index de37c9455..ff45a5799 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -753,6 +753,8 @@ public void ShouldRippleUncheckCorrectly() var bNode = testTree.CreatedTreeNodes[4]; var cNode = testTree.CreatedTreeNodes[5]; + // Initial state, everything unchecked + Assert.AreEqual(CheckState.Empty, rootNode.CheckState); Assert.AreEqual(CheckState.Empty, parentNode.CheckState); Assert.AreEqual(CheckState.Empty, aNode.CheckState); @@ -761,6 +763,8 @@ public void ShouldRippleUncheckCorrectly() testTree.ToggleNodeChecked(1, rootNode); + // Checked the root node, everything checked + Assert.AreEqual(CheckState.Checked, rootNode.CheckState); Assert.AreEqual(CheckState.Checked, parentNode.CheckState); Assert.AreEqual(CheckState.Checked, aNode.CheckState); @@ -770,6 +774,8 @@ public void ShouldRippleUncheckCorrectly() testTreeListener.Received(3).AddCheckedNode(Arg.Any()); testTreeListener.ClearReceivedCalls(); + // Unchecked c.txt, c.txt unchecked, parents mixed + testTree.ToggleNodeChecked(5, cNode); Assert.AreEqual(CheckState.Mixed, rootNode.CheckState); @@ -783,6 +789,8 @@ public void ShouldRippleUncheckCorrectly() testTree.ToggleNodeChecked(5, cNode); + // Checked c.txt, everything checked + Assert.AreEqual(CheckState.Checked, rootNode.CheckState); Assert.AreEqual(CheckState.Checked, parentNode.CheckState); Assert.AreEqual(CheckState.Checked, aNode.CheckState); From 16907561184d8e7c2198db052d594ad556f812f4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 4 Oct 2018 09:14:35 -0400 Subject: [PATCH 435/567] Rename test --- src/tests/UnitTests/UI/TreeBaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index ff45a5799..b76e88cdd 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -680,7 +680,7 @@ public void ShouldCheckParentOfMetaFile() } [Test] - public void ShouldRippleUncheckCorrectly() + public void ShouldRippleChecksCorrectly() { var testTree = new TestTree(true); var testTreeListener = testTree.TestTreeListener; From 4049672e67310ffd49d2832f769e1bf874e798a2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 4 Oct 2018 09:37:10 -0400 Subject: [PATCH 436/567] Adding extra logging to check if the path still exists when we go to write to it --- src/GitHub.Api/Installer/OctorunInstaller.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index e1f92252b..9766b7c99 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -52,8 +52,14 @@ private NPath GrabZipFromResources() private NPath MoveOctorun(NPath fromPath) { var toPath = installDetails.InstallationPath; + + Logger.Info("MoveOctorun fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + toPath.DeleteIfExists(); toPath.EnsureParentDirectoryExists(); + + Logger.Info("toPath Exists: {0}", toPath.Exists()); + fromPath.Move(toPath); fromPath.Parent.Delete(); return installDetails.ExecutablePath; From ae4f4f650b5449f3e01ea9a7eb1837a785c0bb80 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 4 Oct 2018 09:54:58 -0400 Subject: [PATCH 437/567] Adding more logs --- src/GitHub.Api/Installer/GitInstaller.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 4feb21c75..6d5734ceb 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -301,13 +301,17 @@ private GitInstallationState ExtractGit(GitInstallationState state) return true; }); unzipTask.Progress(p => Progress.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Message)); - var path = unzipTask.RunSynchronously(); + var source = unzipTask.RunSynchronously(); var target = state.GitInstallationPath; if (unzipTask.Successful) { - var source = path; + Logger.Info("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); + target.DeleteIfExists(); target.EnsureParentDirectoryExists(); + + Logger.Info("target Exists: {0}", target.Exists()); + source.Move(target); state.GitIsValid = true; state.IsCustomGitPath = state.GitExecutablePath != installDetails.GitExecutablePath; @@ -326,13 +330,17 @@ private GitInstallationState ExtractGit(GitInstallationState state) return true; }); unzipTask.Progress(p => Progress.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Message)); - var path = unzipTask.RunSynchronously(); + var source = unzipTask.RunSynchronously(); var target = state.GitLfsInstallationPath; if (unzipTask.Successful) { - var source = path; + Logger.Info("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); + target.DeleteIfExists(); target.EnsureParentDirectoryExists(); + + Logger.Info("target Exists: {0}", target.Exists()); + source.Move(target); state.GitLfsIsValid = true; } From dba56321bffeab985f87fa61cd4c5c58e97c873a Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 4 Oct 2018 15:34:22 -0400 Subject: [PATCH 438/567] Trying to move the files into the target as opposed to moving the whole thing --- src/GitHub.Api/Installer/OctorunInstaller.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 9766b7c99..284ce9e11 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -55,13 +55,15 @@ private NPath MoveOctorun(NPath fromPath) Logger.Info("MoveOctorun fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); - toPath.DeleteIfExists(); - toPath.EnsureParentDirectoryExists(); + Logger.Info("DeleteContents toPath:{0}", toPath.ToString()); + toPath.DeleteContents(); - Logger.Info("toPath Exists: {0}", toPath.Exists()); + Logger.Info("MoveFiles fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + fromPath.MoveFiles(toPath, true); - fromPath.Move(toPath); + Logger.Info("Delete fromPath:{0}", fromPath.ToString()); fromPath.Parent.Delete(); + return installDetails.ExecutablePath; } From 009a34a50758d24cb3880adf7e74ff59a62cd438 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 5 Oct 2018 09:52:11 -0400 Subject: [PATCH 439/567] Changing copy method of git and logs --- src/GitHub.Api/Installer/GitInstaller.cs | 15 +++++++++------ src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 6d5734ceb..453f76335 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -307,12 +307,15 @@ private GitInstallationState ExtractGit(GitInstallationState state) { Logger.Info("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); - target.DeleteIfExists(); - target.EnsureParentDirectoryExists(); + Logger.Info("DeleteContents target:{0}", target.ToString()); + target.DeleteContents(); - Logger.Info("target Exists: {0}", target.Exists()); + Logger.Info("MoveFiles fromPath: {0} toPath:{1}", source.ToString(), target.ToString()); + source.MoveFiles(target, true); + + Logger.Info("Delete source:{0}", source.ToString()); + source.Delete(); - source.Move(target); state.GitIsValid = true; state.IsCustomGitPath = state.GitExecutablePath != installDetails.GitExecutablePath; } @@ -334,12 +337,12 @@ private GitInstallationState ExtractGit(GitInstallationState state) var target = state.GitLfsInstallationPath; if (unzipTask.Successful) { - Logger.Info("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); + Logger.Info("Moving GitLFS source:{0} target:{1}", source.ToString(), target.ToString()); target.DeleteIfExists(); target.EnsureParentDirectoryExists(); - Logger.Info("target Exists: {0}", target.Exists()); + Logger.Info("GitLFS target Exists: {0}", target.Exists()); source.Move(target); state.GitLfsIsValid = true; diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 284ce9e11..0d483a2ad 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -61,7 +61,7 @@ private NPath MoveOctorun(NPath fromPath) Logger.Info("MoveFiles fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); fromPath.MoveFiles(toPath, true); - Logger.Info("Delete fromPath:{0}", fromPath.ToString()); + Logger.Info("Delete fromPath.Parent:{0}", fromPath.Parent.ToString()); fromPath.Parent.Delete(); return installDetails.ExecutablePath; From 4dacb26907d30a2addaa6ccf7028698739f9007f Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 5 Oct 2018 10:41:51 -0400 Subject: [PATCH 440/567] Moving LFS the same way --- src/GitHub.Api/Installer/GitInstaller.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 453f76335..5fcb992a3 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -317,6 +317,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) source.Delete(); state.GitIsValid = true; + state.IsCustomGitPath = state.GitExecutablePath != installDetails.GitExecutablePath; } } @@ -339,12 +340,15 @@ private GitInstallationState ExtractGit(GitInstallationState state) { Logger.Info("Moving GitLFS source:{0} target:{1}", source.ToString(), target.ToString()); - target.DeleteIfExists(); - target.EnsureParentDirectoryExists(); + Logger.Info("DeleteContents target:{0}", target.ToString()); + target.DeleteContents(); + + Logger.Info("MoveFiles fromPath: {0} toPath:{1}", source.ToString(), target.ToString()); + source.MoveFiles(target, true); - Logger.Info("GitLFS target Exists: {0}", target.Exists()); + Logger.Info("Delete source:{0}", source.ToString()); + source.Delete(); - source.Move(target); state.GitLfsIsValid = true; } } From ff93b0e85d720aaba2237e73c6e50e7d5c877f8f Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 5 Oct 2018 10:49:26 -0400 Subject: [PATCH 441/567] Cleanup --- src/GitHub.Api/Installer/GitInstaller.cs | 18 ++++-------------- src/GitHub.Api/Installer/OctorunInstaller.cs | 7 +------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 5fcb992a3..71dd77361 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -305,16 +305,11 @@ private GitInstallationState ExtractGit(GitInstallationState state) var target = state.GitInstallationPath; if (unzipTask.Successful) { - Logger.Info("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); + Logger.Trace("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); - Logger.Info("DeleteContents target:{0}", target.ToString()); target.DeleteContents(); - - Logger.Info("MoveFiles fromPath: {0} toPath:{1}", source.ToString(), target.ToString()); source.MoveFiles(target, true); - - Logger.Info("Delete source:{0}", source.ToString()); - source.Delete(); + source.Parent.Delete(); state.GitIsValid = true; @@ -338,16 +333,11 @@ private GitInstallationState ExtractGit(GitInstallationState state) var target = state.GitLfsInstallationPath; if (unzipTask.Successful) { - Logger.Info("Moving GitLFS source:{0} target:{1}", source.ToString(), target.ToString()); + Logger.Trace("Moving GitLFS source:{0} target:{1}", source.ToString(), target.ToString()); - Logger.Info("DeleteContents target:{0}", target.ToString()); target.DeleteContents(); - - Logger.Info("MoveFiles fromPath: {0} toPath:{1}", source.ToString(), target.ToString()); source.MoveFiles(target, true); - - Logger.Info("Delete source:{0}", source.ToString()); - source.Delete(); + source.Parent.Delete(); state.GitLfsIsValid = true; } diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 0d483a2ad..9baaca033 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -53,15 +53,10 @@ private NPath MoveOctorun(NPath fromPath) { var toPath = installDetails.InstallationPath; - Logger.Info("MoveOctorun fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + Logger.Trace("MoveOctorun fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); - Logger.Info("DeleteContents toPath:{0}", toPath.ToString()); toPath.DeleteContents(); - - Logger.Info("MoveFiles fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); fromPath.MoveFiles(toPath, true); - - Logger.Info("Delete fromPath.Parent:{0}", fromPath.Parent.ToString()); fromPath.Parent.Delete(); return installDetails.ExecutablePath; From 77265ae635831eda557af13ec3841606a5cab676 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 5 Oct 2018 11:11:11 -0400 Subject: [PATCH 442/567] Added final clause to test --- src/tests/UnitTests/UI/TreeBaseTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tests/UnitTests/UI/TreeBaseTests.cs b/src/tests/UnitTests/UI/TreeBaseTests.cs index b76e88cdd..e2e5e07ed 100644 --- a/src/tests/UnitTests/UI/TreeBaseTests.cs +++ b/src/tests/UnitTests/UI/TreeBaseTests.cs @@ -799,6 +799,21 @@ public void ShouldRippleChecksCorrectly() testTreeListener.Received(1).AddCheckedNode(Arg.Any()); testTreeListener.ClearReceivedCalls(); + + // Unchecked a.txt b.txt and c.txt, everything checked + + testTree.ToggleNodeChecked(3, aNode); + testTree.ToggleNodeChecked(4, bNode); + testTree.ToggleNodeChecked(5, cNode); + + Assert.AreEqual(CheckState.Empty, rootNode.CheckState); + Assert.AreEqual(CheckState.Empty, parentNode.CheckState); + Assert.AreEqual(CheckState.Empty, aNode.CheckState); + Assert.AreEqual(CheckState.Empty, bNode.CheckState); + Assert.AreEqual(CheckState.Empty, cNode.CheckState); + + testTreeListener.Received(3).RemoveCheckedNode(Arg.Any()); + testTreeListener.ClearReceivedCalls(); } [Test] From 4d8d664adcf766288e5f3f11287f563b6da41c67 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 8 Oct 2018 12:42:43 -0400 Subject: [PATCH 443/567] Preventing repo with no commits from throwing error --- src/GitHub.Api/Git/GitClient.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index d3bc60351..5e454f6fa 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -325,7 +325,11 @@ public ITask AheadBehindStatus(string gitRef, string other public ITask> Log(BaseOutputListProcessor processor = null) { return new GitLogTask(new GitObjectFactory(environment), cancellationToken, processor) - .Configure(processManager); + .Configure(processManager) + .Catch(exception => exception is ProcessException && + exception.Message.StartsWith("fatal: your current branch") && + exception.Message.EndsWith("does not have any commits yet")) + .Then((success, list) => success ? list : new List()); } /// @@ -596,7 +600,11 @@ public ITask Unlock(NPath file, bool force, public ITask GetHead(IOutputProcessor processor = null) { return new FirstNonNullLineProcessTask(cancellationToken, "rev-parse --short HEAD") { Name = "Getting current head..." } - .Configure(processManager); + .Configure(processManager) + .Catch(exception => exception is ProcessException && + exception.Message.StartsWith("fatal: your current branch") && + exception.Message.EndsWith("does not have any commits yet")) + .Then((success, head) => success ? head : null); } protected static ILogging Logger { get; } = LogHelper.GetLogger(); From 5a0e127fdb0225afdb5c852b18e11462fc85640e Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 8 Oct 2018 15:06:07 -0400 Subject: [PATCH 444/567] Adding SharpZipLib from source --- lib/ICSharpCode.NRefactory.dll | 3 - lib/ICSharpCode.SharpZipLib.dll | 3 - src/GitHub.Api/GitHub.Api.45.csproj | 46 +- src/GitHub.Api/GitHub.Api.csproj | 44 +- src/GitHub.Api/Installer/ZipHelper.cs | 2 +- .../SharpZipLib/Checksums/Adler32.cs | 237 + src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs | 223 + .../SharpZipLib/Checksums/IChecksum.cs | 93 + .../SharpZipLib/Checksums/StrangeCRC.cs | 208 + .../SharpZipLib/Core/FileSystemScanner.cs | 533 ++ .../SharpZipLib/Core/INameTransform.cs | 57 + .../SharpZipLib/Core/IScanFilter.cs | 50 + src/GitHub.Api/SharpZipLib/Core/NameFilter.cs | 290 ++ src/GitHub.Api/SharpZipLib/Core/PathFilter.cs | 334 ++ .../SharpZipLib/Core/StreamUtils.cs | 246 + .../SharpZipLib/Core/WindowsPathUtils.cs | 94 + .../SharpZipLib/Encryption/PkzipClassic.cs | 498 ++ .../SharpZipLib/Encryption/ZipAESStream.cs | 170 + .../SharpZipLib/Encryption/ZipAESTransform.cs | 219 + .../SharpZipLib/SharpZipBaseException.cs | 94 + .../SharpZipLib/Zip/Compression/Deflater.cs | 557 ++ .../Zip/Compression/DeflaterConstants.cs | 186 + .../Zip/Compression/DeflaterEngine.cs | 869 ++++ .../Zip/Compression/DeflaterHuffman.cs | 908 ++++ .../Zip/Compression/DeflaterPending.cs | 57 + .../SharpZipLib/Zip/Compression/Inflater.cs | 864 ++++ .../Zip/Compression/InflaterDynHeader.cs | 218 + .../Zip/Compression/InflaterHuffmanTree.cs | 232 + .../Zip/Compression/PendingBuffer.cs | 295 ++ .../Streams/DeflaterOutputStream.cs | 602 +++ .../Streams/InflaterInputStream.cs | 732 +++ .../Zip/Compression/Streams/OutputWindow.cs | 235 + .../Compression/Streams/StreamManipulator.cs | 297 ++ src/GitHub.Api/SharpZipLib/Zip/FastZip.cs | 729 +++ .../SharpZipLib/Zip/IEntryFactory.cs | 82 + .../SharpZipLib/Zip/WindowsNameTransform.cs | 272 + .../SharpZipLib/Zip/ZipConstants.cs | 632 +++ src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs | 1252 +++++ .../SharpZipLib/Zip/ZipEntryFactory.cs | 413 ++ .../SharpZipLib/Zip/ZipException.cs | 94 + .../SharpZipLib/Zip/ZipExtraData.cs | 987 ++++ src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs | 4486 +++++++++++++++++ .../SharpZipLib/Zip/ZipHelperStream.cs | 623 +++ .../SharpZipLib/Zip/ZipInputStream.cs | 675 +++ .../SharpZipLib/Zip/ZipNameTransform.cs | 269 + .../SharpZipLib/Zip/ZipOutputStream.cs | 900 ++++ 46 files changed, 20896 insertions(+), 14 deletions(-) delete mode 100644 lib/ICSharpCode.NRefactory.dll delete mode 100644 lib/ICSharpCode.SharpZipLib.dll create mode 100644 src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs create mode 100644 src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs create mode 100644 src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs create mode 100644 src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/INameTransform.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/NameFilter.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/PathFilter.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs create mode 100644 src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs create mode 100644 src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs create mode 100644 src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs create mode 100644 src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs create mode 100644 src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/FastZip.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipException.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs create mode 100644 src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs diff --git a/lib/ICSharpCode.NRefactory.dll b/lib/ICSharpCode.NRefactory.dll deleted file mode 100644 index f11688c6c..000000000 --- a/lib/ICSharpCode.NRefactory.dll +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e28e35172498877c7e4a33253b0c5df172bce5655b2ab933a922d6777ae5e6d -size 528384 diff --git a/lib/ICSharpCode.SharpZipLib.dll b/lib/ICSharpCode.SharpZipLib.dll deleted file mode 100644 index 108abfd2a..000000000 --- a/lib/ICSharpCode.SharpZipLib.dll +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fbace48694fbfff69ff99ff7aefbde67965f7723092df9c13ff537d2a319f410 -size 192000 diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj index 651dd9345..05a9c6b23 100644 --- a/src/GitHub.Api/GitHub.Api.45.csproj +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -1,4 +1,4 @@ - + @@ -56,9 +56,6 @@ Debug - - $(SolutionDir)lib\ICSharpCode.SharpZipLib.dll - $(SolutionDir)lib\Mono.Posix.dll @@ -125,6 +122,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 98fd5af53..f52c319da 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -60,9 +60,6 @@ $(SolutionDir)\packages\AsyncBridge.Net35.0.2.3333.0\lib\net35-Client\AsyncBridge.Net35.dll True - - $(SolutionDir)lib\ICSharpCode.SharpZipLib.dll - $(SolutionDir)lib\Mono.Posix.dll @@ -136,6 +133,21 @@ + + + + + + + + + + + + + + + @@ -236,6 +248,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.Api/Installer/ZipHelper.cs b/src/GitHub.Api/Installer/ZipHelper.cs index b01731b01..3d581c5e4 100644 --- a/src/GitHub.Api/Installer/ZipHelper.cs +++ b/src/GitHub.Api/Installer/ZipHelper.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Threading; -using ICSharpCode.SharpZipLib.Zip; +using GitHub.ICSharpCode.SharpZipLib.Zip; using GitHub.Logging; using System.Collections.Generic; diff --git a/src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs b/src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs new file mode 100644 index 000000000..b0fc04a43 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/Adler32.cs @@ -0,0 +1,237 @@ +// Adler32.cs - Computes Adler32 data checksum of a data stream +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + + /// + /// Computes Adler32 checksum for a stream of data. An Adler32 + /// checksum is not as reliable as a CRC32 checksum, but a lot faster to + /// compute. + /// + /// The specification for Adler32 may be found in RFC 1950. + /// ZLIB Compressed Data Format Specification version 3.3) + /// + /// + /// From that document: + /// + /// "ADLER32 (Adler-32 checksum) + /// This contains a checksum value of the uncompressed data + /// (excluding any dictionary data) computed according to Adler-32 + /// algorithm. This algorithm is a 32-bit extension and improvement + /// of the Fletcher algorithm, used in the ITU-T X.224 / ISO 8073 + /// standard. + /// + /// Adler-32 is composed of two sums accumulated per byte: s1 is + /// the sum of all bytes, s2 is the sum of all s1 values. Both sums + /// are done modulo 65521. s1 is initialized to 1, s2 to zero. The + /// Adler-32 checksum is stored as s2*65536 + s1 in most- + /// significant-byte first (network) order." + /// + /// "8.2. The Adler-32 algorithm + /// + /// The Adler-32 algorithm is much faster than the CRC32 algorithm yet + /// still provides an extremely low probability of undetected errors. + /// + /// The modulo on unsigned long accumulators can be delayed for 5552 + /// bytes, so the modulo operation time is negligible. If the bytes + /// are a, b, c, the second sum is 3a + 2b + c + 3, and so is position + /// and order sensitive, unlike the first sum, which is just a + /// checksum. That 65521 is prime is important to avoid a possible + /// large class of two-byte errors that leave the check unchanged. + /// (The Fletcher checksum uses 255, which is not prime and which also + /// makes the Fletcher check insensitive to single byte changes 0 - + /// 255.) + /// + /// The sum s1 is initialized to 1 instead of zero to make the length + /// of the sequence part of s2, so that the length does not have to be + /// checked separately. (Any sequence of zeroes has a Fletcher + /// checksum of zero.)" + /// + /// + /// + public sealed class Adler32 : IChecksum + { + /// + /// largest prime smaller than 65536 + /// + const uint BASE = 65521; + + /// + /// Returns the Adler32 data checksum computed so far. + /// + public long Value { + get { + return checksum; + } + } + + /// + /// Creates a new instance of the Adler32 class. + /// The checksum starts off with a value of 1. + /// + public Adler32() + { + Reset(); + } + + /// + /// Resets the Adler32 checksum to the initial value. + /// + public void Reset() + { + checksum = 1; + } + + /// + /// Updates the checksum with a byte value. + /// + /// + /// The data value to add. The high byte of the int is ignored. + /// + public void Update(int value) + { + // We could make a length 1 byte array and call update again, but I + // would rather not have that overhead + uint s1 = checksum & 0xFFFF; + uint s2 = checksum >> 16; + + s1 = (s1 + ((uint)value & 0xFF)) % BASE; + s2 = (s1 + s2) % BASE; + + checksum = (s2 << 16) + s1; + } + + /// + /// Updates the checksum with an array of bytes. + /// + /// + /// The source of the data to update with. + /// + public void Update(byte[] buffer) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Updates the checksum with the bytes taken from the array. + /// + /// + /// an array of bytes + /// + /// + /// the start of the data used for this update + /// + /// + /// the number of bytes to use for this update + /// + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + if (offset < 0) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "cannot be negative"); +#endif + } + + if ( count < 0 ) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "cannot be negative"); +#endif + } + + if (offset >= buffer.Length) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "not a valid index into buffer"); +#endif + } + + if (offset + count > buffer.Length) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "exceeds buffer size"); +#endif + } + + //(By Per Bothner) + uint s1 = checksum & 0xFFFF; + uint s2 = checksum >> 16; + + while (count > 0) { + // We can defer the modulo operation: + // s1 maximally grows from 65521 to 65521 + 255 * 3800 + // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 + int n = 3800; + if (n > count) { + n = count; + } + count -= n; + while (--n >= 0) { + s1 = s1 + (uint)(buffer[offset++] & 0xff); + s2 = s2 + s1; + } + s1 %= BASE; + s2 %= BASE; + } + + checksum = (s2 << 16) | s1; + } + + #region Instance Fields + uint checksum; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs b/src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs new file mode 100644 index 000000000..086594dbe --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/CRC32.cs @@ -0,0 +1,223 @@ +// CRC32.cs - Computes CRC32 data checksum of a data stream +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + + /// + /// Generate a table for a byte-wise 32-bit CRC calculation on the polynomial: + /// x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1. + /// + /// Polynomials over GF(2) are represented in binary, one bit per coefficient, + /// with the lowest powers in the most significant bit. Then adding polynomials + /// is just exclusive-or, and multiplying a polynomial by x is a right shift by + /// one. If we call the above polynomial p, and represent a byte as the + /// polynomial q, also with the lowest power in the most significant bit (so the + /// byte 0xb1 is the polynomial x^7+x^3+x+1), then the CRC is (q*x^32) mod p, + /// where a mod b means the remainder after dividing a by b. + /// + /// This calculation is done using the shift-register method of multiplying and + /// taking the remainder. The register is initialized to zero, and for each + /// incoming bit, x^32 is added mod p to the register if the bit is a one (where + /// x^32 mod p is p+x^32 = x^26+...+1), and the register is multiplied mod p by + /// x (which is shifting right by one and adding x^32 mod p if the bit shifted + /// out is a one). We start with the highest power (least significant bit) of + /// q and repeat for all eight bits of q. + /// + /// The table is simply the CRC of all possible eight bit values. This is all + /// the information needed to generate CRC's on data a byte at a time for all + /// combinations of CRC register values and incoming bytes. + /// + public sealed class Crc32 : IChecksum + { + const uint CrcSeed = 0xFFFFFFFF; + + readonly static uint[] CrcTable = new uint[] { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, + 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, + 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, + 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, + 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, + 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, + 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, + 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, + 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, + 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, + 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, + 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, + 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, + 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, + 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, + 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, + 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, + 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, + 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, + 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, + 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, + 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, + 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, + 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, + 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, + 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, + 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, + 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, + 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, + 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, + 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, + 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, + 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, + 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, + 0x2D02EF8D + }; + + internal static uint ComputeCrc32(uint oldCrc, byte value) + { + return (uint)(Crc32.CrcTable[(oldCrc ^ value) & 0xFF] ^ (oldCrc >> 8)); + } + + /// + /// The crc data checksum so far. + /// + uint crc; + + /// + /// Returns the CRC32 data checksum computed so far. + /// + public long Value { + get { + return (long)crc; + } + set { + crc = (uint)value; + } + } + + /// + /// Resets the CRC32 data checksum as if no update was ever called. + /// + public void Reset() + { + crc = 0; + } + + /// + /// Updates the checksum with the int bval. + /// + /// + /// the byte is taken as the lower 8 bits of value + /// + public void Update(int value) + { + crc ^= CrcSeed; + crc = CrcTable[(crc ^ value) & 0xFF] ^ (crc >> 8); + crc ^= CrcSeed; + } + + /// + /// Updates the checksum with the bytes taken from the array. + /// + /// + /// buffer an array of bytes + /// + public void Update(byte[] buffer) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Adds the byte array to the data checksum. + /// + /// + /// The buffer which contains the data + /// + /// + /// The offset in the buffer where the data starts + /// + /// + /// The number of data bytes to update the CRC with. + /// + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Count cannot be less than zero"); +#endif + } + + if (offset < 0 || offset + count > buffer.Length) { + throw new ArgumentOutOfRangeException("offset"); + } + + crc ^= CrcSeed; + + while (--count >= 0) { + crc = CrcTable[(crc ^ buffer[offset++]) & 0xFF] ^ (crc >> 8); + } + + crc ^= CrcSeed; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs b/src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs new file mode 100644 index 000000000..6ff24e024 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/IChecksum.cs @@ -0,0 +1,93 @@ +// IChecksum.cs - Interface to compute a data checksum +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + + /// + /// Interface to compute a data checksum used by checked input/output streams. + /// A data checksum can be updated by one byte or with a byte array. After each + /// update the value of the current checksum can be returned by calling + /// getValue. The complete checksum object can also be reset + /// so it can be used again with new data. + /// + public interface IChecksum + { + /// + /// Returns the data checksum computed so far. + /// + long Value + { + get; + } + + /// + /// Resets the data checksum as if no update was ever called. + /// + void Reset(); + + /// + /// Adds one byte to the data checksum. + /// + /// + /// the data value to add. The high byte of the int is ignored. + /// + void Update(int value); + + /// + /// Updates the data checksum with the bytes taken from the array. + /// + /// + /// buffer an array of bytes + /// + void Update(byte[] buffer); + + /// + /// Adds the byte array to the data checksum. + /// + /// + /// The buffer which contains the data + /// + /// + /// The offset in the buffer where the data starts + /// + /// + /// the number of data bytes to add. + /// + void Update(byte[] buffer, int offset, int count); + } +} diff --git a/src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs b/src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs new file mode 100644 index 000000000..6d61a6723 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Checksums/StrangeCRC.cs @@ -0,0 +1,208 @@ +// StrangeCRC.cs - computes a crc used in the bziplib +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Checksums +{ + /// + /// Bzip2 checksum algorithm + /// + public class StrangeCRC : IChecksum + { + readonly static uint[] crc32Table = { + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, + 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, + 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, + 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, + 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, + 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, + 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, + 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, + 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, + 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, + 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, + 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, + 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, + 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, + 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, + 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, + 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, + 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, + 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, + 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, + 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, + 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 + }; + + int globalCrc; + + /// + /// Initialise a default instance of + /// + public StrangeCRC() + { + Reset(); + } + + /// + /// Reset the state of Crc. + /// + public void Reset() + { + globalCrc = -1; + } + + /// + /// Get the current Crc value. + /// + public long Value { + get { + return ~globalCrc; + } + } + + /// + /// Update the Crc value. + /// + /// data update is based on + public void Update(int value) + { + int temp = (globalCrc >> 24) ^ value; + if (temp < 0) { + temp = 256 + temp; + } + globalCrc = unchecked((int)((globalCrc << 8) ^ crc32Table[temp])); + } + + /// + /// Update Crc based on a block of data + /// + /// The buffer containing data to update the crc with. + public void Update(byte[] buffer) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + Update(buffer, 0, buffer.Length); + } + + /// + /// Update Crc based on a portion of a block of data + /// + /// block of data + /// index of first byte to use + /// number of bytes to use + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "cannot be less than zero"); +#endif + } + + if ( count < 0 ) + { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "cannot be less than zero"); +#endif + } + + if ( offset + count > buffer.Length ) + { + throw new ArgumentOutOfRangeException("count"); + } + + for (int i = 0; i < count; ++i) { + Update(buffer[offset++]); + } + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs b/src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs new file mode 100644 index 000000000..52c8bd2e1 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/FileSystemScanner.cs @@ -0,0 +1,533 @@ +// FileSystemScanner.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + #region EventArgs + /// + /// Event arguments for scanning. + /// + public class ScanEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The file or directory name. + public ScanEventArgs(string name) + { + name_ = name; + } + #endregion + + /// + /// The file or directory name for this event. + /// + public string Name + { + get { return name_; } + } + + /// + /// Get set a value indicating if scanning should continue or not. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + #region Instance Fields + string name_; + bool continueRunning_ = true; + #endregion + } + + /// + /// Event arguments during processing of a single file or directory. + /// + public class ProgressEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The file or directory name if known. + /// The number of bytes processed so far + /// The total number of bytes to process, 0 if not known + public ProgressEventArgs(string name, long processed, long target) + { + name_ = name; + processed_ = processed; + target_ = target; + } + #endregion + + /// + /// The name for this event if known. + /// + public string Name + { + get { return name_; } + } + + /// + /// Get set a value indicating wether scanning should continue or not. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + /// + /// Get a percentage representing how much of the has been processed + /// + /// 0.0 to 100.0 percent; 0 if target is not known. + public float PercentComplete + { + get + { + float result; + if (target_ <= 0) + { + result = 0; + } + else + { + result = ((float)processed_ / (float)target_) * 100.0f; + } + return result; + } + } + + /// + /// The number of bytes processed so far + /// + public long Processed + { + get { return processed_; } + } + + /// + /// The number of bytes to process. + /// + /// Target may be 0 or negative if the value isnt known. + public long Target + { + get { return target_; } + } + + #region Instance Fields + string name_; + long processed_; + long target_; + bool continueRunning_ = true; + #endregion + } + + /// + /// Event arguments for directories. + /// + public class DirectoryEventArgs : ScanEventArgs + { + #region Constructors + /// + /// Initialize an instance of . + /// + /// The name for this directory. + /// Flag value indicating if any matching files are contained in this directory. + public DirectoryEventArgs(string name, bool hasMatchingFiles) + : base (name) + { + hasMatchingFiles_ = hasMatchingFiles; + } + #endregion + + /// + /// Get a value indicating if the directory contains any matching files or not. + /// + public bool HasMatchingFiles + { + get { return hasMatchingFiles_; } + } + + #region Instance Fields + bool hasMatchingFiles_; + #endregion + } + + /// + /// Arguments passed when scan failures are detected. + /// + public class ScanFailureEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The name to apply. + /// The exception to use. + public ScanFailureEventArgs(string name, Exception e) + { + name_ = name; + exception_ = e; + continueRunning_ = true; + } + #endregion + + /// + /// The applicable name. + /// + public string Name + { + get { return name_; } + } + + /// + /// The applicable exception. + /// + public Exception Exception + { + get { return exception_; } + } + + /// + /// Get / set a value indicating wether scanning should continue. + /// + public bool ContinueRunning + { + get { return continueRunning_; } + set { continueRunning_ = value; } + } + + #region Instance Fields + string name_; + Exception exception_; + bool continueRunning_; + #endregion + } + + #endregion + + #region Delegates + /// + /// Delegate invoked before starting to process a directory. + /// + public delegate void ProcessDirectoryHandler(object sender, DirectoryEventArgs e); + + /// + /// Delegate invoked before starting to process a file. + /// + /// The source of the event + /// The event arguments. + public delegate void ProcessFileHandler(object sender, ScanEventArgs e); + + /// + /// Delegate invoked during processing of a file or directory + /// + /// The source of the event + /// The event arguments. + public delegate void ProgressHandler(object sender, ProgressEventArgs e); + + /// + /// Delegate invoked when a file has been completely processed. + /// + /// The source of the event + /// The event arguments. + public delegate void CompletedFileHandler(object sender, ScanEventArgs e); + + /// + /// Delegate invoked when a directory failure is detected. + /// + /// The source of the event + /// The event arguments. + public delegate void DirectoryFailureHandler(object sender, ScanFailureEventArgs e); + + /// + /// Delegate invoked when a file failure is detected. + /// + /// The source of the event + /// The event arguments. + public delegate void FileFailureHandler(object sender, ScanFailureEventArgs e); + #endregion + + /// + /// FileSystemScanner provides facilities scanning of files and directories. + /// + public class FileSystemScanner + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The file filter to apply when scanning. + public FileSystemScanner(string filter) + { + fileFilter_ = new PathFilter(filter); + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + /// The directory filter to apply. + public FileSystemScanner(string fileFilter, string directoryFilter) + { + fileFilter_ = new PathFilter(fileFilter); + directoryFilter_ = new PathFilter(directoryFilter); + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + public FileSystemScanner(IScanFilter fileFilter) + { + fileFilter_ = fileFilter; + } + + /// + /// Initialise a new instance of + /// + /// The file filter to apply. + /// The directory filter to apply. + public FileSystemScanner(IScanFilter fileFilter, IScanFilter directoryFilter) + { + fileFilter_ = fileFilter; + directoryFilter_ = directoryFilter; + } + #endregion + + #region Delegates + /// + /// Delegate to invoke when a directory is processed. + /// + public ProcessDirectoryHandler ProcessDirectory; + + /// + /// Delegate to invoke when a file is processed. + /// + public ProcessFileHandler ProcessFile; + + /// + /// Delegate to invoke when processing for a file has finished. + /// + public CompletedFileHandler CompletedFile; + + /// + /// Delegate to invoke when a directory failure is detected. + /// + public DirectoryFailureHandler DirectoryFailure; + + /// + /// Delegate to invoke when a file failure is detected. + /// + public FileFailureHandler FileFailure; + #endregion + + /// + /// Raise the DirectoryFailure event. + /// + /// The directory name. + /// The exception detected. + bool OnDirectoryFailure(string directory, Exception e) + { + DirectoryFailureHandler handler = DirectoryFailure; + bool result = (handler != null); + if ( result ) { + ScanFailureEventArgs args = new ScanFailureEventArgs(directory, e); + handler(this, args); + alive_ = args.ContinueRunning; + } + return result; + } + + /// + /// Raise the FileFailure event. + /// + /// The file name. + /// The exception detected. + bool OnFileFailure(string file, Exception e) + { + FileFailureHandler handler = FileFailure; + + bool result = (handler != null); + + if ( result ){ + ScanFailureEventArgs args = new ScanFailureEventArgs(file, e); + FileFailure(this, args); + alive_ = args.ContinueRunning; + } + return result; + } + + /// + /// Raise the ProcessFile event. + /// + /// The file name. + void OnProcessFile(string file) + { + ProcessFileHandler handler = ProcessFile; + + if ( handler!= null ) { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Raise the complete file event + /// + /// The file name + void OnCompleteFile(string file) + { + CompletedFileHandler handler = CompletedFile; + + if (handler != null) + { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Raise the ProcessDirectory event. + /// + /// The directory name. + /// Flag indicating if the directory has matching files. + void OnProcessDirectory(string directory, bool hasMatchingFiles) + { + ProcessDirectoryHandler handler = ProcessDirectory; + + if ( handler != null ) { + DirectoryEventArgs args = new DirectoryEventArgs(directory, hasMatchingFiles); + handler(this, args); + alive_ = args.ContinueRunning; + } + } + + /// + /// Scan a directory. + /// + /// The base directory to scan. + /// True to recurse subdirectories, false to scan a single directory. + public void Scan(string directory, bool recurse) + { + alive_ = true; + ScanDir(directory, recurse); + } + + void ScanDir(string directory, bool recurse) + { + + try { + string[] names = System.IO.Directory.GetFiles(directory); + bool hasMatch = false; + for (int fileIndex = 0; fileIndex < names.Length; ++fileIndex) { + if ( !fileFilter_.IsMatch(names[fileIndex]) ) { + names[fileIndex] = null; + } else { + hasMatch = true; + } + } + + OnProcessDirectory(directory, hasMatch); + + if ( alive_ && hasMatch ) { + foreach (string fileName in names) { + try { + if ( fileName != null ) { + OnProcessFile(fileName); + if ( !alive_ ) { + break; + } + } + } + catch (Exception e) { + if (!OnFileFailure(fileName, e)) { + throw; + } + } + } + } + } + catch (Exception e) { + if (!OnDirectoryFailure(directory, e)) { + throw; + } + } + + if ( alive_ && recurse ) { + try { + string[] names = System.IO.Directory.GetDirectories(directory); + foreach (string fulldir in names) { + if ((directoryFilter_ == null) || (directoryFilter_.IsMatch(fulldir))) { + ScanDir(fulldir, true); + if ( !alive_ ) { + break; + } + } + } + } + catch (Exception e) { + if (!OnDirectoryFailure(directory, e)) { + throw; + } + } + } + } + + #region Instance Fields + /// + /// The file filter currently in use. + /// + IScanFilter fileFilter_; + /// + /// The directory filter currently in use. + /// + IScanFilter directoryFilter_; + /// + /// Flag indicating if scanning should continue running. + /// + bool alive_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/INameTransform.cs b/src/GitHub.Api/SharpZipLib/Core/INameTransform.cs new file mode 100644 index 000000000..7e5b025ae --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/INameTransform.cs @@ -0,0 +1,57 @@ +// INameTransform.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// INameTransform defines how file system names are transformed for use with archives, or vice versa. + /// + public interface INameTransform + { + /// + /// Given a file name determine the transformed value. + /// + /// The name to transform. + /// The transformed file name. + string TransformFile(string name); + + /// + /// Given a directory name determine the transformed value. + /// + /// The name to transform. + /// The transformed directory name + string TransformDirectory(string name); + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs b/src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs new file mode 100644 index 000000000..1a29bca1f --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/IScanFilter.cs @@ -0,0 +1,50 @@ +// IScanFilter.cs +// +// Copyright 2006 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// Scanning filters support filtering of names. + /// + public interface IScanFilter + { + /// + /// Test a name to see if it 'matches' the filter. + /// + /// The name to test. + /// Returns true if the name matches the filter, false if it does not match. + bool IsMatch(string name); + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/NameFilter.cs b/src/GitHub.Api/SharpZipLib/Core/NameFilter.cs new file mode 100644 index 000000000..83fee778d --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/NameFilter.cs @@ -0,0 +1,290 @@ +// NameFilter.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 2010-03-03 Z-1654 Fixed bug where escape characters were excluded in SplitQuoted() + +using System; +using System.Collections; +using System.Text; +using System.Text.RegularExpressions; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// NameFilter is a string matching class which allows for both positive and negative + /// matching. + /// A filter is a sequence of independant regular expressions separated by semi-colons ';'. + /// To include a semi-colon it may be quoted as in \;. Each expression can be prefixed by a plus '+' sign or + /// a minus '-' sign to denote the expression is intended to include or exclude names. + /// If neither a plus or minus sign is found include is the default. + /// A given name is tested for inclusion before checking exclusions. Only names matching an include spec + /// and not matching an exclude spec are deemed to match the filter. + /// An empty filter matches any name. + /// + /// The following expression includes all name ending in '.dat' with the exception of 'dummy.dat' + /// "+\.dat$;-^dummy\.dat$" + /// + public class NameFilter : IScanFilter + { + #region Constructors + /// + /// Construct an instance based on the filter expression passed + /// + /// The filter expression. + public NameFilter(string filter) + { + filter_ = filter; + inclusions_ = new ArrayList(); + exclusions_ = new ArrayList(); + Compile(); + } + #endregion + + /// + /// Test a string to see if it is a valid regular expression. + /// + /// The expression to test. + /// True if expression is a valid false otherwise. + public static bool IsValidExpression(string expression) + { + bool result = true; + try { + Regex exp = new Regex(expression, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + catch (ArgumentException) { + result = false; + } + return result; + } + + /// + /// Test an expression to see if it is valid as a filter. + /// + /// The filter expression to test. + /// True if the expression is valid, false otherwise. + public static bool IsValidFilterExpression(string toTest) + { + if ( toTest == null ) { + throw new ArgumentNullException("toTest"); + } + + bool result = true; + + try { + string[] items = SplitQuoted(toTest); + for (int i = 0; i < items.Length; ++i) { + if ((items[i] != null) && (items[i].Length > 0)) { + string toCompile; + + if (items[i][0] == '+') { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else if (items[i][0] == '-') { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else { + toCompile = items[i]; + } + + Regex testRegex = new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Singleline); + } + } + } + catch (ArgumentException) { + result = false; + } + + return result; + } + + /// + /// Split a string into its component pieces + /// + /// The original string + /// Returns an array of values containing the individual filter elements. + public static string[] SplitQuoted(string original) + { + char escape = '\\'; + char[] separators = { ';' }; + + ArrayList result = new ArrayList(); + + if ((original != null) && (original.Length > 0)) { + int endIndex = -1; + StringBuilder b = new StringBuilder(); + + while (endIndex < original.Length) { + endIndex += 1; + if (endIndex >= original.Length) { + result.Add(b.ToString()); + } + else if (original[endIndex] == escape) { + endIndex += 1; + if (endIndex >= original.Length) { +#if NETCF_1_0 + throw new ArgumentException("Missing terminating escape character"); +#else + throw new ArgumentException("Missing terminating escape character", "original"); +#endif + } + // include escape if this is not an escaped separator + if (Array.IndexOf(separators, original[endIndex]) < 0) + b.Append(escape); + + b.Append(original[endIndex]); + } + else { + if (Array.IndexOf(separators, original[endIndex]) >= 0) { + result.Add(b.ToString()); + b.Length = 0; + } + else { + b.Append(original[endIndex]); + } + } + } + } + + return (string[])result.ToArray(typeof(string)); + } + + /// + /// Convert this filter to its string equivalent. + /// + /// The string equivalent for this filter. + public override string ToString() + { + return filter_; + } + + /// + /// Test a value to see if it is included by the filter. + /// + /// The value to test. + /// True if the value is included, false otherwise. + public bool IsIncluded(string name) + { + bool result = false; + if ( inclusions_.Count == 0 ) { + result = true; + } + else { + foreach ( Regex r in inclusions_ ) { + if ( r.IsMatch(name) ) { + result = true; + break; + } + } + } + return result; + } + + /// + /// Test a value to see if it is excluded by the filter. + /// + /// The value to test. + /// True if the value is excluded, false otherwise. + public bool IsExcluded(string name) + { + bool result = false; + foreach ( Regex r in exclusions_ ) { + if ( r.IsMatch(name) ) { + result = true; + break; + } + } + return result; + } + + #region IScanFilter Members + /// + /// Test a value to see if it matches the filter. + /// + /// The value to test. + /// True if the value matches, false otherwise. + public bool IsMatch(string name) + { + return (IsIncluded(name) && !IsExcluded(name)); + } + #endregion + + /// + /// Compile this filter. + /// + void Compile() + { + // TODO: Check to see if combining RE's makes it faster/smaller. + // simple scheme would be to have one RE for inclusion and one for exclusion. + if ( filter_ == null ) { + return; + } + + string[] items = SplitQuoted(filter_); + for ( int i = 0; i < items.Length; ++i ) { + if ( (items[i] != null) && (items[i].Length > 0) ) { + bool include = (items[i][0] != '-'); + string toCompile; + + if ( items[i][0] == '+' ) { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else if ( items[i][0] == '-' ) { + toCompile = items[i].Substring(1, items[i].Length - 1); + } + else { + toCompile = items[i]; + } + + // NOTE: Regular expressions can fail to compile here for a number of reasons that cause an exception + // these are left unhandled here as the caller is responsible for ensuring all is valid. + // several functions IsValidFilterExpression and IsValidExpression are provided for such checking + if ( include ) { + inclusions_.Add(new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)); + } + else { + exclusions_.Add(new Regex(toCompile, RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)); + } + } + } + } + + #region Instance Fields + string filter_; + ArrayList inclusions_; + ArrayList exclusions_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/PathFilter.cs b/src/GitHub.Api/SharpZipLib/Core/PathFilter.cs new file mode 100644 index 000000000..662bce785 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/PathFilter.cs @@ -0,0 +1,334 @@ +// PathFilter.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// PathFilter filters directories and files using a form of regular expressions + /// by full path name. + /// See NameFilter for more detail on filtering. + /// + public class PathFilter : IScanFilter + { + #region Constructors + /// + /// Initialise a new instance of . + /// + /// The filter expression to apply. + public PathFilter(string filter) + { + nameFilter_ = new NameFilter(filter); + } + #endregion + + #region IScanFilter Members + /// + /// Test a name to see if it matches the filter. + /// + /// The name to test. + /// True if the name matches, false otherwise. + /// is used to get the full path before matching. + public virtual bool IsMatch(string name) + { + bool result = false; + + if ( name != null ) { + string cooked = (name.Length > 0) ? Path.GetFullPath(name) : ""; + result = nameFilter_.IsMatch(cooked); + } + return result; + } + #endregion + + #region Instance Fields + NameFilter nameFilter_; + #endregion + } + + /// + /// ExtendedPathFilter filters based on name, file size, and the last write time of the file. + /// + /// Provides an example of how to customise filtering. + public class ExtendedPathFilter : PathFilter + { + #region Constructors + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + public ExtendedPathFilter(string filter, + long minSize, long maxSize) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + } + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum to include. + /// The maximum to include. + public ExtendedPathFilter(string filter, + DateTime minDate, DateTime maxDate) + : base(filter) + { + MinDate = minDate; + MaxDate = maxDate; + } + + /// + /// Initialise a new instance of ExtendedPathFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + /// The minimum to include. + /// The maximum to include. + public ExtendedPathFilter(string filter, + long minSize, long maxSize, + DateTime minDate, DateTime maxDate) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + MinDate = minDate; + MaxDate = maxDate; + } + #endregion + + #region IScanFilter Members + /// + /// Test a filename to see if it matches the filter. + /// + /// The filename to test. + /// True if the filter matches, false otherwise. + /// The doesnt exist + public override bool IsMatch(string name) + { + bool result = base.IsMatch(name); + + if ( result ) { + FileInfo fileInfo = new FileInfo(name); + result = + (MinSize <= fileInfo.Length) && + (MaxSize >= fileInfo.Length) && + (MinDate <= fileInfo.LastWriteTime) && + (MaxDate >= fileInfo.LastWriteTime) + ; + } + return result; + } + #endregion + + #region Properties + /// + /// Get/set the minimum size/length for a file that will match this filter. + /// + /// The default value is zero. + /// value is less than zero; greater than + public long MinSize + { + get { return minSize_; } + set + { + if ( (value < 0) || (maxSize_ < value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + minSize_ = value; + } + } + + /// + /// Get/set the maximum size/length for a file that will match this filter. + /// + /// The default value is + /// value is less than zero or less than + public long MaxSize + { + get { return maxSize_; } + set + { + if ( (value < 0) || (minSize_ > value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + maxSize_ = value; + } + } + + /// + /// Get/set the minimum value that will match for this filter. + /// + /// Files with a LastWrite time less than this value are excluded by the filter. + public DateTime MinDate + { + get + { + return minDate_; + } + + set + { + if ( value > maxDate_ ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "Exceeds MaxDate"); +#endif + } + + minDate_ = value; + } + } + + /// + /// Get/set the maximum value that will match for this filter. + /// + /// Files with a LastWrite time greater than this value are excluded by the filter. + public DateTime MaxDate + { + get + { + return maxDate_; + } + + set + { + if ( minDate_ > value ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "Exceeds MinDate"); +#endif + } + + maxDate_ = value; + } + } + #endregion + + #region Instance Fields + long minSize_; + long maxSize_ = long.MaxValue; + DateTime minDate_ = DateTime.MinValue; + DateTime maxDate_ = DateTime.MaxValue; + #endregion + } + + /// + /// NameAndSizeFilter filters based on name and file size. + /// + /// A sample showing how filters might be extended. + [Obsolete("Use ExtendedPathFilter instead")] + public class NameAndSizeFilter : PathFilter + { + + /// + /// Initialise a new instance of NameAndSizeFilter. + /// + /// The filter to apply. + /// The minimum file size to include. + /// The maximum file size to include. + public NameAndSizeFilter(string filter, long minSize, long maxSize) + : base(filter) + { + MinSize = minSize; + MaxSize = maxSize; + } + + /// + /// Test a filename to see if it matches the filter. + /// + /// The filename to test. + /// True if the filter matches, false otherwise. + public override bool IsMatch(string name) + { + bool result = base.IsMatch(name); + + if ( result ) { + FileInfo fileInfo = new FileInfo(name); + long length = fileInfo.Length; + result = + (MinSize <= length) && + (MaxSize >= length); + } + return result; + } + + /// + /// Get/set the minimum size for a file that will match this filter. + /// + public long MinSize + { + get { return minSize_; } + set { + if ( (value < 0) || (maxSize_ < value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + minSize_ = value; + } + } + + /// + /// Get/set the maximum size for a file that will match this filter. + /// + public long MaxSize + { + get { return maxSize_; } + set + { + if ( (value < 0) || (minSize_ > value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + maxSize_ = value; + } + } + + #region Instance Fields + long minSize_; + long maxSize_ = long.MaxValue; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs b/src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs new file mode 100644 index 000000000..59344d54d --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/StreamUtils.cs @@ -0,0 +1,246 @@ +// StreamUtils.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// Provides simple " utilities. + /// + public sealed class StreamUtils + { + /// + /// Read from a ensuring all the required data is read. + /// + /// The stream to read. + /// The buffer to fill. + /// + static public void ReadFully(Stream stream, byte[] buffer) + { + ReadFully(stream, buffer, 0, buffer.Length); + } + + /// + /// Read from a " ensuring all the required data is read. + /// + /// The stream to read data from. + /// The buffer to store data in. + /// The offset at which to begin storing data. + /// The number of bytes of data to store. + /// Required parameter is null + /// and or are invalid. + /// End of stream is encountered before all the data has been read. + static public void ReadFully(Stream stream, byte[] buffer, int offset, int count) + { + if ( stream == null ) { + throw new ArgumentNullException("stream"); + } + + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + // Offset can equal length when buffer and count are 0. + if ( (offset < 0) || (offset > buffer.Length) ) { + throw new ArgumentOutOfRangeException("offset"); + } + + if ( (count < 0) || (offset + count > buffer.Length) ) { + throw new ArgumentOutOfRangeException("count"); + } + + while ( count > 0 ) { + int readCount = stream.Read(buffer, offset, count); + if ( readCount <= 0 ) { + throw new EndOfStreamException(); + } + offset += readCount; + count -= readCount; + } + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + static public void Copy(Stream source, Stream destination, byte[] buffer) + { + if (source == null) { + throw new ArgumentNullException("source"); + } + + if (destination == null) { + throw new ArgumentNullException("destination"); + } + + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + // Ensure a reasonable size of buffer is used without being prohibitive. + if (buffer.Length < 128) { + throw new ArgumentException("Buffer is too small", "buffer"); + } + + bool copying = true; + + while (copying) { + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead > 0) { + destination.Write(buffer, 0, bytesRead); + } + else { + destination.Flush(); + copying = false; + } + } + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + /// The progress handler delegate to use. + /// The minimum between progress updates. + /// The source for this event. + /// The name to use with the event. + /// This form is specialised for use within #Zip to support events during archive operations. + static public void Copy(Stream source, Stream destination, + byte[] buffer, ProgressHandler progressHandler, TimeSpan updateInterval, object sender, string name) + { + Copy(source, destination, buffer, progressHandler, updateInterval, sender, name, -1); + } + + /// + /// Copy the contents of one to another. + /// + /// The stream to source data from. + /// The stream to write data to. + /// The buffer to use during copying. + /// The progress handler delegate to use. + /// The minimum between progress updates. + /// The source for this event. + /// The name to use with the event. + /// A predetermined fixed target value to use with progress updates. + /// If the value is negative the target is calculated by looking at the stream. + /// This form is specialised for use within #Zip to support events during archive operations. + static public void Copy(Stream source, Stream destination, + byte[] buffer, + ProgressHandler progressHandler, TimeSpan updateInterval, + object sender, string name, long fixedTarget) + { + if (source == null) { + throw new ArgumentNullException("source"); + } + + if (destination == null) { + throw new ArgumentNullException("destination"); + } + + if (buffer == null) { + throw new ArgumentNullException("buffer"); + } + + // Ensure a reasonable size of buffer is used without being prohibitive. + if (buffer.Length < 128) { + throw new ArgumentException("Buffer is too small", "buffer"); + } + + if (progressHandler == null) { + throw new ArgumentNullException("progressHandler"); + } + + bool copying = true; + + DateTime marker = DateTime.Now; + long processed = 0; + long target = 0; + + if (fixedTarget >= 0) { + target = fixedTarget; + } + else if (source.CanSeek) { + target = source.Length - source.Position; + } + + // Always fire 0% progress.. + ProgressEventArgs args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + + bool progressFired = true; + + while (copying) { + int bytesRead = source.Read(buffer, 0, buffer.Length); + if (bytesRead > 0) { + processed += bytesRead; + progressFired = false; + destination.Write(buffer, 0, bytesRead); + } + else { + destination.Flush(); + copying = false; + } + + if (DateTime.Now - marker > updateInterval) { + progressFired = true; + marker = DateTime.Now; + args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + + copying = args.ContinueRunning; + } + } + + if (!progressFired) { + args = new ProgressEventArgs(name, processed, target); + progressHandler(sender, args); + } + } + + /// + /// Initialise an instance of + /// + private StreamUtils() + { + // Do nothing. + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs b/src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs new file mode 100644 index 000000000..820461c56 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Core/WindowsPathUtils.cs @@ -0,0 +1,94 @@ +// WindowsPathUtils.cs +// +// Copyright 2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Core +{ + /// + /// WindowsPathUtils provides simple utilities for handling windows paths. + /// + public abstract class WindowsPathUtils + { + /// + /// Initializes a new instance of the class. + /// + internal WindowsPathUtils() + { + } + + /// + /// Remove any path root present in the path + /// + /// A containing path information. + /// The path with the root removed if it was present; path otherwise. + /// Unlike the class the path isnt otherwise checked for validity. + public static string DropPathRoot(string path) + { + string result = path; + + if ( (path != null) && (path.Length > 0) ) { + if ((path[0] == '\\') || (path[0] == '/')) { + // UNC name ? + if ((path.Length > 1) && ((path[1] == '\\') || (path[1] == '/'))) { + int index = 2; + int elements = 2; + + // Scan for two separate elements \\machine\share\restofpath + while ((index <= path.Length) && + (((path[index] != '\\') && (path[index] != '/')) || (--elements > 0))) { + index++; + } + + index++; + + if (index < path.Length) { + result = path.Substring(index); + } + else { + result = ""; + } + } + } + else if ((path.Length > 1) && (path[1] == ':')) { + int dropCount = 2; + if ((path.Length > 2) && ((path[2] == '\\') || (path[2] == '/'))) { + dropCount = 3; + } + result = result.Remove(0, dropCount); + } + } + return result; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs b/src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs new file mode 100644 index 000000000..e030aaa64 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Encryption/PkzipClassic.cs @@ -0,0 +1,498 @@ +// +// PkzipClassic encryption +// +// Copyright 2004 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. +// + + +#if !NETCF_1_0 + +using System; +using System.Security.Cryptography; +using GitHub.ICSharpCode.SharpZipLib.Checksums; + +namespace GitHub.ICSharpCode.SharpZipLib.Encryption +{ + /// + /// PkzipClassic embodies the classic or original encryption facilities used in Pkzip archives. + /// While it has been superceded by more recent and more powerful algorithms, its still in use and + /// is viable for preventing casual snooping + /// + public abstract class PkzipClassic : SymmetricAlgorithm + { + /// + /// Generates new encryption keys based on given seed + /// + /// The seed value to initialise keys with. + /// A new key value. + static public byte[] GenerateKeys(byte[] seed) + { + if ( seed == null ) { + throw new ArgumentNullException("seed"); + } + + if ( seed.Length == 0 ) { + throw new ArgumentException("Length is zero", "seed"); + } + + uint[] newKeys = new uint[] { + 0x12345678, + 0x23456789, + 0x34567890 + }; + + for (int i = 0; i < seed.Length; ++i) { + newKeys[0] = Crc32.ComputeCrc32(newKeys[0], seed[i]); + newKeys[1] = newKeys[1] + (byte)newKeys[0]; + newKeys[1] = newKeys[1] * 134775813 + 1; + newKeys[2] = Crc32.ComputeCrc32(newKeys[2], (byte)(newKeys[1] >> 24)); + } + + byte[] result = new byte[12]; + result[0] = (byte)(newKeys[0] & 0xff); + result[1] = (byte)((newKeys[0] >> 8) & 0xff); + result[2] = (byte)((newKeys[0] >> 16) & 0xff); + result[3] = (byte)((newKeys[0] >> 24) & 0xff); + result[4] = (byte)(newKeys[1] & 0xff); + result[5] = (byte)((newKeys[1] >> 8) & 0xff); + result[6] = (byte)((newKeys[1] >> 16) & 0xff); + result[7] = (byte)((newKeys[1] >> 24) & 0xff); + result[8] = (byte)(newKeys[2] & 0xff); + result[9] = (byte)((newKeys[2] >> 8) & 0xff); + result[10] = (byte)((newKeys[2] >> 16) & 0xff); + result[11] = (byte)((newKeys[2] >> 24) & 0xff); + return result; + } + } + + /// + /// PkzipClassicCryptoBase provides the low level facilities for encryption + /// and decryption using the PkzipClassic algorithm. + /// + class PkzipClassicCryptoBase + { + /// + /// Transform a single byte + /// + /// + /// The transformed value + /// + protected byte TransformByte() + { + uint temp = ((keys[2] & 0xFFFF) | 2); + return (byte)((temp * (temp ^ 1)) >> 8); + } + + /// + /// Set the key schedule for encryption/decryption. + /// + /// The data use to set the keys from. + protected void SetKeys(byte[] keyData) + { + if ( keyData == null ) { + throw new ArgumentNullException("keyData"); + } + + if ( keyData.Length != 12 ) { + throw new InvalidOperationException("Key length is not valid"); + } + + keys = new uint[3]; + keys[0] = (uint)((keyData[3] << 24) | (keyData[2] << 16) | (keyData[1] << 8) | keyData[0]); + keys[1] = (uint)((keyData[7] << 24) | (keyData[6] << 16) | (keyData[5] << 8) | keyData[4]); + keys[2] = (uint)((keyData[11] << 24) | (keyData[10] << 16) | (keyData[9] << 8) | keyData[8]); + } + + /// + /// Update encryption keys + /// + protected void UpdateKeys(byte ch) + { + keys[0] = Crc32.ComputeCrc32(keys[0], ch); + keys[1] = keys[1] + (byte)keys[0]; + keys[1] = keys[1] * 134775813 + 1; + keys[2] = Crc32.ComputeCrc32(keys[2], (byte)(keys[1] >> 24)); + } + + /// + /// Reset the internal state. + /// + protected void Reset() + { + keys[0] = 0; + keys[1] = 0; + keys[2] = 0; + } + + #region Instance Fields + uint[] keys; + #endregion + } + + /// + /// PkzipClassic CryptoTransform for encryption. + /// + class PkzipClassicEncryptCryptoTransform : PkzipClassicCryptoBase, ICryptoTransform + { + /// + /// Initialise a new instance of + /// + /// The key block to use. + internal PkzipClassicEncryptCryptoTransform(byte[] keyBlock) + { + SetKeys(keyBlock); + } + + #region ICryptoTransform Members + + /// + /// Transforms the specified region of the specified byte array. + /// + /// The input for which to compute the transform. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// The computed transform. + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] result = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, result, 0); + return result; + } + + /// + /// Transforms the specified region of the input byte array and copies + /// the resulting transform to the specified region of the output byte array. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + for (int i = inputOffset; i < inputOffset + inputCount; ++i) { + byte oldbyte = inputBuffer[i]; + outputBuffer[outputOffset++] = (byte)(inputBuffer[i] ^ TransformByte()); + UpdateKeys(oldbyte); + } + return inputCount; + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform + { + get { + return true; + } + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks + { + get { + return true; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Cleanup internal state. + /// + public void Dispose() + { + Reset(); + } + + #endregion + } + + + /// + /// PkzipClassic CryptoTransform for decryption. + /// + class PkzipClassicDecryptCryptoTransform : PkzipClassicCryptoBase, ICryptoTransform + { + /// + /// Initialise a new instance of . + /// + /// The key block to decrypt with. + internal PkzipClassicDecryptCryptoTransform(byte[] keyBlock) + { + SetKeys(keyBlock); + } + + #region ICryptoTransform Members + + /// + /// Transforms the specified region of the specified byte array. + /// + /// The input for which to compute the transform. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the byte array to use as data. + /// The computed transform. + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + byte[] result = new byte[inputCount]; + TransformBlock(inputBuffer, inputOffset, inputCount, result, 0); + return result; + } + + /// + /// Transforms the specified region of the input byte array and copies + /// the resulting transform to the specified region of the output byte array. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { + for (int i = inputOffset; i < inputOffset + inputCount; ++i) { + byte newByte = (byte)(inputBuffer[i] ^ TransformByte()); + outputBuffer[outputOffset++] = newByte; + UpdateKeys(newByte); + } + return inputCount; + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform + { + get { + return true; + } + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize + { + get { + return 1; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks + { + get { + return true; + } + } + + #endregion + + #region IDisposable Members + + /// + /// Cleanup internal state. + /// + public void Dispose() + { + Reset(); + } + + #endregion + } + + /// + /// Defines a wrapper object to access the Pkzip algorithm. + /// This class cannot be inherited. + /// + public sealed class PkzipClassicManaged : PkzipClassic + { + /// + /// Get / set the applicable block size in bits. + /// + /// The only valid block size is 8. + public override int BlockSize + { + get { + return 8; + } + + set { + if (value != 8) { + throw new CryptographicException("Block size is invalid"); + } + } + } + + /// + /// Get an array of legal key sizes. + /// + public override KeySizes[] LegalKeySizes + { + get { + KeySizes[] keySizes = new KeySizes[1]; + keySizes[0] = new KeySizes(12 * 8, 12 * 8, 0); + return keySizes; + } + } + + /// + /// Generate an initial vector. + /// + public override void GenerateIV() + { + // Do nothing. + } + + /// + /// Get an array of legal block sizes. + /// + public override KeySizes[] LegalBlockSizes + { + get { + KeySizes[] keySizes = new KeySizes[1]; + keySizes[0] = new KeySizes(1 * 8, 1 * 8, 0); + return keySizes; + } + } + + /// + /// Get / set the key value applicable. + /// + public override byte[] Key + { + get { + if ( key_ == null ) { + GenerateKey(); + } + + return (byte[]) key_.Clone(); + } + + set { + if ( value == null ) { + throw new ArgumentNullException("value"); + } + + if ( value.Length != 12 ) { + throw new CryptographicException("Key size is illegal"); + } + + key_ = (byte[]) value.Clone(); + } + } + + /// + /// Generate a new random key. + /// + public override void GenerateKey() + { + key_ = new byte[12]; + Random rnd = new Random(); + rnd.NextBytes(key_); + } + + /// + /// Create an encryptor. + /// + /// The key to use for this encryptor. + /// Initialisation vector for the new encryptor. + /// Returns a new PkzipClassic encryptor + public override ICryptoTransform CreateEncryptor( + byte[] rgbKey, + byte[] rgbIV) + { + key_ = rgbKey; + return new PkzipClassicEncryptCryptoTransform(Key); + } + + /// + /// Create a decryptor. + /// + /// Keys to use for this new decryptor. + /// Initialisation vector for the new decryptor. + /// Returns a new decryptor. + public override ICryptoTransform CreateDecryptor( + byte[] rgbKey, + byte[] rgbIV) + { + key_ = rgbKey; + return new PkzipClassicDecryptCryptoTransform(Key); + } + + #region Instance Fields + byte[] key_; + #endregion + } +} +#endif diff --git a/src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs new file mode 100644 index 000000000..8721b4a92 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESStream.cs @@ -0,0 +1,170 @@ +// +// ZipAESStream.cs +// +// Copyright 2009 David Pierson +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. +// + +#if !NET_1_1 && !NETCF_2_0 + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace GitHub.ICSharpCode.SharpZipLib.Encryption { + + // Based on information from http://www.winzip.com/aes_info.htm + // and http://www.gladman.me.uk/cryptography_technology/fileencrypt/ + + /// + /// Encrypts and decrypts AES ZIP + /// + internal class ZipAESStream : CryptoStream { + + /// + /// Constructor + /// + /// The stream on which to perform the cryptographic transformation. + /// Instance of ZipAESTransform + /// Read or Write + public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode mode) + : base(stream, transform, mode) { + + _stream = stream; + _transform = transform; + _slideBuffer = new byte[1024]; + + _blockAndAuth = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH; + + // mode: + // CryptoStreamMode.Read means we read from "stream" and pass decrypted to our Read() method. + // Write bypasses this stream and uses the Transform directly. + if (mode != CryptoStreamMode.Read) { + throw new Exception("ZipAESStream only for read"); + } + } + + // The final n bytes of the AES stream contain the Auth Code. + private const int AUTH_CODE_LENGTH = 10; + + private Stream _stream; + private ZipAESTransform _transform; + private byte[] _slideBuffer; + private int _slideBufStartPos; + private int _slideBufFreePos; + // Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32. + private const int CRYPTO_BLOCK_SIZE = 16; + private int _blockAndAuth; + + /// + /// Reads a sequence of bytes from the current CryptoStream into buffer, + /// and advances the position within the stream by the number of bytes read. + /// + public override int Read(byte[] outBuffer, int offset, int count) { + int nBytes = 0; + while (nBytes < count) { + // Calculate buffer quantities vs read-ahead size, and check for sufficient free space + int byteCount = _slideBufFreePos - _slideBufStartPos; + + // Need to handle final block and Auth Code specially, but don't know total data length. + // Maintain a read-ahead equal to the length of (crypto block + Auth Code). + // When that runs out we can detect these final sections. + int lengthToRead = _blockAndAuth - byteCount; + if (_slideBuffer.Length - _slideBufFreePos < lengthToRead) { + // Shift the data to the beginning of the buffer + int iTo = 0; + for (int iFrom = _slideBufStartPos; iFrom < _slideBufFreePos; iFrom++, iTo++) { + _slideBuffer[iTo] = _slideBuffer[iFrom]; + } + _slideBufFreePos -= _slideBufStartPos; // Note the -= + _slideBufStartPos = 0; + } + int obtained = _stream.Read(_slideBuffer, _slideBufFreePos, lengthToRead); + _slideBufFreePos += obtained; + + // Recalculate how much data we now have + byteCount = _slideBufFreePos - _slideBufStartPos; + if (byteCount >= _blockAndAuth) { + // At least a 16 byte block and an auth code remains. + _transform.TransformBlock(_slideBuffer, + _slideBufStartPos, + CRYPTO_BLOCK_SIZE, + outBuffer, + offset); + nBytes += CRYPTO_BLOCK_SIZE; + offset += CRYPTO_BLOCK_SIZE; + _slideBufStartPos += CRYPTO_BLOCK_SIZE; + } else { + // Last round. + if (byteCount > AUTH_CODE_LENGTH) { + // At least one byte of data plus auth code + int finalBlock = byteCount - AUTH_CODE_LENGTH; + _transform.TransformBlock(_slideBuffer, + _slideBufStartPos, + finalBlock, + outBuffer, + offset); + + nBytes += finalBlock; + _slideBufStartPos += finalBlock; + } + else if (byteCount < AUTH_CODE_LENGTH) + throw new Exception("Internal error missed auth code"); // Coding bug + // Final block done. Check Auth code. + byte[] calcAuthCode = _transform.GetAuthCode(); + for (int i = 0; i < AUTH_CODE_LENGTH; i++) { + if (calcAuthCode[i] != _slideBuffer[_slideBufStartPos + i]) { + throw new Exception("AES Authentication Code does not match. This is a super-CRC check on the data in the file after compression and encryption. \r\n" + + "The file may be damaged."); + } + } + + break; // Reached the auth code + } + } + return nBytes; + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) { + // ZipAESStream is used for reading but not for writing. Writing uses the ZipAESTransform directly. + throw new NotImplementedException(); + } + } +} +#endif \ No newline at end of file diff --git a/src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs new file mode 100644 index 000000000..002036cc3 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Encryption/ZipAESTransform.cs @@ -0,0 +1,219 @@ +// +// ZipAESTransform.cs +// +// Copyright 2009 David Pierson +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. +// + +#if !NET_1_1 && !NETCF_2_0 +// Framework version 2.0 required for Rfc2898DeriveBytes + +using System; +using System.Security.Cryptography; + +namespace GitHub.ICSharpCode.SharpZipLib.Encryption { + + /// + /// Transforms stream using AES in CTR mode + /// + internal class ZipAESTransform : ICryptoTransform { + + private const int PWD_VER_LENGTH = 2; + + // WinZip use iteration count of 1000 for PBKDF2 key generation + private const int KEY_ROUNDS = 1000; + + // For 128-bit AES (16 bytes) the encryption is implemented as expected. + // For 256-bit AES (32 bytes) WinZip do full 256 bit AES of the nonce to create the encryption + // block but use only the first 16 bytes of it, and discard the second half. + private const int ENCRYPT_BLOCK = 16; + + private int _blockSize; + private ICryptoTransform _encryptor; + private readonly byte[] _counterNonce; + private byte[] _encryptBuffer; + private int _encrPos; + private byte[] _pwdVerifier; + private HMACSHA1 _hmacsha1; + private bool _finalised; + + private bool _writeMode; + + /// + /// Constructor. + /// + /// Password string + /// Random bytes, length depends on encryption strength. + /// 128 bits = 8 bytes, 192 bits = 12 bytes, 256 bits = 16 bytes. + /// The encryption strength, in bytes eg 16 for 128 bits. + /// True when creating a zip, false when reading. For the AuthCode. + /// + public ZipAESTransform(string key, byte[] saltBytes, int blockSize, bool writeMode) { + + if (blockSize != 16 && blockSize != 32) // 24 valid for AES but not supported by Winzip + throw new Exception("Invalid blocksize " + blockSize + ". Must be 16 or 32."); + if (saltBytes.Length != blockSize / 2) + throw new Exception("Invalid salt len. Must be " + blockSize / 2 + " for blocksize " + blockSize); + // initialise the encryption buffer and buffer pos + _blockSize = blockSize; + _encryptBuffer = new byte[_blockSize]; + _encrPos = ENCRYPT_BLOCK; + + // Performs the equivalent of derive_key in Dr Brian Gladman's pwd2key.c + Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(key, saltBytes, KEY_ROUNDS); + RijndaelManaged rm = new RijndaelManaged(); + rm.Mode = CipherMode.ECB; // No feedback from cipher for CTR mode + _counterNonce = new byte[_blockSize]; + byte[] byteKey1 = pdb.GetBytes(_blockSize); + byte[] byteKey2 = pdb.GetBytes(_blockSize); + _encryptor = rm.CreateEncryptor(byteKey1, byteKey2); + _pwdVerifier = pdb.GetBytes(PWD_VER_LENGTH); + // + _hmacsha1 = new HMACSHA1(byteKey2); + _writeMode = writeMode; + } + + /// + /// Implement the ICryptoTransform method. + /// + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) { + + // Pass the data stream to the hash algorithm for generating the Auth Code. + // This does not change the inputBuffer. Do this before decryption for read mode. + if (!_writeMode) { + _hmacsha1.TransformBlock(inputBuffer, inputOffset, inputCount, inputBuffer, inputOffset); + } + // Encrypt with AES in CTR mode. Regards to Dr Brian Gladman for this. + int ix = 0; + while (ix < inputCount) { + if (_encrPos == ENCRYPT_BLOCK) { + /* increment encryption nonce */ + int j = 0; + while (++_counterNonce[j] == 0) { + ++j; + } + /* encrypt the nonce to form next xor buffer */ + _encryptor.TransformBlock(_counterNonce, 0, _blockSize, _encryptBuffer, 0); + _encrPos = 0; + } + outputBuffer[ix + outputOffset] = (byte)(inputBuffer[ix + inputOffset] ^ _encryptBuffer[_encrPos++]); + // + ix++; + } + if (_writeMode) { + // This does not change the buffer. + _hmacsha1.TransformBlock(outputBuffer, outputOffset, inputCount, outputBuffer, outputOffset); + } + return inputCount; + } + + /// + /// Returns the 2 byte password verifier + /// + public byte[] PwdVerifier { + get { + return _pwdVerifier; + } + } + + /// + /// Returns the 10 byte AUTH CODE to be checked or appended immediately following the AES data stream. + /// + public byte[] GetAuthCode() { + // We usually don't get advance notice of final block. Hash requres a TransformFinal. + if (!_finalised) { + byte[] dummy = new byte[0]; + _hmacsha1.TransformFinalBlock(dummy, 0, 0); + _finalised = true; + } + return _hmacsha1.Hash; + } + + #region ICryptoTransform Members + + /// + /// Not implemented. + /// + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) { + + throw new NotImplementedException("ZipAESTransform.TransformFinalBlock"); + } + + /// + /// Gets the size of the input data blocks in bytes. + /// + public int InputBlockSize { + get { + return _blockSize; + } + } + + /// + /// Gets the size of the output data blocks in bytes. + /// + public int OutputBlockSize { + get { + return _blockSize; + } + } + + /// + /// Gets a value indicating whether multiple blocks can be transformed. + /// + public bool CanTransformMultipleBlocks { + get { + return true; + } + } + + /// + /// Gets a value indicating whether the current transform can be reused. + /// + public bool CanReuseTransform { + get { + return true; + } + } + + /// + /// Cleanup internal state. + /// + public void Dispose() { + _encryptor.Dispose(); + } + + #endregion + + } +} +#endif \ No newline at end of file diff --git a/src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs b/src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs new file mode 100644 index 000000000..227a951cb --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/SharpZipBaseException.cs @@ -0,0 +1,94 @@ +// SharpZipBaseException.cs +// +// Copyright 2004 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +#if !NETCF_1_0 && !NETCF_2_0 +using System.Runtime.Serialization; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib +{ + /// + /// SharpZipBaseException is the base exception class for the SharpZipLibrary. + /// All library exceptions are derived from this. + /// + /// NOTE: Not all exceptions thrown will be derived from this class. + /// A variety of other exceptions are possible for example +#if !NETCF_1_0 && !NETCF_2_0 + [Serializable] +#endif + public class SharpZipBaseException : ApplicationException + { +#if !NETCF_1_0 && !NETCF_2_0 + /// + /// Deserialization constructor + /// + /// for this constructor + /// for this constructor + protected SharpZipBaseException(SerializationInfo info, StreamingContext context ) + : base( info, context ) + { + } +#endif + + /// + /// Initializes a new instance of the SharpZipBaseException class. + /// + public SharpZipBaseException() + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with a specified error message. + /// + /// A message describing the exception. + public SharpZipBaseException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the SharpZipBaseException class with a specified + /// error message and a reference to the inner exception that is the cause of this exception. + /// + /// A message describing the exception. + /// The inner exception + public SharpZipBaseException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs new file mode 100644 index 000000000..3ba505385 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Deflater.cs @@ -0,0 +1,557 @@ +// Deflater.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This is the Deflater class. The deflater class compresses input + /// with the deflate algorithm described in RFC 1951. It has several + /// compression levels and three different strategies described below. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of deflate and setInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class Deflater + { + #region Deflater Documentation + /* + * The Deflater can do the following state transitions: + * + * (1) -> INIT_STATE ----> INIT_FINISHING_STATE ---. + * / | (2) (5) | + * / v (5) | + * (3)| SETDICT_STATE ---> SETDICT_FINISHING_STATE |(3) + * \ | (3) | ,--------' + * | | | (3) / + * v v (5) v v + * (1) -> BUSY_STATE ----> FINISHING_STATE + * | (6) + * v + * FINISHED_STATE + * \_____________________________________/ + * | (7) + * v + * CLOSED_STATE + * + * (1) If we should produce a header we start in INIT_STATE, otherwise + * we start in BUSY_STATE. + * (2) A dictionary may be set only when we are in INIT_STATE, then + * we change the state as indicated. + * (3) Whether a dictionary is set or not, on the first call of deflate + * we change to BUSY_STATE. + * (4) -- intentionally left blank -- :) + * (5) FINISHING_STATE is entered, when flush() is called to indicate that + * there is no more INPUT. There are also states indicating, that + * the header wasn't written yet. + * (6) FINISHED_STATE is entered, when everything has been flushed to the + * internal pending output buffer. + * (7) At any time (7) + * + */ + #endregion + #region Public Constants + /// + /// The best and slowest compression level. This tries to find very + /// long and distant string repetitions. + /// + public const int BEST_COMPRESSION = 9; + + /// + /// The worst but fastest compression level. + /// + public const int BEST_SPEED = 1; + + /// + /// The default compression level. + /// + public const int DEFAULT_COMPRESSION = -1; + + /// + /// This level won't compress at all but output uncompressed blocks. + /// + public const int NO_COMPRESSION = 0; + + /// + /// The compression method. This is the only method supported so far. + /// There is no need to use this constant at all. + /// + public const int DEFLATED = 8; + #endregion + #region Local Constants + private const int IS_SETDICT = 0x01; + private const int IS_FLUSHING = 0x04; + private const int IS_FINISHING = 0x08; + + private const int INIT_STATE = 0x00; + private const int SETDICT_STATE = 0x01; + // private static int INIT_FINISHING_STATE = 0x08; + // private static int SETDICT_FINISHING_STATE = 0x09; + private const int BUSY_STATE = 0x10; + private const int FLUSHING_STATE = 0x14; + private const int FINISHING_STATE = 0x1c; + private const int FINISHED_STATE = 0x1e; + private const int CLOSED_STATE = 0x7f; + #endregion + #region Constructors + /// + /// Creates a new deflater with default compression level. + /// + public Deflater() : this(DEFAULT_COMPRESSION, false) + { + + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION, or DEFAULT_COMPRESSION. + /// + /// if lvl is out of range. + public Deflater(int level) : this(level, false) + { + + } + + /// + /// Creates a new deflater with given compression level. + /// + /// + /// the compression level, a value between NO_COMPRESSION + /// and BEST_COMPRESSION. + /// + /// + /// true, if we should suppress the Zlib/RFC1950 header at the + /// beginning and the adler checksum at the end of the output. This is + /// useful for the GZIP/PKZIP formats. + /// + /// if lvl is out of range. + public Deflater(int level, bool noZlibHeaderOrFooter) + { + if (level == DEFAULT_COMPRESSION) { + level = 6; + } else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) { + throw new ArgumentOutOfRangeException("level"); + } + + pending = new DeflaterPending(); + engine = new DeflaterEngine(pending); + this.noZlibHeaderOrFooter = noZlibHeaderOrFooter; + SetStrategy(DeflateStrategy.Default); + SetLevel(level); + Reset(); + } + #endregion + + /// + /// Resets the deflater. The deflater acts afterwards as if it was + /// just created with the same compression level and strategy as it + /// had before. + /// + public void Reset() + { + state = (noZlibHeaderOrFooter ? BUSY_STATE : INIT_STATE); + totalOut = 0; + pending.Reset(); + engine.Reset(); + } + + /// + /// Gets the current adler checksum of the data that was processed so far. + /// + public int Adler { + get { + return engine.Adler; + } + } + + /// + /// Gets the number of input bytes processed so far. + /// + public long TotalIn { + get { + return engine.TotalIn; + } + } + + /// + /// Gets the number of output bytes so far. + /// + public long TotalOut { + get { + return totalOut; + } + } + + /// + /// Flushes the current input block. Further calls to deflate() will + /// produce enough output to inflate everything in the current input + /// block. This is not part of Sun's JDK so I have made it package + /// private. It is used by DeflaterOutputStream to implement + /// flush(). + /// + public void Flush() + { + state |= IS_FLUSHING; + } + + /// + /// Finishes the deflater with the current input block. It is an error + /// to give more input after this method was called. This method must + /// be called to force all bytes to be flushed. + /// + public void Finish() + { + state |= (IS_FLUSHING | IS_FINISHING); + } + + /// + /// Returns true if the stream was finished and no more output bytes + /// are available. + /// + public bool IsFinished { + get { + return (state == FINISHED_STATE) && pending.IsFlushed; + } + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method can also return true when the stream + /// was finished. + /// + public bool IsNeedingInput { + get { + return engine.NeedsInput(); + } + } + + /// + /// Sets the data which should be compressed next. This should be only + /// called when needsInput indicates that more input is needed. + /// If you call setInput when needsInput() returns false, the + /// previous input that is still pending will be thrown away. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// This call is equivalent to setInput(input, 0, input.length). + /// + /// + /// the buffer containing the input data. + /// + /// + /// if the buffer was finished() or ended(). + /// + public void SetInput(byte[] input) + { + SetInput(input, 0, input.Length); + } + + /// + /// Sets the data which should be compressed next. This should be + /// only called when needsInput indicates that more input is needed. + /// The given byte array should not be changed, before needsInput() returns + /// true again. + /// + /// + /// the buffer containing the input data. + /// + /// + /// the start of the data. + /// + /// + /// the number of data bytes of input. + /// + /// + /// if the buffer was Finish()ed or if previous input is still pending. + /// + public void SetInput(byte[] input, int offset, int count) + { + if ((state & IS_FINISHING) != 0) { + throw new InvalidOperationException("Finish() already called"); + } + engine.SetInput(input, offset, count); + } + + /// + /// Sets the compression level. There is no guarantee of the exact + /// position of the change, but if you call this when needsInput is + /// true the change of compression level will occur somewhere near + /// before the end of the so far given input. + /// + /// + /// the new compression level. + /// + public void SetLevel(int level) + { + if (level == DEFAULT_COMPRESSION) { + level = 6; + } else if (level < NO_COMPRESSION || level > BEST_COMPRESSION) { + throw new ArgumentOutOfRangeException("level"); + } + + if (this.level != level) { + this.level = level; + engine.SetLevel(level); + } + } + + /// + /// Get current compression level + /// + /// Returns the current compression level + public int GetLevel() { + return level; + } + + /// + /// Sets the compression strategy. Strategy is one of + /// DEFAULT_STRATEGY, HUFFMAN_ONLY and FILTERED. For the exact + /// position where the strategy is changed, the same as for + /// SetLevel() applies. + /// + /// + /// The new compression strategy. + /// + public void SetStrategy(DeflateStrategy strategy) + { + engine.Strategy = strategy; + } + + /// + /// Deflates the current input block with to the given array. + /// + /// + /// The buffer where compressed data is stored + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// IsNeedingInput() or IsFinished returns true or length is zero. + /// + public int Deflate(byte[] output) + { + return Deflate(output, 0, output.Length); + } + + /// + /// Deflates the current input block to the given array. + /// + /// + /// Buffer to store the compressed data. + /// + /// + /// Offset into the output array. + /// + /// + /// The maximum number of bytes that may be stored. + /// + /// + /// The number of compressed bytes added to the output, or 0 if either + /// needsInput() or finished() returns true or length is zero. + /// + /// + /// If Finish() was previously called. + /// + /// + /// If offset or length don't match the array length. + /// + public int Deflate(byte[] output, int offset, int length) + { + int origLength = length; + + if (state == CLOSED_STATE) { + throw new InvalidOperationException("Deflater closed"); + } + + if (state < BUSY_STATE) { + // output header + int header = (DEFLATED + + ((DeflaterConstants.MAX_WBITS - 8) << 4)) << 8; + int level_flags = (level - 1) >> 1; + if (level_flags < 0 || level_flags > 3) { + level_flags = 3; + } + header |= level_flags << 6; + if ((state & IS_SETDICT) != 0) { + // Dictionary was set + header |= DeflaterConstants.PRESET_DICT; + } + header += 31 - (header % 31); + + pending.WriteShortMSB(header); + if ((state & IS_SETDICT) != 0) { + int chksum = engine.Adler; + engine.ResetAdler(); + pending.WriteShortMSB(chksum >> 16); + pending.WriteShortMSB(chksum & 0xffff); + } + + state = BUSY_STATE | (state & (IS_FLUSHING | IS_FINISHING)); + } + + for (;;) { + int count = pending.Flush(output, offset, length); + offset += count; + totalOut += count; + length -= count; + + if (length == 0 || state == FINISHED_STATE) { + break; + } + + if (!engine.Deflate((state & IS_FLUSHING) != 0, (state & IS_FINISHING) != 0)) { + if (state == BUSY_STATE) { + // We need more input now + return origLength - length; + } else if (state == FLUSHING_STATE) { + if (level != NO_COMPRESSION) { + /* We have to supply some lookahead. 8 bit lookahead + * is needed by the zlib inflater, and we must fill + * the next byte, so that all bits are flushed. + */ + int neededbits = 8 + ((-pending.BitCount) & 7); + while (neededbits > 0) { + /* write a static tree block consisting solely of + * an EOF: + */ + pending.WriteBits(2, 10); + neededbits -= 10; + } + } + state = BUSY_STATE; + } else if (state == FINISHING_STATE) { + pending.AlignToByte(); + + // Compressed data is complete. Write footer information if required. + if (!noZlibHeaderOrFooter) { + int adler = engine.Adler; + pending.WriteShortMSB(adler >> 16); + pending.WriteShortMSB(adler & 0xffff); + } + state = FINISHED_STATE; + } + } + } + return origLength - length; + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// This call is equivalent to setDictionary(dict, 0, dict.Length). + /// + /// + /// the dictionary. + /// + /// + /// if SetInput () or Deflate () were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary) + { + SetDictionary(dictionary, 0, dictionary.Length); + } + + /// + /// Sets the dictionary which should be used in the deflate process. + /// The dictionary is a byte array containing strings that are + /// likely to occur in the data which should be compressed. The + /// dictionary is not stored in the compressed output, only a + /// checksum. To decompress the output you need to supply the same + /// dictionary again. + /// + /// + /// The dictionary data + /// + /// + /// The index where dictionary information commences. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// If SetInput () or Deflate() were already called or another dictionary was already set. + /// + public void SetDictionary(byte[] dictionary, int index, int count) + { + if (state != INIT_STATE) { + throw new InvalidOperationException(); + } + + state = SETDICT_STATE; + engine.SetDictionary(dictionary, index, count); + } + + #region Instance Fields + /// + /// Compression level. + /// + int level; + + /// + /// If true no Zlib/RFC1950 headers or footers are generated + /// + bool noZlibHeaderOrFooter; + + /// + /// The current state. + /// + int state; + + /// + /// The total bytes of output written. + /// + long totalOut; + + /// + /// The pending output. + /// + DeflaterPending pending; + + /// + /// The deflater engine. + /// + DeflaterEngine engine; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs new file mode 100644 index 000000000..abc68cbac --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterConstants.cs @@ -0,0 +1,186 @@ +// DeflaterConstants.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This class contains constants used for deflation. + /// + public class DeflaterConstants + { + /// + /// Set to true to enable debugging + /// + public const bool DEBUGGING = false; + + /// + /// Written to Zip file to identify a stored block + /// + public const int STORED_BLOCK = 0; + + /// + /// Identifies static tree in Zip file + /// + public const int STATIC_TREES = 1; + + /// + /// Identifies dynamic tree in Zip file + /// + public const int DYN_TREES = 2; + + /// + /// Header flag indicating a preset dictionary for deflation + /// + public const int PRESET_DICT = 0x20; + + /// + /// Sets internal buffer sizes for Huffman encoding + /// + public const int DEFAULT_MEM_LEVEL = 8; + + /// + /// Internal compression engine constant + /// + public const int MAX_MATCH = 258; + + /// + /// Internal compression engine constant + /// + public const int MIN_MATCH = 3; + + /// + /// Internal compression engine constant + /// + public const int MAX_WBITS = 15; + + /// + /// Internal compression engine constant + /// + public const int WSIZE = 1 << MAX_WBITS; + + /// + /// Internal compression engine constant + /// + public const int WMASK = WSIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; + + /// + /// Internal compression engine constant + /// + public const int HASH_SIZE = 1 << HASH_BITS; + + /// + /// Internal compression engine constant + /// + public const int HASH_MASK = HASH_SIZE - 1; + + /// + /// Internal compression engine constant + /// + public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; + + /// + /// Internal compression engine constant + /// + public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; + + /// + /// Internal compression engine constant + /// + public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; + + /// + /// Internal compression engine constant + /// + public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); + + /// + /// Internal compression engine constant + /// + public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_STORED = 0; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_FAST = 1; + + /// + /// Internal compression engine constant + /// + public const int DEFLATE_SLOW = 2; + + /// + /// Internal compression engine constant + /// + public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; + + /// + /// Internal compression engine constant + /// + public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; + + /// + /// Internal compression engine constant + /// + public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; + + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs new file mode 100644 index 000000000..f3a39d8e9 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterEngine.cs @@ -0,0 +1,869 @@ +// DeflaterEngine.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// Strategies for deflater + /// + public enum DeflateStrategy + { + /// + /// The default strategy + /// + Default = 0, + + /// + /// This strategy will only allow longer string repetitions. It is + /// useful for random data with a small character set. + /// + Filtered = 1, + + + /// + /// This strategy will not look for string repetitions at all. It + /// only encodes with Huffman trees (which means, that more common + /// characters get a smaller encoding. + /// + HuffmanOnly = 2 + } + + // DEFLATE ALGORITHM: + // + // The uncompressed stream is inserted into the window array. When + // the window array is full the first half is thrown away and the + // second half is copied to the beginning. + // + // The head array is a hash table. Three characters build a hash value + // and they the value points to the corresponding index in window of + // the last string with this hash. The prev array implements a + // linked list of matches with the same hash: prev[index & WMASK] points + // to the previous index with the same hash. + // + + + /// + /// Low level compression engine for deflate algorithm which uses a 32K sliding window + /// with secondary compression from Huffman/Shannon-Fano codes. + /// + public class DeflaterEngine : DeflaterConstants + { + #region Constants + const int TooFar = 4096; + #endregion + + #region Constructors + /// + /// Construct instance with pending buffer + /// + /// + /// Pending buffer to use + /// > + public DeflaterEngine(DeflaterPending pending) + { + this.pending = pending; + huffman = new DeflaterHuffman(pending); + adler = new Adler32(); + + window = new byte[2 * WSIZE]; + head = new short[HASH_SIZE]; + prev = new short[WSIZE]; + + // We start at index 1, to avoid an implementation deficiency, that + // we cannot build a repeat pattern at index 0. + blockStart = strstart = 1; + } + + #endregion + + /// + /// Deflate drives actual compression of data + /// + /// True to flush input buffers + /// Finish deflation with the current input. + /// Returns true if progress has been made. + public bool Deflate(bool flush, bool finish) + { + bool progress; + do + { + FillWindow(); + bool canFlush = flush && (inputOff == inputEnd); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("window: [" + blockStart + "," + strstart + "," + + lookahead + "], " + compressionFunction + "," + canFlush); + } +#endif + switch (compressionFunction) + { + case DEFLATE_STORED: + progress = DeflateStored(canFlush, finish); + break; + case DEFLATE_FAST: + progress = DeflateFast(canFlush, finish); + break; + case DEFLATE_SLOW: + progress = DeflateSlow(canFlush, finish); + break; + default: + throw new InvalidOperationException("unknown compressionFunction"); + } + } while (pending.IsFlushed && progress); // repeat while we have no pending output and progress was made + return progress; + } + + /// + /// Sets input data to be deflated. Should only be called when NeedsInput() + /// returns true + /// + /// The buffer containing input data. + /// The offset of the first byte of data. + /// The number of bytes of data to use as input. + public void SetInput(byte[] buffer, int offset, int count) + { + if ( buffer == null ) + { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) + { + throw new ArgumentOutOfRangeException("offset"); + } + + if ( count < 0 ) + { + throw new ArgumentOutOfRangeException("count"); + } + + if (inputOff < inputEnd) + { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + /* We want to throw an ArrayIndexOutOfBoundsException early. The + * check is very tricky: it also handles integer wrap around. + */ + if ((offset > end) || (end > buffer.Length) ) + { + throw new ArgumentOutOfRangeException("count"); + } + + inputBuf = buffer; + inputOff = offset; + inputEnd = end; + } + + /// + /// Determines if more input is needed. + /// + /// Return true if input is needed via SetInput + public bool NeedsInput() + { + return (inputEnd == inputOff); + } + + /// + /// Set compression dictionary + /// + /// The buffer containing the dictionary data + /// The offset in the buffer for the first byte of data + /// The length of the dictionary data. + public void SetDictionary(byte[] buffer, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (strstart != 1) ) + { + throw new InvalidOperationException("strstart not 1"); + } +#endif + adler.Update(buffer, offset, length); + if (length < MIN_MATCH) + { + return; + } + + if (length > MAX_DIST) + { + offset += length - MAX_DIST; + length = MAX_DIST; + } + + System.Array.Copy(buffer, offset, window, strstart, length); + + UpdateHash(); + --length; + while (--length > 0) + { + InsertString(); + strstart++; + } + strstart += 2; + blockStart = strstart; + } + + /// + /// Reset internal state + /// + public void Reset() + { + huffman.Reset(); + adler.Reset(); + blockStart = strstart = 1; + lookahead = 0; + totalIn = 0; + prevAvailable = false; + matchLen = MIN_MATCH - 1; + + for (int i = 0; i < HASH_SIZE; i++) { + head[i] = 0; + } + + for (int i = 0; i < WSIZE; i++) { + prev[i] = 0; + } + } + + /// + /// Reset Adler checksum + /// + public void ResetAdler() + { + adler.Reset(); + } + + /// + /// Get current value of Adler checksum + /// + public int Adler { + get { + return unchecked((int)adler.Value); + } + } + + /// + /// Total data processed + /// + public long TotalIn { + get { + return totalIn; + } + } + + /// + /// Get/set the deflate strategy + /// + public DeflateStrategy Strategy { + get { + return strategy; + } + set { + strategy = value; + } + } + + /// + /// Set the deflate level (0-9) + /// + /// The value to set the level to. + public void SetLevel(int level) + { + if ( (level < 0) || (level > 9) ) + { + throw new ArgumentOutOfRangeException("level"); + } + + goodLength = DeflaterConstants.GOOD_LENGTH[level]; + max_lazy = DeflaterConstants.MAX_LAZY[level]; + niceLength = DeflaterConstants.NICE_LENGTH[level]; + max_chain = DeflaterConstants.MAX_CHAIN[level]; + + if (DeflaterConstants.COMPR_FUNC[level] != compressionFunction) { + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.WriteLine("Change from " + compressionFunction + " to " + + DeflaterConstants.COMPR_FUNC[level]); + } +#endif + switch (compressionFunction) { + case DEFLATE_STORED: + if (strstart > blockStart) { + huffman.FlushStoredBlock(window, blockStart, + strstart - blockStart, false); + blockStart = strstart; + } + UpdateHash(); + break; + + case DEFLATE_FAST: + if (strstart > blockStart) { + huffman.FlushBlock(window, blockStart, strstart - blockStart, + false); + blockStart = strstart; + } + break; + + case DEFLATE_SLOW: + if (prevAvailable) { + huffman.TallyLit(window[strstart-1] & 0xff); + } + if (strstart > blockStart) { + huffman.FlushBlock(window, blockStart, strstart - blockStart, false); + blockStart = strstart; + } + prevAvailable = false; + matchLen = MIN_MATCH - 1; + break; + } + compressionFunction = COMPR_FUNC[level]; + } + } + + /// + /// Fill the window + /// + public void FillWindow() + { + /* If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (strstart >= WSIZE + MAX_DIST) + { + SlideWindow(); + } + + /* If there is not enough lookahead, but still some input left, + * read in the input + */ + while (lookahead < DeflaterConstants.MIN_LOOKAHEAD && inputOff < inputEnd) + { + int more = 2 * WSIZE - lookahead - strstart; + + if (more > inputEnd - inputOff) + { + more = inputEnd - inputOff; + } + + System.Array.Copy(inputBuf, inputOff, window, strstart + lookahead, more); + adler.Update(inputBuf, inputOff, more); + + inputOff += more; + totalIn += more; + lookahead += more; + } + + if (lookahead >= MIN_MATCH) + { + UpdateHash(); + } + } + + void UpdateHash() + { +/* + if (DEBUGGING) { + Console.WriteLine("updateHash: "+strstart); + } +*/ + ins_h = (window[strstart] << HASH_SHIFT) ^ window[strstart + 1]; + } + + /// + /// Inserts the current string in the head hash and returns the previous + /// value for this hash. + /// + /// The previous hash value + int InsertString() + { + short match; + int hash = ((ins_h << HASH_SHIFT) ^ window[strstart + (MIN_MATCH -1)]) & HASH_MASK; + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + if (hash != (((window[strstart] << (2*HASH_SHIFT)) ^ + (window[strstart + 1] << HASH_SHIFT) ^ + (window[strstart + 2])) & HASH_MASK)) { + throw new SharpZipBaseException("hash inconsistent: " + hash + "/" + +window[strstart] + "," + +window[strstart + 1] + "," + +window[strstart + 2] + "," + HASH_SHIFT); + } + } +#endif + prev[strstart & WMASK] = match = head[hash]; + head[hash] = unchecked((short)strstart); + ins_h = hash; + return match & 0xffff; + } + + void SlideWindow() + { + Array.Copy(window, WSIZE, window, 0, WSIZE); + matchStart -= WSIZE; + strstart -= WSIZE; + blockStart -= WSIZE; + + // Slide the hash table (could be avoided with 32 bit values + // at the expense of memory usage). + for (int i = 0; i < HASH_SIZE; ++i) { + int m = head[i] & 0xffff; + head[i] = (short)(m >= WSIZE ? (m - WSIZE) : 0); + } + + // Slide the prev table. + for (int i = 0; i < WSIZE; i++) { + int m = prev[i] & 0xffff; + prev[i] = (short)(m >= WSIZE ? (m - WSIZE) : 0); + } + } + + /// + /// Find the best (longest) string in the window matching the + /// string starting at strstart. + /// + /// Preconditions: + /// + /// strstart + MAX_MATCH <= window.length. + /// + /// + /// True if a match greater than the minimum length is found + bool FindLongestMatch(int curMatch) + { + int chainLength = this.max_chain; + int niceLength = this.niceLength; + short[] prev = this.prev; + int scan = this.strstart; + int match; + int best_end = this.strstart + matchLen; + int best_len = Math.Max(matchLen, MIN_MATCH - 1); + + int limit = Math.Max(strstart - MAX_DIST, 0); + + int strend = strstart + MAX_MATCH - 1; + byte scan_end1 = window[best_end - 1]; + byte scan_end = window[best_end]; + + // Do not waste too much time if we already have a good match: + if (best_len >= this.goodLength) { + chainLength >>= 2; + } + + /* Do not look for matches beyond the end of the input. This is necessary + * to make deflate deterministic. + */ + if (niceLength > lookahead) { + niceLength = lookahead; + } + +#if DebugDeflation + + if (DeflaterConstants.DEBUGGING && (strstart > 2 * WSIZE - MIN_LOOKAHEAD)) + { + throw new InvalidOperationException("need lookahead"); + } +#endif + + do { + +#if DebugDeflation + + if (DeflaterConstants.DEBUGGING && (curMatch >= strstart) ) + { + throw new InvalidOperationException("no future"); + } +#endif + if (window[curMatch + best_len] != scan_end || + window[curMatch + best_len - 1] != scan_end1 || + window[curMatch] != window[scan] || + window[curMatch + 1] != window[scan + 1]) { + continue; + } + + match = curMatch + 2; + scan += 2; + + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart + 258. + */ + while ( + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + window[++scan] == window[++match] && + (scan < strend)) + { + // Do nothing + } + + if (scan > best_end) { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (ins_h == 0) ) + Console.Error.WriteLine("Found match: " + curMatch + "-" + (scan - strstart)); +#endif + matchStart = curMatch; + best_end = scan; + best_len = scan - strstart; + + if (best_len >= niceLength) { + break; + } + + scan_end1 = window[best_end - 1]; + scan_end = window[best_end]; + } + scan = strstart; + } while ((curMatch = (prev[curMatch & WMASK] & 0xffff)) > limit && --chainLength != 0); + + matchLen = Math.Min(best_len, lookahead); + return matchLen >= MIN_MATCH; + } + + bool DeflateStored(bool flush, bool finish) + { + if (!flush && (lookahead == 0)) { + return false; + } + + strstart += lookahead; + lookahead = 0; + + int storedLength = strstart - blockStart; + + if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full + (blockStart < WSIZE && storedLength >= MAX_DIST) || // Block may move out of window + flush) { + bool lastBlock = finish; + if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) { + storedLength = DeflaterConstants.MAX_BLOCK_SIZE; + lastBlock = false; + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + Console.WriteLine("storedBlock[" + storedLength + "," + lastBlock + "]"); + } +#endif + + huffman.FlushStoredBlock(window, blockStart, storedLength, lastBlock); + blockStart += storedLength; + return !lastBlock; + } + return true; + } + + bool DeflateFast(bool flush, bool finish) + { + if (lookahead < MIN_LOOKAHEAD && !flush) { + return false; + } + + while (lookahead >= MIN_LOOKAHEAD || flush) { + if (lookahead == 0) { + // We are flushing everything + huffman.FlushBlock(window, blockStart, strstart - blockStart, finish); + blockStart = strstart; + return false; + } + + if (strstart > 2 * WSIZE - MIN_LOOKAHEAD) { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int hashHead; + if (lookahead >= MIN_MATCH && + (hashHead = InsertString()) != 0 && + strategy != DeflateStrategy.HuffmanOnly && + strstart - hashHead <= MAX_DIST && + FindLongestMatch(hashHead)) { + // longestMatch sets matchStart and matchLen +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart + i] != window[matchStart + i]) { + throw new SharpZipBaseException("Match failure"); + } + } + } +#endif + + bool full = huffman.TallyDist(strstart - matchStart, matchLen); + + lookahead -= matchLen; + if (matchLen <= max_lazy && lookahead >= MIN_MATCH) { + while (--matchLen > 0) { + ++strstart; + InsertString(); + } + ++strstart; + } else { + strstart += matchLen; + if (lookahead >= MIN_MATCH - 1) { + UpdateHash(); + } + } + matchLen = MIN_MATCH - 1; + if (!full) { + continue; + } + } else { + // No match found + huffman.TallyLit(window[strstart] & 0xff); + ++strstart; + --lookahead; + } + + if (huffman.IsFull()) { + bool lastBlock = finish && (lookahead == 0); + huffman.FlushBlock(window, blockStart, strstart - blockStart, lastBlock); + blockStart = strstart; + return !lastBlock; + } + } + return true; + } + + bool DeflateSlow(bool flush, bool finish) + { + if (lookahead < MIN_LOOKAHEAD && !flush) { + return false; + } + + while (lookahead >= MIN_LOOKAHEAD || flush) { + if (lookahead == 0) { + if (prevAvailable) { + huffman.TallyLit(window[strstart-1] & 0xff); + } + prevAvailable = false; + + // We are flushing everything +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && !flush) + { + throw new SharpZipBaseException("Not flushing, but no lookahead"); + } +#endif + huffman.FlushBlock(window, blockStart, strstart - blockStart, + finish); + blockStart = strstart; + return false; + } + + if (strstart >= 2 * WSIZE - MIN_LOOKAHEAD) { + /* slide window, as FindLongestMatch needs this. + * This should only happen when flushing and the window + * is almost full. + */ + SlideWindow(); + } + + int prevMatch = matchStart; + int prevLen = matchLen; + if (lookahead >= MIN_MATCH) { + + int hashHead = InsertString(); + + if (strategy != DeflateStrategy.HuffmanOnly && + hashHead != 0 && + strstart - hashHead <= MAX_DIST && + FindLongestMatch(hashHead)) { + + // longestMatch sets matchStart and matchLen + + // Discard match if too small and too far away + if (matchLen <= 5 && (strategy == DeflateStrategy.Filtered || (matchLen == MIN_MATCH && strstart - matchStart > TooFar))) { + matchLen = MIN_MATCH - 1; + } + } + } + + // previous match was better + if ((prevLen >= MIN_MATCH) && (matchLen <= prevLen) ) { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) + { + for (int i = 0 ; i < matchLen; i++) { + if (window[strstart-1+i] != window[prevMatch + i]) + throw new SharpZipBaseException(); + } + } +#endif + huffman.TallyDist(strstart - 1 - prevMatch, prevLen); + prevLen -= 2; + do { + strstart++; + lookahead--; + if (lookahead >= MIN_MATCH) { + InsertString(); + } + } while (--prevLen > 0); + + strstart ++; + lookahead--; + prevAvailable = false; + matchLen = MIN_MATCH - 1; + } else { + if (prevAvailable) { + huffman.TallyLit(window[strstart-1] & 0xff); + } + prevAvailable = true; + strstart++; + lookahead--; + } + + if (huffman.IsFull()) { + int len = strstart - blockStart; + if (prevAvailable) { + len--; + } + bool lastBlock = (finish && (lookahead == 0) && !prevAvailable); + huffman.FlushBlock(window, blockStart, len, lastBlock); + blockStart += len; + return !lastBlock; + } + } + return true; + } + + #region Instance Fields + + // Hash index of string to be inserted + int ins_h; + + /// + /// Hashtable, hashing three characters to an index for window, so + /// that window[index]..window[index+2] have this hash code. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + short[] head; + + /// + /// prev[index & WMASK] points to the previous index that has the + /// same hash code as the string starting at index. This way + /// entries with the same hash code are in a linked list. + /// Note that the array should really be unsigned short, so you need + /// to and the values with 0xffff. + /// + short[] prev; + + int matchStart; + // Length of best match + int matchLen; + // Set if previous match exists + bool prevAvailable; + int blockStart; + + /// + /// Points to the current character in the window. + /// + int strstart; + + /// + /// lookahead is the number of characters starting at strstart in + /// window that are valid. + /// So window[strstart] until window[strstart+lookahead-1] are valid + /// characters. + /// + int lookahead; + + /// + /// This array contains the part of the uncompressed stream that + /// is of relevance. The current character is indexed by strstart. + /// + byte[] window; + + DeflateStrategy strategy; + int max_chain, max_lazy, niceLength, goodLength; + + /// + /// The current compression function. + /// + int compressionFunction; + + /// + /// The input data for compression. + /// + byte[] inputBuf; + + /// + /// The total bytes of input read. + /// + long totalIn; + + /// + /// The offset into inputBuf, where input data starts. + /// + int inputOff; + + /// + /// The end offset of the input data. + /// + int inputEnd; + + DeflaterPending pending; + DeflaterHuffman huffman; + + /// + /// The adler checksum + /// + Adler32 adler; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs new file mode 100644 index 000000000..b7251e95f --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterHuffman.cs @@ -0,0 +1,908 @@ +// DeflaterHuffman.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This is the DeflaterHuffman class. + /// + /// This class is not thread safe. This is inherent in the API, due + /// to the split of Deflate and SetInput. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterHuffman + { + const int BUFSIZE = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); + const int LITERAL_NUM = 286; + + // Number of distance codes + const int DIST_NUM = 30; + // Number of codes used to transfer bit lengths + const int BITLEN_NUM = 19; + + // repeat previous bit length 3-6 times (2 bits of repeat count) + const int REP_3_6 = 16; + // repeat a zero length 3-10 times (3 bits of repeat count) + const int REP_3_10 = 17; + // repeat a zero length 11-138 times (7 bits of repeat count) + const int REP_11_138 = 18; + + const int EOF_SYMBOL = 256; + + // The lengths of the bit length codes are sent in order of decreasing + // probability, to avoid transmitting the lengths for unused bit length codes. + static readonly int[] BL_ORDER = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + static readonly byte[] bit4Reverse = { + 0, + 8, + 4, + 12, + 2, + 10, + 6, + 14, + 1, + 9, + 5, + 13, + 3, + 11, + 7, + 15 + }; + + static short[] staticLCodes; + static byte[] staticLLength; + static short[] staticDCodes; + static byte[] staticDLength; + + class Tree + { + #region Instance Fields + public short[] freqs; + + public byte[] length; + + public int minNumCodes; + + public int numCodes; + + short[] codes; + int[] bl_counts; + int maxLength; + DeflaterHuffman dh; + #endregion + + #region Constructors + public Tree(DeflaterHuffman dh, int elems, int minCodes, int maxLength) + { + this.dh = dh; + this.minNumCodes = minCodes; + this.maxLength = maxLength; + freqs = new short[elems]; + bl_counts = new int[maxLength]; + } + + #endregion + + /// + /// Resets the internal state of the tree + /// + public void Reset() + { + for (int i = 0; i < freqs.Length; i++) { + freqs[i] = 0; + } + codes = null; + length = null; + } + + public void WriteSymbol(int code) + { + // if (DeflaterConstants.DEBUGGING) { + // freqs[code]--; + // // Console.Write("writeSymbol("+freqs.length+","+code+"): "); + // } + dh.pending.WriteBits(codes[code] & 0xffff, length[code]); + } + + /// + /// Check that all frequencies are zero + /// + /// + /// At least one frequency is non-zero + /// + public void CheckEmpty() + { + bool empty = true; + for (int i = 0; i < freqs.Length; i++) { + if (freqs[i] != 0) { + //Console.WriteLine("freqs[" + i + "] == " + freqs[i]); + empty = false; + } + } + + if (!empty) { + throw new SharpZipBaseException("!Empty"); + } + } + + /// + /// Set static codes and length + /// + /// new codes + /// length for new codes + public void SetStaticCodes(short[] staticCodes, byte[] staticLengths) + { + codes = staticCodes; + length = staticLengths; + } + + /// + /// Build dynamic codes and lengths + /// + public void BuildCodes() + { + int numSymbols = freqs.Length; + int[] nextCode = new int[maxLength]; + int code = 0; + + codes = new short[freqs.Length]; + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("buildCodes: "+freqs.Length); + // } + + for (int bits = 0; bits < maxLength; bits++) { + nextCode[bits] = code; + code += bl_counts[bits] << (15 - bits); + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("bits: " + ( bits + 1) + " count: " + bl_counts[bits] + // +" nextCode: "+code); + // } + } + +#if DebugDeflation + if ( DeflaterConstants.DEBUGGING && (code != 65536) ) + { + throw new SharpZipBaseException("Inconsistent bl_counts!"); + } +#endif + for (int i=0; i < numCodes; i++) { + int bits = length[i]; + if (bits > 0) { + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("codes["+i+"] = rev(" + nextCode[bits-1]+"), + // +bits); + // } + + codes[i] = BitReverse(nextCode[bits-1]); + nextCode[bits-1] += 1 << (16 - bits); + } + } + } + + public void BuildTree() + { + int numSymbols = freqs.Length; + + /* heap is a priority queue, sorted by frequency, least frequent + * nodes first. The heap is a binary tree, with the property, that + * the parent node is smaller than both child nodes. This assures + * that the smallest node is the first parent. + * + * The binary tree is encoded in an array: 0 is root node and + * the nodes 2*n+1, 2*n+2 are the child nodes of node n. + */ + int[] heap = new int[numSymbols]; + int heapLen = 0; + int maxCode = 0; + for (int n = 0; n < numSymbols; n++) { + int freq = freqs[n]; + if (freq != 0) { + // Insert n into heap + int pos = heapLen++; + int ppos; + while (pos > 0 && freqs[heap[ppos = (pos - 1) / 2]] > freq) { + heap[pos] = heap[ppos]; + pos = ppos; + } + heap[pos] = n; + + maxCode = n; + } + } + + /* We could encode a single literal with 0 bits but then we + * don't see the literals. Therefore we force at least two + * literals to avoid this case. We don't care about order in + * this case, both literals get a 1 bit code. + */ + while (heapLen < 2) { + int node = maxCode < 2 ? ++maxCode : 0; + heap[heapLen++] = node; + } + + numCodes = Math.Max(maxCode + 1, minNumCodes); + + int numLeafs = heapLen; + int[] childs = new int[4 * heapLen - 2]; + int[] values = new int[2 * heapLen - 1]; + int numNodes = numLeafs; + for (int i = 0; i < heapLen; i++) { + int node = heap[i]; + childs[2 * i] = node; + childs[2 * i + 1] = -1; + values[i] = freqs[node] << 8; + heap[i] = i; + } + + /* Construct the Huffman tree by repeatedly combining the least two + * frequent nodes. + */ + do { + int first = heap[0]; + int last = heap[--heapLen]; + + // Propagate the hole to the leafs of the heap + int ppos = 0; + int path = 1; + + while (path < heapLen) { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path+1]]) { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = path * 2 + 1; + } + + /* Now propagate the last element down along path. Normally + * it shouldn't go too deep. + */ + int lastVal = values[last]; + while ((path = ppos) > 0 && values[heap[ppos = (path - 1)/2]] > lastVal) { + heap[path] = heap[ppos]; + } + heap[path] = last; + + + int second = heap[0]; + + // Create a new node father of first and second + last = numNodes++; + childs[2 * last] = first; + childs[2 * last + 1] = second; + int mindepth = Math.Min(values[first] & 0xff, values[second] & 0xff); + values[last] = lastVal = values[first] + values[second] - mindepth + 1; + + // Again, propagate the hole to the leafs + ppos = 0; + path = 1; + + while (path < heapLen) { + if (path + 1 < heapLen && values[heap[path]] > values[heap[path+1]]) { + path++; + } + + heap[ppos] = heap[path]; + ppos = path; + path = ppos * 2 + 1; + } + + // Now propagate the new element down along path + while ((path = ppos) > 0 && values[heap[ppos = (path - 1)/2]] > lastVal) { + heap[path] = heap[ppos]; + } + heap[path] = last; + } while (heapLen > 1); + + if (heap[0] != childs.Length / 2 - 1) { + throw new SharpZipBaseException("Heap invariant violated"); + } + + BuildLength(childs); + } + + /// + /// Get encoded length + /// + /// Encoded length, the sum of frequencies * lengths + public int GetEncodedLength() + { + int len = 0; + for (int i = 0; i < freqs.Length; i++) { + len += freqs[i] * length[i]; + } + return len; + } + + /// + /// Scan a literal or distance tree to determine the frequencies of the codes + /// in the bit length tree. + /// + public void CalcBLFreq(Tree blTree) + { + int max_count; /* max repeat count */ + int min_count; /* min repeat count */ + int count; /* repeat count of the current code */ + int curlen = -1; /* length of current code */ + + int i = 0; + while (i < numCodes) { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) { + max_count = 138; + min_count = 3; + } else { + max_count = 6; + min_count = 3; + if (curlen != nextlen) { + blTree.freqs[nextlen]++; + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) { + i++; + if (++count >= max_count) { + break; + } + } + + if (count < min_count) { + blTree.freqs[curlen] += (short)count; + } else if (curlen != 0) { + blTree.freqs[REP_3_6]++; + } else if (count <= 10) { + blTree.freqs[REP_3_10]++; + } else { + blTree.freqs[REP_11_138]++; + } + } + } + + /// + /// Write tree values + /// + /// Tree to write + public void WriteTree(Tree blTree) + { + int max_count; // max repeat count + int min_count; // min repeat count + int count; // repeat count of the current code + int curlen = -1; // length of current code + + int i = 0; + while (i < numCodes) { + count = 1; + int nextlen = length[i]; + if (nextlen == 0) { + max_count = 138; + min_count = 3; + } else { + max_count = 6; + min_count = 3; + if (curlen != nextlen) { + blTree.WriteSymbol(nextlen); + count = 0; + } + } + curlen = nextlen; + i++; + + while (i < numCodes && curlen == length[i]) { + i++; + if (++count >= max_count) { + break; + } + } + + if (count < min_count) { + while (count-- > 0) { + blTree.WriteSymbol(curlen); + } + } else if (curlen != 0) { + blTree.WriteSymbol(REP_3_6); + dh.pending.WriteBits(count - 3, 2); + } else if (count <= 10) { + blTree.WriteSymbol(REP_3_10); + dh.pending.WriteBits(count - 3, 3); + } else { + blTree.WriteSymbol(REP_11_138); + dh.pending.WriteBits(count - 11, 7); + } + } + } + + void BuildLength(int[] childs) + { + this.length = new byte [freqs.Length]; + int numNodes = childs.Length / 2; + int numLeafs = (numNodes + 1) / 2; + int overflow = 0; + + for (int i = 0; i < maxLength; i++) { + bl_counts[i] = 0; + } + + // First calculate optimal bit lengths + int[] lengths = new int[numNodes]; + lengths[numNodes-1] = 0; + + for (int i = numNodes - 1; i >= 0; i--) { + if (childs[2 * i + 1] != -1) { + int bitLength = lengths[i] + 1; + if (bitLength > maxLength) { + bitLength = maxLength; + overflow++; + } + lengths[childs[2 * i]] = lengths[childs[2 * i + 1]] = bitLength; + } else { + // A leaf node + int bitLength = lengths[i]; + bl_counts[bitLength - 1]++; + this.length[childs[2*i]] = (byte) lengths[i]; + } + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Tree "+freqs.Length+" lengths:"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + + if (overflow == 0) { + return; + } + + int incrBitLen = maxLength - 1; + do { + // Find the first bit length which could increase: + while (bl_counts[--incrBitLen] == 0) + ; + + // Move this node one down and remove a corresponding + // number of overflow nodes. + do { + bl_counts[incrBitLen]--; + bl_counts[++incrBitLen]++; + overflow -= 1 << (maxLength - 1 - incrBitLen); + } while (overflow > 0 && incrBitLen < maxLength - 1); + } while (overflow > 0); + + /* We may have overshot above. Move some nodes from maxLength to + * maxLength-1 in that case. + */ + bl_counts[maxLength-1] += overflow; + bl_counts[maxLength-2] -= overflow; + + /* Now recompute all bit lengths, scanning in increasing + * frequency. It is simpler to reconstruct all lengths instead of + * fixing only the wrong ones. This idea is taken from 'ar' + * written by Haruhiko Okumura. + * + * The nodes were inserted with decreasing frequency into the childs + * array. + */ + int nodePtr = 2 * numLeafs; + for (int bits = maxLength; bits != 0; bits--) { + int n = bl_counts[bits-1]; + while (n > 0) { + int childPtr = 2*childs[nodePtr++]; + if (childs[childPtr + 1] == -1) { + // We found another leaf + length[childs[childPtr]] = (byte) bits; + n--; + } + } + } + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("*** After overflow elimination. ***"); + // for (int i=0; i < numLeafs; i++) { + // //Console.WriteLine("Node "+childs[2*i]+" freq: "+freqs[childs[2*i]] + // + " len: "+length[childs[2*i]]); + // } + // } + } + + } + + #region Instance Fields + /// + /// Pending buffer to use + /// + public DeflaterPending pending; + + Tree literalTree; + Tree distTree; + Tree blTree; + + // Buffer for distances + short[] d_buf; + byte[] l_buf; + int last_lit; + int extra_bits; + #endregion + + static DeflaterHuffman() + { + // See RFC 1951 3.2.6 + // Literal codes + staticLCodes = new short[LITERAL_NUM]; + staticLLength = new byte[LITERAL_NUM]; + + int i = 0; + while (i < 144) { + staticLCodes[i] = BitReverse((0x030 + i) << 8); + staticLLength[i++] = 8; + } + + while (i < 256) { + staticLCodes[i] = BitReverse((0x190 - 144 + i) << 7); + staticLLength[i++] = 9; + } + + while (i < 280) { + staticLCodes[i] = BitReverse((0x000 - 256 + i) << 9); + staticLLength[i++] = 7; + } + + while (i < LITERAL_NUM) { + staticLCodes[i] = BitReverse((0x0c0 - 280 + i) << 8); + staticLLength[i++] = 8; + } + + // Distance codes + staticDCodes = new short[DIST_NUM]; + staticDLength = new byte[DIST_NUM]; + for (i = 0; i < DIST_NUM; i++) { + staticDCodes[i] = BitReverse(i << 11); + staticDLength[i] = 5; + } + } + + /// + /// Construct instance with pending buffer + /// + /// Pending buffer to use + public DeflaterHuffman(DeflaterPending pending) + { + this.pending = pending; + + literalTree = new Tree(this, LITERAL_NUM, 257, 15); + distTree = new Tree(this, DIST_NUM, 1, 15); + blTree = new Tree(this, BITLEN_NUM, 4, 7); + + d_buf = new short[BUFSIZE]; + l_buf = new byte [BUFSIZE]; + } + + /// + /// Reset internal state + /// + public void Reset() + { + last_lit = 0; + extra_bits = 0; + literalTree.Reset(); + distTree.Reset(); + blTree.Reset(); + } + + /// + /// Write all trees to pending buffer + /// + /// The number/rank of treecodes to send. + public void SendAllTrees(int blTreeCodes) + { + blTree.BuildCodes(); + literalTree.BuildCodes(); + distTree.BuildCodes(); + pending.WriteBits(literalTree.numCodes - 257, 5); + pending.WriteBits(distTree.numCodes - 1, 5); + pending.WriteBits(blTreeCodes - 4, 4); + for (int rank = 0; rank < blTreeCodes; rank++) { + pending.WriteBits(blTree.length[BL_ORDER[rank]], 3); + } + literalTree.WriteTree(blTree); + distTree.WriteTree(blTree); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + blTree.CheckEmpty(); + } +#endif + } + + /// + /// Compress current buffer writing data to pending buffer + /// + public void CompressBlock() + { + for (int i = 0; i < last_lit; i++) { + int litlen = l_buf[i] & 0xff; + int dist = d_buf[i]; + if (dist-- != 0) { + // if (DeflaterConstants.DEBUGGING) { + // Console.Write("["+(dist+1)+","+(litlen+3)+"]: "); + // } + + int lc = Lcode(litlen); + literalTree.WriteSymbol(lc); + + int bits = (lc - 261) / 4; + if (bits > 0 && bits <= 5) { + pending.WriteBits(litlen & ((1 << bits) - 1), bits); + } + + int dc = Dcode(dist); + distTree.WriteSymbol(dc); + + bits = dc / 2 - 1; + if (bits > 0) { + pending.WriteBits(dist & ((1 << bits) - 1), bits); + } + } else { + // if (DeflaterConstants.DEBUGGING) { + // if (litlen > 32 && litlen < 127) { + // Console.Write("("+(char)litlen+"): "); + // } else { + // Console.Write("{"+litlen+"}: "); + // } + // } + literalTree.WriteSymbol(litlen); + } + } + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + Console.Write("EOF: "); + } +#endif + literalTree.WriteSymbol(EOF_SYMBOL); + +#if DebugDeflation + if (DeflaterConstants.DEBUGGING) { + literalTree.CheckEmpty(); + distTree.CheckEmpty(); + } +#endif + } + + /// + /// Flush block to output with no compression + /// + /// Data to write + /// Index of first byte to write + /// Count of bytes to write + /// True if this is the last block + public void FlushStoredBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { +#if DebugDeflation + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Flushing stored block "+ storedLength); + // } +#endif + pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); + pending.AlignToByte(); + pending.WriteShort(storedLength); + pending.WriteShort(~storedLength); + pending.WriteBlock(stored, storedOffset, storedLength); + Reset(); + } + + /// + /// Flush block to output with compression + /// + /// Data to flush + /// Index of first byte to flush + /// Count of bytes to flush + /// True if this is the last block + public void FlushBlock(byte[] stored, int storedOffset, int storedLength, bool lastBlock) + { + literalTree.freqs[EOF_SYMBOL]++; + + // Build trees + literalTree.BuildTree(); + distTree.BuildTree(); + + // Calculate bitlen frequency + literalTree.CalcBLFreq(blTree); + distTree.CalcBLFreq(blTree); + + // Build bitlen tree + blTree.BuildTree(); + + int blTreeCodes = 4; + for (int i = 18; i > blTreeCodes; i--) { + if (blTree.length[BL_ORDER[i]] > 0) { + blTreeCodes = i+1; + } + } + int opt_len = 14 + blTreeCodes * 3 + blTree.GetEncodedLength() + + literalTree.GetEncodedLength() + distTree.GetEncodedLength() + + extra_bits; + + int static_len = extra_bits; + for (int i = 0; i < LITERAL_NUM; i++) { + static_len += literalTree.freqs[i] * staticLLength[i]; + } + for (int i = 0; i < DIST_NUM; i++) { + static_len += distTree.freqs[i] * staticDLength[i]; + } + if (opt_len >= static_len) { + // Force static trees + opt_len = static_len; + } + + if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) { + // Store Block + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("Storing, since " + storedLength + " < " + opt_len + // + " <= " + static_len); + // } + FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); + } else if (opt_len == static_len) { + // Encode with static tree + pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); + literalTree.SetStaticCodes(staticLCodes, staticLLength); + distTree.SetStaticCodes(staticDCodes, staticDLength); + CompressBlock(); + Reset(); + } else { + // Encode with dynamic tree + pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); + SendAllTrees(blTreeCodes); + CompressBlock(); + Reset(); + } + } + + /// + /// Get value indicating if internal buffer is full + /// + /// true if buffer is full + public bool IsFull() + { + return last_lit >= BUFSIZE; + } + + /// + /// Add literal to buffer + /// + /// Literal value to add to buffer. + /// Value indicating internal buffer is full + public bool TallyLit(int literal) + { + // if (DeflaterConstants.DEBUGGING) { + // if (lit > 32 && lit < 127) { + // //Console.WriteLine("("+(char)lit+")"); + // } else { + // //Console.WriteLine("{"+lit+"}"); + // } + // } + d_buf[last_lit] = 0; + l_buf[last_lit++] = (byte)literal; + literalTree.freqs[literal]++; + return IsFull(); + } + + /// + /// Add distance code and length to literal and distance trees + /// + /// Distance code + /// Length + /// Value indicating if internal buffer is full + public bool TallyDist(int distance, int length) + { + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("[" + distance + "," + length + "]"); + // } + + d_buf[last_lit] = (short)distance; + l_buf[last_lit++] = (byte)(length - 3); + + int lc = Lcode(length - 3); + literalTree.freqs[lc]++; + if (lc >= 265 && lc < 285) { + extra_bits += (lc - 261) / 4; + } + + int dc = Dcode(distance - 1); + distTree.freqs[dc]++; + if (dc >= 4) { + extra_bits += dc / 2 - 1; + } + return IsFull(); + } + + + /// + /// Reverse the bits of a 16 bit value. + /// + /// Value to reverse bits + /// Value with bits reversed + public static short BitReverse(int toReverse) + { + return (short) (bit4Reverse[toReverse & 0xF] << 12 | + bit4Reverse[(toReverse >> 4) & 0xF] << 8 | + bit4Reverse[(toReverse >> 8) & 0xF] << 4 | + bit4Reverse[toReverse >> 12]); + } + + static int Lcode(int length) + { + if (length == 255) { + return 285; + } + + int code = 257; + while (length >= 8) { + code += 4; + length >>= 1; + } + return code + length; + } + + static int Dcode(int distance) + { + int code = 0; + while (distance >= 4) { + code += 2; + distance >>= 1; + } + return code + distance; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs new file mode 100644 index 000000000..dbdcac6b2 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/DeflaterPending.cs @@ -0,0 +1,57 @@ +// DeflaterPending.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This class stores the pending output of the Deflater. + /// + /// author of the original java version : Jochen Hoenicke + /// + public class DeflaterPending : PendingBuffer + { + /// + /// Construct instance with default buffer size + /// + public DeflaterPending() : base(DeflaterConstants.PENDING_BUF_SIZE) + { + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs new file mode 100644 index 000000000..80f98cbaa --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Inflater.cs @@ -0,0 +1,864 @@ +// Inflater.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + /// + /// Inflater is used to decompress data that has been compressed according + /// to the "deflate" standard described in rfc1951. + /// + /// By default Zlib (rfc1950) headers and footers are expected in the input. + /// You can use constructor public Inflater(bool noHeader) passing true + /// if there is no Zlib header information + /// + /// The usage is as following. First you have to set some input with + /// SetInput(), then Inflate() it. If inflate doesn't + /// inflate any bytes there may be three reasons: + ///
    + ///
  • IsNeedingInput() returns true because the input buffer is empty. + /// You have to provide more input with SetInput(). + /// NOTE: IsNeedingInput() also returns true when, the stream is finished. + ///
  • + ///
  • IsNeedingDictionary() returns true, you have to provide a preset + /// dictionary with SetDictionary().
  • + ///
  • IsFinished returns true, the inflater has finished.
  • + ///
+ /// Once the first output byte is produced, a dictionary will not be + /// needed at a later stage. + /// + /// author of the original java version : John Leuner, Jochen Hoenicke + ///
+ public class Inflater + { + #region Constants/Readonly + /// + /// Copy lengths for literal codes 257..285 + /// + static readonly int[] CPLENS = { + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 + }; + + /// + /// Extra bits for literal codes 257..285 + /// + static readonly int[] CPLEXT = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 + }; + + /// + /// Copy offsets for distance codes 0..29 + /// + static readonly int[] CPDIST = { + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577 + }; + + /// + /// Extra bits for distance codes + /// + static readonly int[] CPDEXT = { + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13 + }; + + /// + /// These are the possible states for an inflater + /// + const int DECODE_HEADER = 0; + const int DECODE_DICT = 1; + const int DECODE_BLOCKS = 2; + const int DECODE_STORED_LEN1 = 3; + const int DECODE_STORED_LEN2 = 4; + const int DECODE_STORED = 5; + const int DECODE_DYN_HEADER = 6; + const int DECODE_HUFFMAN = 7; + const int DECODE_HUFFMAN_LENBITS = 8; + const int DECODE_HUFFMAN_DIST = 9; + const int DECODE_HUFFMAN_DISTBITS = 10; + const int DECODE_CHKSUM = 11; + const int FINISHED = 12; + #endregion + + #region Instance Fields + /// + /// This variable contains the current state. + /// + int mode; + + /// + /// The adler checksum of the dictionary or of the decompressed + /// stream, as it is written in the header resp. footer of the + /// compressed stream. + /// Only valid if mode is DECODE_DICT or DECODE_CHKSUM. + /// + int readAdler; + + /// + /// The number of bits needed to complete the current state. This + /// is valid, if mode is DECODE_DICT, DECODE_CHKSUM, + /// DECODE_HUFFMAN_LENBITS or DECODE_HUFFMAN_DISTBITS. + /// + int neededBits; + int repLength; + int repDist; + int uncomprLen; + + /// + /// True, if the last block flag was set in the last block of the + /// inflated stream. This means that the stream ends after the + /// current block. + /// + bool isLastBlock; + + /// + /// The total number of inflated bytes. + /// + long totalOut; + + /// + /// The total number of bytes set with setInput(). This is not the + /// value returned by the TotalIn property, since this also includes the + /// unprocessed input. + /// + long totalIn; + + /// + /// This variable stores the noHeader flag that was given to the constructor. + /// True means, that the inflated stream doesn't contain a Zlib header or + /// footer. + /// + bool noHeader; + + StreamManipulator input; + OutputWindow outputWindow; + InflaterDynHeader dynHeader; + InflaterHuffmanTree litlenTree, distTree; + Adler32 adler; + #endregion + + #region Constructors + /// + /// Creates a new inflater or RFC1951 decompressor + /// RFC1950/Zlib headers and footers will be expected in the input data + /// + public Inflater() : this(false) + { + } + + /// + /// Creates a new inflater. + /// + /// + /// True if no RFC1950/Zlib header and footer fields are expected in the input data + /// + /// This is used for GZIPed/Zipped input. + /// + /// For compatibility with + /// Sun JDK you should provide one byte of input more than needed in + /// this case. + /// + public Inflater(bool noHeader) + { + this.noHeader = noHeader; + this.adler = new Adler32(); + input = new StreamManipulator(); + outputWindow = new OutputWindow(); + mode = noHeader ? DECODE_BLOCKS : DECODE_HEADER; + } + #endregion + + /// + /// Resets the inflater so that a new stream can be decompressed. All + /// pending input and output will be discarded. + /// + public void Reset() + { + mode = noHeader ? DECODE_BLOCKS : DECODE_HEADER; + totalIn = 0; + totalOut = 0; + input.Reset(); + outputWindow.Reset(); + dynHeader = null; + litlenTree = null; + distTree = null; + isLastBlock = false; + adler.Reset(); + } + + /// + /// Decodes a zlib/RFC1950 header. + /// + /// + /// False if more input is needed. + /// + /// + /// The header is invalid. + /// + private bool DecodeHeader() + { + int header = input.PeekBits(16); + if (header < 0) { + return false; + } + input.DropBits(16); + + // The header is written in "wrong" byte order + header = ((header << 8) | (header >> 8)) & 0xffff; + if (header % 31 != 0) { + throw new SharpZipBaseException("Header checksum illegal"); + } + + if ((header & 0x0f00) != (Deflater.DEFLATED << 8)) { + throw new SharpZipBaseException("Compression Method unknown"); + } + + /* Maximum size of the backwards window in bits. + * We currently ignore this, but we could use it to make the + * inflater window more space efficient. On the other hand the + * full window (15 bits) is needed most times, anyway. + int max_wbits = ((header & 0x7000) >> 12) + 8; + */ + + if ((header & 0x0020) == 0) { // Dictionary flag? + mode = DECODE_BLOCKS; + } else { + mode = DECODE_DICT; + neededBits = 32; + } + return true; + } + + /// + /// Decodes the dictionary checksum after the deflate header. + /// + /// + /// False if more input is needed. + /// + private bool DecodeDict() + { + while (neededBits > 0) { + int dictByte = input.PeekBits(8); + if (dictByte < 0) { + return false; + } + input.DropBits(8); + readAdler = (readAdler << 8) | dictByte; + neededBits -= 8; + } + return false; + } + + /// + /// Decodes the huffman encoded symbols in the input stream. + /// + /// + /// false if more input is needed, true if output window is + /// full or the current block ends. + /// + /// + /// if deflated stream is invalid. + /// + private bool DecodeHuffman() + { + int free = outputWindow.GetFreeSpace(); + while (free >= 258) + { + int symbol; + switch (mode) + { + case DECODE_HUFFMAN: + // This is the inner loop so it is optimized a bit + while (((symbol = litlenTree.GetSymbol(input)) & ~0xff) == 0) + { + outputWindow.Write(symbol); + if (--free < 258) + { + return true; + } + } + + if (symbol < 257) + { + if (symbol < 0) + { + return false; + } + else + { + // symbol == 256: end of block + distTree = null; + litlenTree = null; + mode = DECODE_BLOCKS; + return true; + } + } + + try + { + repLength = CPLENS[symbol - 257]; + neededBits = CPLEXT[symbol - 257]; + } + catch (Exception) + { + throw new SharpZipBaseException("Illegal rep length code"); + } + goto case DECODE_HUFFMAN_LENBITS; // fall through + + case DECODE_HUFFMAN_LENBITS: + if (neededBits > 0) + { + mode = DECODE_HUFFMAN_LENBITS; + int i = input.PeekBits(neededBits); + if (i < 0) + { + return false; + } + input.DropBits(neededBits); + repLength += i; + } + mode = DECODE_HUFFMAN_DIST; + goto case DECODE_HUFFMAN_DIST; // fall through + + case DECODE_HUFFMAN_DIST: + symbol = distTree.GetSymbol(input); + if (symbol < 0) + { + return false; + } + + try + { + repDist = CPDIST[symbol]; + neededBits = CPDEXT[symbol]; + } + catch (Exception) + { + throw new SharpZipBaseException("Illegal rep dist code"); + } + + goto case DECODE_HUFFMAN_DISTBITS; // fall through + + case DECODE_HUFFMAN_DISTBITS: + if (neededBits > 0) + { + mode = DECODE_HUFFMAN_DISTBITS; + int i = input.PeekBits(neededBits); + if (i < 0) + { + return false; + } + input.DropBits(neededBits); + repDist += i; + } + + outputWindow.Repeat(repLength, repDist); + free -= repLength; + mode = DECODE_HUFFMAN; + break; + + default: + throw new SharpZipBaseException("Inflater unknown mode"); + } + } + return true; + } + + /// + /// Decodes the adler checksum after the deflate stream. + /// + /// + /// false if more input is needed. + /// + /// + /// If checksum doesn't match. + /// + private bool DecodeChksum() + { + while (neededBits > 0) { + int chkByte = input.PeekBits(8); + if (chkByte < 0) { + return false; + } + input.DropBits(8); + readAdler = (readAdler << 8) | chkByte; + neededBits -= 8; + } + + if ((int) adler.Value != readAdler) { + throw new SharpZipBaseException("Adler chksum doesn't match: " + (int)adler.Value + " vs. " + readAdler); + } + + mode = FINISHED; + return false; + } + + /// + /// Decodes the deflated stream. + /// + /// + /// false if more input is needed, or if finished. + /// + /// + /// if deflated stream is invalid. + /// + private bool Decode() + { + switch (mode) { + case DECODE_HEADER: + return DecodeHeader(); + + case DECODE_DICT: + return DecodeDict(); + + case DECODE_CHKSUM: + return DecodeChksum(); + + case DECODE_BLOCKS: + if (isLastBlock) { + if (noHeader) { + mode = FINISHED; + return false; + } else { + input.SkipToByteBoundary(); + neededBits = 32; + mode = DECODE_CHKSUM; + return true; + } + } + + int type = input.PeekBits(3); + if (type < 0) { + return false; + } + input.DropBits(3); + + if ((type & 1) != 0) { + isLastBlock = true; + } + switch (type >> 1){ + case DeflaterConstants.STORED_BLOCK: + input.SkipToByteBoundary(); + mode = DECODE_STORED_LEN1; + break; + case DeflaterConstants.STATIC_TREES: + litlenTree = InflaterHuffmanTree.defLitLenTree; + distTree = InflaterHuffmanTree.defDistTree; + mode = DECODE_HUFFMAN; + break; + case DeflaterConstants.DYN_TREES: + dynHeader = new InflaterDynHeader(); + mode = DECODE_DYN_HEADER; + break; + default: + throw new SharpZipBaseException("Unknown block type " + type); + } + return true; + + case DECODE_STORED_LEN1: + { + if ((uncomprLen = input.PeekBits(16)) < 0) { + return false; + } + input.DropBits(16); + mode = DECODE_STORED_LEN2; + } + goto case DECODE_STORED_LEN2; // fall through + + case DECODE_STORED_LEN2: + { + int nlen = input.PeekBits(16); + if (nlen < 0) { + return false; + } + input.DropBits(16); + if (nlen != (uncomprLen ^ 0xffff)) { + throw new SharpZipBaseException("broken uncompressed block"); + } + mode = DECODE_STORED; + } + goto case DECODE_STORED; // fall through + + case DECODE_STORED: + { + int more = outputWindow.CopyStored(input, uncomprLen); + uncomprLen -= more; + if (uncomprLen == 0) { + mode = DECODE_BLOCKS; + return true; + } + return !input.IsNeedingInput; + } + + case DECODE_DYN_HEADER: + if (!dynHeader.Decode(input)) { + return false; + } + + litlenTree = dynHeader.BuildLitLenTree(); + distTree = dynHeader.BuildDistTree(); + mode = DECODE_HUFFMAN; + goto case DECODE_HUFFMAN; // fall through + + case DECODE_HUFFMAN: + case DECODE_HUFFMAN_LENBITS: + case DECODE_HUFFMAN_DIST: + case DECODE_HUFFMAN_DISTBITS: + return DecodeHuffman(); + + case FINISHED: + return false; + + default: + throw new SharpZipBaseException("Inflater.Decode unknown mode"); + } + } + + /// + /// Sets the preset dictionary. This should only be called, if + /// needsDictionary() returns true and it should set the same + /// dictionary, that was used for deflating. The getAdler() + /// function returns the checksum of the dictionary needed. + /// + /// + /// The dictionary. + /// + public void SetDictionary(byte[] buffer) + { + SetDictionary(buffer, 0, buffer.Length); + } + + /// + /// Sets the preset dictionary. This should only be called, if + /// needsDictionary() returns true and it should set the same + /// dictionary, that was used for deflating. The getAdler() + /// function returns the checksum of the dictionary needed. + /// + /// + /// The dictionary. + /// + /// + /// The index into buffer where the dictionary starts. + /// + /// + /// The number of bytes in the dictionary. + /// + /// + /// No dictionary is needed. + /// + /// + /// The adler checksum for the buffer is invalid + /// + public void SetDictionary(byte[] buffer, int index, int count) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( index < 0 ) { + throw new ArgumentOutOfRangeException("index"); + } + + if ( count < 0 ) { + throw new ArgumentOutOfRangeException("count"); + } + + if (!IsNeedingDictionary) { + throw new InvalidOperationException("Dictionary is not needed"); + } + + adler.Update(buffer, index, count); + + if ((int)adler.Value != readAdler) { + throw new SharpZipBaseException("Wrong adler checksum"); + } + adler.Reset(); + outputWindow.CopyDict(buffer, index, count); + mode = DECODE_BLOCKS; + } + + /// + /// Sets the input. This should only be called, if needsInput() + /// returns true. + /// + /// + /// the input. + /// + public void SetInput(byte[] buffer) + { + SetInput(buffer, 0, buffer.Length); + } + + /// + /// Sets the input. This should only be called, if needsInput() + /// returns true. + /// + /// + /// The source of input data + /// + /// + /// The index into buffer where the input starts. + /// + /// + /// The number of bytes of input to use. + /// + /// + /// No input is needed. + /// + /// + /// The index and/or count are wrong. + /// + public void SetInput(byte[] buffer, int index, int count) + { + input.SetInput(buffer, index, count); + totalIn += (long)count; + } + + /// + /// Inflates the compressed stream to the output buffer. If this + /// returns 0, you should check, whether IsNeedingDictionary(), + /// IsNeedingInput() or IsFinished() returns true, to determine why no + /// further output is produced. + /// + /// + /// the output buffer. + /// + /// + /// The number of bytes written to the buffer, 0 if no further + /// output can be produced. + /// + /// + /// if buffer has length 0. + /// + /// + /// if deflated stream is invalid. + /// + public int Inflate(byte[] buffer) + { + if ( buffer == null ) + { + throw new ArgumentNullException("buffer"); + } + + return Inflate(buffer, 0, buffer.Length); + } + + /// + /// Inflates the compressed stream to the output buffer. If this + /// returns 0, you should check, whether needsDictionary(), + /// needsInput() or finished() returns true, to determine why no + /// further output is produced. + /// + /// + /// the output buffer. + /// + /// + /// the offset in buffer where storing starts. + /// + /// + /// the maximum number of bytes to output. + /// + /// + /// the number of bytes written to the buffer, 0 if no further output can be produced. + /// + /// + /// if count is less than 0. + /// + /// + /// if the index and / or count are wrong. + /// + /// + /// if deflated stream is invalid. + /// + public int Inflate(byte[] buffer, int offset, int count) + { + if ( buffer == null ) + { + throw new ArgumentNullException("buffer"); + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "count cannot be negative"); +#endif + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "offset cannot be negative"); +#endif + } + + if ( offset + count > buffer.Length ) { + throw new ArgumentException("count exceeds buffer bounds"); + } + + // Special case: count may be zero + if (count == 0) + { + if (!IsFinished) { // -jr- 08-Nov-2003 INFLATE_BUG fix.. + Decode(); + } + return 0; + } + + int bytesCopied = 0; + + do { + if (mode != DECODE_CHKSUM) { + /* Don't give away any output, if we are waiting for the + * checksum in the input stream. + * + * With this trick we have always: + * IsNeedingInput() and not IsFinished() + * implies more output can be produced. + */ + int more = outputWindow.CopyOutput(buffer, offset, count); + if ( more > 0 ) { + adler.Update(buffer, offset, more); + offset += more; + bytesCopied += more; + totalOut += (long)more; + count -= more; + if (count == 0) { + return bytesCopied; + } + } + } + } while (Decode() || ((outputWindow.GetAvailable() > 0) && (mode != DECODE_CHKSUM))); + return bytesCopied; + } + + /// + /// Returns true, if the input buffer is empty. + /// You should then call setInput(). + /// NOTE: This method also returns true when the stream is finished. + /// + public bool IsNeedingInput { + get { + return input.IsNeedingInput; + } + } + + /// + /// Returns true, if a preset dictionary is needed to inflate the input. + /// + public bool IsNeedingDictionary { + get { + return mode == DECODE_DICT && neededBits == 0; + } + } + + /// + /// Returns true, if the inflater has finished. This means, that no + /// input is needed and no output can be produced. + /// + public bool IsFinished { + get { + return mode == FINISHED && outputWindow.GetAvailable() == 0; + } + } + + /// + /// Gets the adler checksum. This is either the checksum of all + /// uncompressed bytes returned by inflate(), or if needsDictionary() + /// returns true (and thus no output was yet produced) this is the + /// adler checksum of the expected dictionary. + /// + /// + /// the adler checksum. + /// + public int Adler { + get { + return IsNeedingDictionary ? readAdler : (int) adler.Value; + } + } + + /// + /// Gets the total number of output bytes returned by Inflate(). + /// + /// + /// the total number of output bytes. + /// + public long TotalOut { + get { + return totalOut; + } + } + + /// + /// Gets the total number of processed compressed input bytes. + /// + /// + /// The total number of bytes of processed input bytes. + /// + public long TotalIn { + get { + return totalIn - (long)RemainingInput; + } + } + + /// + /// Gets the number of unprocessed input bytes. Useful, if the end of the + /// stream is reached and you want to further process the bytes after + /// the deflate stream. + /// + /// + /// The number of bytes of the input which have not been processed. + /// + public int RemainingInput { + // TODO: This should be a long? + get { + return input.AvailableBytes; + } + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs new file mode 100644 index 000000000..cb019b8b5 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterDynHeader.cs @@ -0,0 +1,218 @@ +// InflaterDynHeader.cs +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + class InflaterDynHeader + { + #region Constants + const int LNUM = 0; + const int DNUM = 1; + const int BLNUM = 2; + const int BLLENS = 3; + const int LENS = 4; + const int REPS = 5; + + static readonly int[] repMin = { 3, 3, 11 }; + static readonly int[] repBits = { 2, 3, 7 }; + + static readonly int[] BL_ORDER = + { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + #endregion + + #region Constructors + public InflaterDynHeader() + { + } + #endregion + + public bool Decode(StreamManipulator input) + { + decode_loop: + for (;;) { + switch (mode) { + case LNUM: + lnum = input.PeekBits(5); + if (lnum < 0) { + return false; + } + lnum += 257; + input.DropBits(5); + // System.err.println("LNUM: "+lnum); + mode = DNUM; + goto case DNUM; // fall through + case DNUM: + dnum = input.PeekBits(5); + if (dnum < 0) { + return false; + } + dnum++; + input.DropBits(5); + // System.err.println("DNUM: "+dnum); + num = lnum+dnum; + litdistLens = new byte[num]; + mode = BLNUM; + goto case BLNUM; // fall through + case BLNUM: + blnum = input.PeekBits(4); + if (blnum < 0) { + return false; + } + blnum += 4; + input.DropBits(4); + blLens = new byte[19]; + ptr = 0; + // System.err.println("BLNUM: "+blnum); + mode = BLLENS; + goto case BLLENS; // fall through + case BLLENS: + while (ptr < blnum) { + int len = input.PeekBits(3); + if (len < 0) { + return false; + } + input.DropBits(3); + // System.err.println("blLens["+BL_ORDER[ptr]+"]: "+len); + blLens[BL_ORDER[ptr]] = (byte) len; + ptr++; + } + blTree = new InflaterHuffmanTree(blLens); + blLens = null; + ptr = 0; + mode = LENS; + goto case LENS; // fall through + case LENS: + { + int symbol; + while (((symbol = blTree.GetSymbol(input)) & ~15) == 0) { + /* Normal case: symbol in [0..15] */ + + // System.err.println("litdistLens["+ptr+"]: "+symbol); + litdistLens[ptr++] = lastLen = (byte)symbol; + + if (ptr == num) { + /* Finished */ + return true; + } + } + + /* need more input ? */ + if (symbol < 0) { + return false; + } + + /* otherwise repeat code */ + if (symbol >= 17) { + /* repeat zero */ + // System.err.println("repeating zero"); + lastLen = 0; + } else { + if (ptr == 0) { + throw new SharpZipBaseException(); + } + } + repSymbol = symbol-16; + } + mode = REPS; + goto case REPS; // fall through + case REPS: + { + int bits = repBits[repSymbol]; + int count = input.PeekBits(bits); + if (count < 0) { + return false; + } + input.DropBits(bits); + count += repMin[repSymbol]; + // System.err.println("litdistLens repeated: "+count); + + if (ptr + count > num) { + throw new SharpZipBaseException(); + } + while (count-- > 0) { + litdistLens[ptr++] = lastLen; + } + + if (ptr == num) { + /* Finished */ + return true; + } + } + mode = LENS; + goto decode_loop; + } + } + } + + public InflaterHuffmanTree BuildLitLenTree() + { + byte[] litlenLens = new byte[lnum]; + Array.Copy(litdistLens, 0, litlenLens, 0, lnum); + return new InflaterHuffmanTree(litlenLens); + } + + public InflaterHuffmanTree BuildDistTree() + { + byte[] distLens = new byte[dnum]; + Array.Copy(litdistLens, lnum, distLens, 0, dnum); + return new InflaterHuffmanTree(distLens); + } + + #region Instance Fields + byte[] blLens; + byte[] litdistLens; + + InflaterHuffmanTree blTree; + + /// + /// The current decode mode + /// + int mode; + int lnum, dnum, blnum, num; + int repSymbol; + byte lastLen; + int ptr; + #endregion + + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs new file mode 100644 index 000000000..e1467fa82 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/InflaterHuffmanTree.cs @@ -0,0 +1,232 @@ +// InflaterHuffmanTree.cs +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// Huffman tree used for inflation + /// + public class InflaterHuffmanTree + { + #region Constants + const int MAX_BITLEN = 15; + #endregion + + #region Instance Fields + short[] tree; + #endregion + + /// + /// Literal length tree + /// + public static InflaterHuffmanTree defLitLenTree; + + /// + /// Distance tree + /// + public static InflaterHuffmanTree defDistTree; + + static InflaterHuffmanTree() + { + try { + byte[] codeLengths = new byte[288]; + int i = 0; + while (i < 144) { + codeLengths[i++] = 8; + } + while (i < 256) { + codeLengths[i++] = 9; + } + while (i < 280) { + codeLengths[i++] = 7; + } + while (i < 288) { + codeLengths[i++] = 8; + } + defLitLenTree = new InflaterHuffmanTree(codeLengths); + + codeLengths = new byte[32]; + i = 0; + while (i < 32) { + codeLengths[i++] = 5; + } + defDistTree = new InflaterHuffmanTree(codeLengths); + } catch (Exception) { + throw new SharpZipBaseException("InflaterHuffmanTree: static tree length illegal"); + } + } + + #region Constructors + /// + /// Constructs a Huffman tree from the array of code lengths. + /// + /// + /// the array of code lengths + /// + public InflaterHuffmanTree(byte[] codeLengths) + { + BuildTree(codeLengths); + } + #endregion + + void BuildTree(byte[] codeLengths) + { + int[] blCount = new int[MAX_BITLEN + 1]; + int[] nextCode = new int[MAX_BITLEN + 1]; + + for (int i = 0; i < codeLengths.Length; i++) { + int bits = codeLengths[i]; + if (bits > 0) { + blCount[bits]++; + } + } + + int code = 0; + int treeSize = 512; + for (int bits = 1; bits <= MAX_BITLEN; bits++) { + nextCode[bits] = code; + code += blCount[bits] << (16 - bits); + if (bits >= 10) { + /* We need an extra table for bit lengths >= 10. */ + int start = nextCode[bits] & 0x1ff80; + int end = code & 0x1ff80; + treeSize += (end - start) >> (16 - bits); + } + } + +/* -jr comment this out! doesnt work for dynamic trees and pkzip 2.04g + if (code != 65536) + { + throw new SharpZipBaseException("Code lengths don't add up properly."); + } +*/ + /* Now create and fill the extra tables from longest to shortest + * bit len. This way the sub trees will be aligned. + */ + tree = new short[treeSize]; + int treePtr = 512; + for (int bits = MAX_BITLEN; bits >= 10; bits--) { + int end = code & 0x1ff80; + code -= blCount[bits] << (16 - bits); + int start = code & 0x1ff80; + for (int i = start; i < end; i += 1 << 7) { + tree[DeflaterHuffman.BitReverse(i)] = (short) ((-treePtr << 4) | bits); + treePtr += 1 << (bits-9); + } + } + + for (int i = 0; i < codeLengths.Length; i++) { + int bits = codeLengths[i]; + if (bits == 0) { + continue; + } + code = nextCode[bits]; + int revcode = DeflaterHuffman.BitReverse(code); + if (bits <= 9) { + do { + tree[revcode] = (short) ((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < 512); + } else { + int subTree = tree[revcode & 511]; + int treeLen = 1 << (subTree & 15); + subTree = -(subTree >> 4); + do { + tree[subTree | (revcode >> 9)] = (short) ((i << 4) | bits); + revcode += 1 << bits; + } while (revcode < treeLen); + } + nextCode[bits] = code + (1 << (16 - bits)); + } + + } + + /// + /// Reads the next symbol from input. The symbol is encoded using the + /// huffman tree. + /// + /// + /// input the input source. + /// + /// + /// the next symbol, or -1 if not enough input is available. + /// + public int GetSymbol(StreamManipulator input) + { + int lookahead, symbol; + if ((lookahead = input.PeekBits(9)) >= 0) { + if ((symbol = tree[lookahead]) >= 0) { + input.DropBits(symbol & 15); + return symbol >> 4; + } + int subtree = -(symbol >> 4); + int bitlen = symbol & 15; + if ((lookahead = input.PeekBits(bitlen)) >= 0) { + symbol = tree[subtree | (lookahead >> 9)]; + input.DropBits(symbol & 15); + return symbol >> 4; + } else { + int bits = input.AvailableBits; + lookahead = input.PeekBits(bits); + symbol = tree[subtree | (lookahead >> 9)]; + if ((symbol & 15) <= bits) { + input.DropBits(symbol & 15); + return symbol >> 4; + } else { + return -1; + } + } + } else { + int bits = input.AvailableBits; + lookahead = input.PeekBits(bits); + symbol = tree[lookahead]; + if (symbol >= 0 && (symbol & 15) <= bits) { + input.DropBits(symbol & 15); + return symbol >> 4; + } else { + return -1; + } + } + } + } +} + diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs new file mode 100644 index 000000000..1ea1fb4b2 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/PendingBuffer.cs @@ -0,0 +1,295 @@ +// PendingBuffer.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression +{ + + /// + /// This class is general purpose class for writing data to a buffer. + /// + /// It allows you to write bits as well as bytes + /// Based on DeflaterPending.java + /// + /// author of the original java version : Jochen Hoenicke + /// + public class PendingBuffer + { + #region Instance Fields + /// + /// Internal work buffer + /// + byte[] buffer_; + + int start; + int end; + + uint bits; + int bitCount; + #endregion + + #region Constructors + /// + /// construct instance using default buffer size of 4096 + /// + public PendingBuffer() : this( 4096 ) + { + } + + /// + /// construct instance using specified buffer size + /// + /// + /// size to use for internal buffer + /// + public PendingBuffer(int bufferSize) + { + buffer_ = new byte[bufferSize]; + } + + #endregion + + /// + /// Clear internal state/buffers + /// + public void Reset() + { + start = end = bitCount = 0; + } + + /// + /// Write a byte to buffer + /// + /// + /// The value to write + /// + public void WriteByte(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) value); + } + + /// + /// Write a short value to buffer LSB first + /// + /// + /// The value to write. + /// + public void WriteShort(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) value); + buffer_[end++] = unchecked((byte) (value >> 8)); + } + + /// + /// write an integer LSB first + /// + /// The value to write. + public void WriteInt(int value) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) value); + buffer_[end++] = unchecked((byte) (value >> 8)); + buffer_[end++] = unchecked((byte) (value >> 16)); + buffer_[end++] = unchecked((byte) (value >> 24)); + } + + /// + /// Write a block of data to buffer + /// + /// data to write + /// offset of first byte to write + /// number of bytes to write + public void WriteBlock(byte[] block, int offset, int length) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + System.Array.Copy(block, offset, buffer_, end, length); + end += length; + } + + /// + /// The number of bits written to the buffer + /// + public int BitCount { + get { + return bitCount; + } + } + + /// + /// Align internal buffer on a byte boundary + /// + public void AlignToByte() + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + if (bitCount > 0) + { + buffer_[end++] = unchecked((byte) bits); + if (bitCount > 8) { + buffer_[end++] = unchecked((byte) (bits >> 8)); + } + } + bits = 0; + bitCount = 0; + } + + /// + /// Write bits to internal buffer + /// + /// source of bits + /// number of bits to write + public void WriteBits(int b, int count) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } + + // if (DeflaterConstants.DEBUGGING) { + // //Console.WriteLine("writeBits("+b+","+count+")"); + // } +#endif + bits |= (uint)(b << bitCount); + bitCount += count; + if (bitCount >= 16) { + buffer_[end++] = unchecked((byte) bits); + buffer_[end++] = unchecked((byte) (bits >> 8)); + bits >>= 16; + bitCount -= 16; + } + } + + /// + /// Write a short value to internal buffer most significant byte first + /// + /// value to write + public void WriteShortMSB(int s) + { +#if DebugDeflation + if (DeflaterConstants.DEBUGGING && (start != 0) ) + { + throw new SharpZipBaseException("Debug check: start != 0"); + } +#endif + buffer_[end++] = unchecked((byte) (s >> 8)); + buffer_[end++] = unchecked((byte) s); + } + + /// + /// Indicates if buffer has been flushed + /// + public bool IsFlushed { + get { + return end == 0; + } + } + + /// + /// Flushes the pending buffer into the given output array. If the + /// output array is to small, only a partial flush is done. + /// + /// The output array. + /// The offset into output array. + /// The maximum number of bytes to store. + /// The number of bytes flushed. + public int Flush(byte[] output, int offset, int length) + { + if (bitCount >= 8) { + buffer_[end++] = unchecked((byte) bits); + bits >>= 8; + bitCount -= 8; + } + + if (length > end - start) { + length = end - start; + System.Array.Copy(buffer_, start, output, offset, length); + start = 0; + end = 0; + } else { + System.Array.Copy(buffer_, start, output, offset, length); + start += length; + } + return length; + } + + /// + /// Convert internal buffer to byte array. + /// Buffer is empty on completion + /// + /// + /// The internal buffer contents converted to a byte array. + /// + public byte[] ToByteArray() + { + byte[] result = new byte[end - start]; + System.Array.Copy(buffer_, start, result, 0, result.Length); + start = 0; + end = 0; + return result; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs new file mode 100644 index 000000000..9adb557c0 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/DeflaterOutputStream.cs @@ -0,0 +1,602 @@ +// DeflaterOutputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 DavidPierson Added AES support + +using System; +using System.IO; + +#if !NETCF_1_0 +using System.Security.Cryptography; +using GitHub.ICSharpCode.SharpZipLib.Encryption; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + /// + /// A special stream deflating or compressing the bytes that are + /// written to it. It uses a Deflater to perform actual deflating.
+ /// Authors of the original java version : Tom Tromey, Jochen Hoenicke + ///
+ public class DeflaterOutputStream : Stream + { + #region Constructors + /// + /// Creates a new DeflaterOutputStream with a default Deflater and default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + public DeflaterOutputStream(Stream baseOutputStream) + : this(baseOutputStream, new Deflater(), 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// default buffer size. + /// + /// + /// the output stream where deflated output should be written. + /// + /// + /// the underlying deflater. + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater) + : this(baseOutputStream, deflater, 512) + { + } + + /// + /// Creates a new DeflaterOutputStream with the given Deflater and + /// buffer size. + /// + /// + /// The output stream where deflated output is written. + /// + /// + /// The underlying deflater to use + /// + /// + /// The buffer size in bytes to use when deflating (minimum value 512) + /// + /// + /// bufsize is less than or equal to zero. + /// + /// + /// baseOutputStream does not support writing + /// + /// + /// deflater instance is null + /// + public DeflaterOutputStream(Stream baseOutputStream, Deflater deflater, int bufferSize) + { + if ( baseOutputStream == null ) { + throw new ArgumentNullException("baseOutputStream"); + } + + if (baseOutputStream.CanWrite == false) { + throw new ArgumentException("Must support writing", "baseOutputStream"); + } + + if (deflater == null) { + throw new ArgumentNullException("deflater"); + } + + if (bufferSize < 512) { + throw new ArgumentOutOfRangeException("bufferSize"); + } + + baseOutputStream_ = baseOutputStream; + buffer_ = new byte[bufferSize]; + deflater_ = deflater; + } + #endregion + + #region Public API + /// + /// Finishes the stream by calling finish() on the deflater. + /// + /// + /// Not all input is deflated + /// + public virtual void Finish() + { + deflater_.Finish(); + while (!deflater_.IsFinished) { + int len = deflater_.Deflate(buffer_, 0, buffer_.Length); + if (len <= 0) { + break; + } + +#if NETCF_1_0 + if ( keys != null ) { +#else + if (cryptoTransform_ != null) { +#endif + EncryptBlock(buffer_, 0, len); + } + + baseOutputStream_.Write(buffer_, 0, len); + } + + if (!deflater_.IsFinished) { + throw new SharpZipBaseException("Can't deflate all input?"); + } + + baseOutputStream_.Flush(); + +#if NETCF_1_0 + if ( keys != null ) { + keys = null; + } +#else + if (cryptoTransform_ != null) { +#if !NET_1_1 && !NETCF_2_0 + if (cryptoTransform_ is ZipAESTransform) { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } +#endif + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } +#endif + } + + /// + /// Get/set flag indicating ownership of the underlying stream. + /// When the flag is true will close the underlying stream also. + /// + public bool IsStreamOwner + { + get { return isStreamOwner_; } + set { isStreamOwner_ = value; } + } + + /// + /// Allows client to determine if an entry can be patched after its added + /// + public bool CanPatchEntries { + get { + return baseOutputStream_.CanSeek; + } + } + + #endregion + + #region Encryption + + string password; + +#if NETCF_1_0 + uint[] keys; +#else + ICryptoTransform cryptoTransform_; + + /// + /// Returns the 10 byte AUTH CODE to be appended immediately following the AES data stream. + /// + protected byte[] AESAuthCode; +#endif + + /// + /// Get/set the password used for encryption. + /// + /// When set to null or if the password is empty no encryption is performed + public string Password { + get { + return password; + } + set { + if ( (value != null) && (value.Length == 0) ) { + password = null; + } else { + password = value; + } + } + } + + /// + /// Encrypt a block of data + /// + /// + /// Data to encrypt. NOTE the original contents of the buffer are lost + /// + /// + /// Offset of first byte in buffer to encrypt + /// + /// + /// Number of bytes in buffer to encrypt + /// + protected void EncryptBlock(byte[] buffer, int offset, int length) + { +#if NETCF_1_0 + for (int i = offset; i < offset + length; ++i) { + byte oldbyte = buffer[i]; + buffer[i] ^= EncryptByte(); + UpdateKeys(oldbyte); + } +#else + cryptoTransform_.TransformBlock(buffer, 0, length, buffer, 0); +#endif + } + + /// + /// Initializes encryption keys based on given . + /// + /// The password. + protected void InitializePassword(string password) + { +#if NETCF_1_0 + keys = new uint[] { + 0x12345678, + 0x23456789, + 0x34567890 + }; + + byte[] rawPassword = ZipConstants.ConvertToArray(password); + + for (int i = 0; i < rawPassword.Length; ++i) { + UpdateKeys((byte)rawPassword[i]); + } + +#else + PkzipClassicManaged pkManaged = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipConstants.ConvertToArray(password)); + cryptoTransform_ = pkManaged.CreateEncryptor(key, null); +#endif + } + +#if !NET_1_1 && !NETCF_2_0 + /// + /// Initializes encryption keys based on given password. + /// + protected void InitializeAESPassword(ZipEntry entry, string rawPassword, + out byte[] salt, out byte[] pwdVerifier) { + salt = new byte[entry.AESSaltLen]; + // Salt needs to be cryptographically random, and unique per file + if (_aesRnd == null) + _aesRnd = new RNGCryptoServiceProvider(); + _aesRnd.GetBytes(salt); + int blockSize = entry.AESKeySize / 8; // bits to bytes + + cryptoTransform_ = new ZipAESTransform(rawPassword, salt, blockSize, true); + pwdVerifier = ((ZipAESTransform)cryptoTransform_).PwdVerifier; + } +#endif + +#if NETCF_1_0 + + /// + /// Encrypt a single byte + /// + /// + /// The encrypted value + /// + protected byte EncryptByte() + { + uint temp = ((keys[2] & 0xFFFF) | 2); + return (byte)((temp * (temp ^ 1)) >> 8); + } + + /// + /// Update encryption keys + /// + protected void UpdateKeys(byte ch) + { + keys[0] = Crc32.ComputeCrc32(keys[0], ch); + keys[1] = keys[1] + (byte)keys[0]; + keys[1] = keys[1] * 134775813 + 1; + keys[2] = Crc32.ComputeCrc32(keys[2], (byte)(keys[1] >> 24)); + } +#endif + + #endregion + + #region Deflation Support + /// + /// Deflates everything in the input buffers. This will call + /// def.deflate() until all bytes from the input buffers + /// are processed. + /// + protected void Deflate() + { + while (!deflater_.IsNeedingInput) + { + int deflateCount = deflater_.Deflate(buffer_, 0, buffer_.Length); + + if (deflateCount <= 0) { + break; + } +#if NETCF_1_0 + if (keys != null) +#else + if (cryptoTransform_ != null) +#endif + { + EncryptBlock(buffer_, 0, deflateCount); + } + + baseOutputStream_.Write(buffer_, 0, deflateCount); + } + + if (!deflater_.IsNeedingInput) { + throw new SharpZipBaseException("DeflaterOutputStream can't deflate all input?"); + } + } + #endregion + + #region Stream Overrides + /// + /// Gets value indicating stream can be read from + /// + public override bool CanRead + { + get { + return false; + } + } + + /// + /// Gets a value indicating if seeking is supported for this stream + /// This property always returns false + /// + public override bool CanSeek { + get { + return false; + } + } + + /// + /// Get value indicating if this stream supports writing + /// + public override bool CanWrite { + get { + return baseOutputStream_.CanWrite; + } + } + + /// + /// Get current length of stream + /// + public override long Length { + get { + return baseOutputStream_.Length; + } + } + + /// + /// Gets the current position within the stream. + /// + /// Any attempt to set position + public override long Position { + get { + return baseOutputStream_.Position; + } + set { + throw new NotSupportedException("Position property not supported"); + } + } + + /// + /// Sets the current position of this stream to the given value. Not supported by this class! + /// + /// The offset relative to the to seek. + /// The to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("DeflaterOutputStream Seek not supported"); + } + + /// + /// Sets the length of this stream to the given value. Not supported by this class! + /// + /// The new stream length. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("DeflaterOutputStream SetLength not supported"); + } + + /// + /// Read a byte from stream advancing position by one + /// + /// The byte read cast to an int. THe value is -1 if at the end of the stream. + /// Any access + public override int ReadByte() + { + throw new NotSupportedException("DeflaterOutputStream ReadByte not supported"); + } + + /// + /// Read a block of bytes from stream + /// + /// The buffer to store read data in. + /// The offset to start storing at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. Zero if end of stream is detected. + /// Any access + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("DeflaterOutputStream Read not supported"); + } + + /// + /// Asynchronous reads are not supported a NotSupportedException is always thrown + /// + /// The buffer to read into. + /// The offset to start storing data at. + /// The number of bytes to read + /// The async callback to use. + /// The state to use. + /// Returns an + /// Any access + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException("DeflaterOutputStream BeginRead not currently supported"); + } + + /// + /// Asynchronous writes arent supported, a NotSupportedException is always thrown + /// + /// The buffer to write. + /// The offset to begin writing at. + /// The number of bytes to write. + /// The to use. + /// The state object. + /// Returns an IAsyncResult. + /// Any access + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException("BeginWrite is not supported"); + } + + /// + /// Flushes the stream by calling Flush on the deflater and then + /// on the underlying stream. This ensures that all bytes are flushed. + /// + public override void Flush() + { + deflater_.Flush(); + Deflate(); + baseOutputStream_.Flush(); + } + + /// + /// Calls and closes the underlying + /// stream when is true. + /// + public override void Close() + { + if ( !isClosed_ ) { + isClosed_ = true; + + try { + Finish(); +#if NETCF_1_0 + keys=null; +#else + if ( cryptoTransform_ != null ) { + GetAuthCodeIfAES(); + cryptoTransform_.Dispose(); + cryptoTransform_ = null; + } +#endif + } + finally { + if( isStreamOwner_ ) { + baseOutputStream_.Close(); + } + } + } + } + + private void GetAuthCodeIfAES() { +#if !NET_1_1 && !NETCF_2_0 + if (cryptoTransform_ is ZipAESTransform) { + AESAuthCode = ((ZipAESTransform)cryptoTransform_).GetAuthCode(); + } +#endif + } + + /// + /// Writes a single byte to the compressed output stream. + /// + /// + /// The byte value. + /// + public override void WriteByte(byte value) + { + byte[] b = new byte[1]; + b[0] = value; + Write(b, 0, 1); + } + + /// + /// Writes bytes from an array to the compressed stream. + /// + /// + /// The byte array + /// + /// + /// The offset into the byte array where to start. + /// + /// + /// The number of bytes to write. + /// + public override void Write(byte[] buffer, int offset, int count) + { + deflater_.SetInput(buffer, offset, count); + Deflate(); + } + #endregion + + #region Instance Fields + /// + /// This buffer is used temporarily to retrieve the bytes from the + /// deflater and write them to the underlying output stream. + /// + byte[] buffer_; + + /// + /// The deflater which is used to deflate the stream. + /// + protected Deflater deflater_; + + /// + /// Base stream the deflater depends on. + /// + protected Stream baseOutputStream_; + + bool isClosed_; + + bool isStreamOwner_ = true; + #endregion + + #region Static Fields + +#if !NET_1_1 && !NETCF_2_0 + // Static to help ensure that multiple files within a zip will get different random salt + private static RNGCryptoServiceProvider _aesRnd; +#endif + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs new file mode 100644 index 000000000..f1599041b --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs @@ -0,0 +1,732 @@ +// InflaterInputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 11-08-2009 GeoffHart T9121 Added Multi-member gzip support + +using System; +using System.IO; + +#if !NETCF_1_0 +using System.Security.Cryptography; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + + /// + /// An input buffer customised for use by + /// + /// + /// The buffer supports decryption of incoming data. + /// + public class InflaterInputBuffer + { + #region Constructors + /// + /// Initialise a new instance of with a default buffer size + /// + /// The stream to buffer. + public InflaterInputBuffer(Stream stream) : this(stream , 4096) + { + } + + /// + /// Initialise a new instance of + /// + /// The stream to buffer. + /// The size to use for the buffer + /// A minimum buffer size of 1KB is permitted. Lower sizes are treated as 1KB. + public InflaterInputBuffer(Stream stream, int bufferSize) + { + inputStream = stream; + if ( bufferSize < 1024 ) { + bufferSize = 1024; + } + rawData = new byte[bufferSize]; + clearText = rawData; + } + #endregion + + /// + /// Get the length of bytes bytes in the + /// + public int RawLength + { + get { + return rawLength; + } + } + + /// + /// Get the contents of the raw data buffer. + /// + /// This may contain encrypted data. + public byte[] RawData + { + get { + return rawData; + } + } + + /// + /// Get the number of useable bytes in + /// + public int ClearTextLength + { + get { + return clearTextLength; + } + } + + /// + /// Get the contents of the clear text buffer. + /// + public byte[] ClearText + { + get { + return clearText; + } + } + + /// + /// Get/set the number of bytes available + /// + public int Available + { + get { return available; } + set { available = value; } + } + + /// + /// Call passing the current clear text buffer contents. + /// + /// The inflater to set input for. + public void SetInflaterInput(Inflater inflater) + { + if ( available > 0 ) { + inflater.SetInput(clearText, clearTextLength - available, available); + available = 0; + } + } + + /// + /// Fill the buffer from the underlying input stream. + /// + public void Fill() + { + rawLength = 0; + int toRead = rawData.Length; + + while (toRead > 0) { + int count = inputStream.Read(rawData, rawLength, toRead); + if ( count <= 0 ) { + break; + } + rawLength += count; + toRead -= count; + } + +#if !NETCF_1_0 + if ( cryptoTransform != null ) { + clearTextLength = cryptoTransform.TransformBlock(rawData, 0, rawLength, clearText, 0); + } + else +#endif + { + clearTextLength = rawLength; + } + + available = clearTextLength; + } + + /// + /// Read a buffer directly from the input stream + /// + /// The buffer to fill + /// Returns the number of bytes read. + public int ReadRawBuffer(byte[] buffer) + { + return ReadRawBuffer(buffer, 0, buffer.Length); + } + + /// + /// Read a buffer directly from the input stream + /// + /// The buffer to read into + /// The offset to start reading data into. + /// The number of bytes to read. + /// Returns the number of bytes read. + public int ReadRawBuffer(byte[] outBuffer, int offset, int length) + { + if ( length < 0 ) { + throw new ArgumentOutOfRangeException("length"); + } + + int currentOffset = offset; + int currentLength = length; + + while ( currentLength > 0 ) { + if ( available <= 0 ) { + Fill(); + if (available <= 0) { + return 0; + } + } + int toCopy = Math.Min(currentLength, available); + System.Array.Copy(rawData, rawLength - (int)available, outBuffer, currentOffset, toCopy); + currentOffset += toCopy; + currentLength -= toCopy; + available -= toCopy; + } + return length; + } + + /// + /// Read clear text data from the input stream. + /// + /// The buffer to add data to. + /// The offset to start adding data at. + /// The number of bytes to read. + /// Returns the number of bytes actually read. + public int ReadClearTextBuffer(byte[] outBuffer, int offset, int length) + { + if ( length < 0 ) { + throw new ArgumentOutOfRangeException("length"); + } + + int currentOffset = offset; + int currentLength = length; + + while ( currentLength > 0 ) { + if ( available <= 0 ) { + Fill(); + if (available <= 0) { + return 0; + } + } + + int toCopy = Math.Min(currentLength, available); + Array.Copy(clearText, clearTextLength - (int)available, outBuffer, currentOffset, toCopy); + currentOffset += toCopy; + currentLength -= toCopy; + available -= toCopy; + } + return length; + } + + /// + /// Read a from the input stream. + /// + /// Returns the byte read. + public int ReadLeByte() + { + if (available <= 0) { + Fill(); + if (available <= 0) { + throw new ZipException("EOF in header"); + } + } + byte result = rawData[rawLength - available]; + available -= 1; + return result; + } + + /// + /// Read an in little endian byte order. + /// + /// The short value read case to an int. + public int ReadLeShort() + { + return ReadLeByte() | (ReadLeByte() << 8); + } + + /// + /// Read an in little endian byte order. + /// + /// The int value read. + public int ReadLeInt() + { + return ReadLeShort() | (ReadLeShort() << 16); + } + + /// + /// Read a in little endian byte order. + /// + /// The long value read. + public long ReadLeLong() + { + return (uint)ReadLeInt() | ((long)ReadLeInt() << 32); + } + +#if !NETCF_1_0 + /// + /// Get/set the to apply to any data. + /// + /// Set this value to null to have no transform applied. + public ICryptoTransform CryptoTransform + { + set { + cryptoTransform = value; + if ( cryptoTransform != null ) { + if ( rawData == clearText ) { + if ( internalClearText == null ) { + internalClearText = new byte[rawData.Length]; + } + clearText = internalClearText; + } + clearTextLength = rawLength; + if ( available > 0 ) { + cryptoTransform.TransformBlock(rawData, rawLength - available, available, clearText, rawLength - available); + } + } else { + clearText = rawData; + clearTextLength = rawLength; + } + } + } +#endif + + #region Instance Fields + int rawLength; + byte[] rawData; + + int clearTextLength; + byte[] clearText; +#if !NETCF_1_0 + byte[] internalClearText; +#endif + + int available; + +#if !NETCF_1_0 + ICryptoTransform cryptoTransform; +#endif + Stream inputStream; + #endregion + } + + /// + /// This filter stream is used to decompress data compressed using the "deflate" + /// format. The "deflate" format is described in RFC 1951. + /// + /// This stream may form the basis for other decompression filters, such + /// as the GZipInputStream. + /// + /// Author of the original java version : John Leuner. + /// + public class InflaterInputStream : Stream + { + #region Constructors + /// + /// Create an InflaterInputStream with the default decompressor + /// and a default buffer size of 4KB. + /// + /// + /// The InputStream to read bytes from + /// + public InflaterInputStream(Stream baseInputStream) + : this(baseInputStream, new Inflater(), 4096) + { + } + + /// + /// Create an InflaterInputStream with the specified decompressor + /// and a default buffer size of 4KB. + /// + /// + /// The source of input data + /// + /// + /// The decompressor used to decompress data read from baseInputStream + /// + public InflaterInputStream(Stream baseInputStream, Inflater inf) + : this(baseInputStream, inf, 4096) + { + } + + /// + /// Create an InflaterInputStream with the specified decompressor + /// and the specified buffer size. + /// + /// + /// The InputStream to read bytes from + /// + /// + /// The decompressor to use + /// + /// + /// Size of the buffer to use + /// + public InflaterInputStream(Stream baseInputStream, Inflater inflater, int bufferSize) + { + if (baseInputStream == null) { + throw new ArgumentNullException("baseInputStream"); + } + + if (inflater == null) { + throw new ArgumentNullException("inflater"); + } + + if (bufferSize <= 0) { + throw new ArgumentOutOfRangeException("bufferSize"); + } + + this.baseInputStream = baseInputStream; + this.inf = inflater; + + inputBuffer = new InflaterInputBuffer(baseInputStream, bufferSize); + } + + #endregion + + /// + /// Get/set flag indicating ownership of underlying stream. + /// When the flag is true will close the underlying stream also. + /// + /// + /// The default value is true. + /// + public bool IsStreamOwner + { + get { return isStreamOwner; } + set { isStreamOwner = value; } + } + + /// + /// Skip specified number of bytes of uncompressed data + /// + /// + /// Number of bytes to skip + /// + /// + /// The number of bytes skipped, zero if the end of + /// stream has been reached + /// + /// + /// The number of bytes to skip is less than or equal to zero. + /// + public long Skip(long count) + { + if (count <= 0) { + throw new ArgumentOutOfRangeException("count"); + } + + // v0.80 Skip by seeking if underlying stream supports it... + if (baseInputStream.CanSeek) { + baseInputStream.Seek(count, SeekOrigin.Current); + return count; + } + else { + int length = 2048; + if (count < length) { + length = (int) count; + } + + byte[] tmp = new byte[length]; + int readCount = 1; + long toSkip = count; + + while ((toSkip > 0) && (readCount > 0) ) { + if (toSkip < length) { + length = (int)toSkip; + } + + readCount = baseInputStream.Read(tmp, 0, length); + toSkip -= readCount; + } + + return count - toSkip; + } + } + + /// + /// Clear any cryptographic state. + /// + protected void StopDecrypting() + { +#if !NETCF_1_0 + inputBuffer.CryptoTransform = null; +#endif + } + + /// + /// Returns 0 once the end of the stream (EOF) has been reached. + /// Otherwise returns 1. + /// + public virtual int Available + { + get { + return inf.IsFinished ? 0 : 1; + } + } + + /// + /// Fills the buffer with more data to decompress. + /// + /// + /// Stream ends early + /// + protected void Fill() + { + // Protect against redundant calls + if (inputBuffer.Available <= 0) { + inputBuffer.Fill(); + if (inputBuffer.Available <= 0) { + throw new SharpZipBaseException("Unexpected EOF"); + } + } + inputBuffer.SetInflaterInput(inf); + } + + #region Stream Overrides + /// + /// Gets a value indicating whether the current stream supports reading + /// + public override bool CanRead + { + get { + return baseInputStream.CanRead; + } + } + + /// + /// Gets a value of false indicating seeking is not supported for this stream. + /// + public override bool CanSeek { + get { + return false; + } + } + + /// + /// Gets a value of false indicating that this stream is not writeable. + /// + public override bool CanWrite { + get { + return false; + } + } + + /// + /// A value representing the length of the stream in bytes. + /// + public override long Length { + get { + return inputBuffer.RawLength; + } + } + + /// + /// The current position within the stream. + /// Throws a NotSupportedException when attempting to set the position + /// + /// Attempting to set the position + public override long Position { + get { + return baseInputStream.Position; + } + set { + throw new NotSupportedException("InflaterInputStream Position not supported"); + } + } + + /// + /// Flushes the baseInputStream + /// + public override void Flush() + { + baseInputStream.Flush(); + } + + /// + /// Sets the position within the current stream + /// Always throws a NotSupportedException + /// + /// The relative offset to seek to. + /// The defining where to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("Seek not supported"); + } + + /// + /// Set the length of the current stream + /// Always throws a NotSupportedException + /// + /// The new length value for the stream. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("InflaterInputStream SetLength not supported"); + } + + /// + /// Writes a sequence of bytes to stream and advances the current position + /// This method always throws a NotSupportedException + /// + /// Thew buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Any access + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("InflaterInputStream Write not supported"); + } + + /// + /// Writes one byte to the current stream and advances the current position + /// Always throws a NotSupportedException + /// + /// The byte to write. + /// Any access + public override void WriteByte(byte value) + { + throw new NotSupportedException("InflaterInputStream WriteByte not supported"); + } + + /// + /// Entry point to begin an asynchronous write. Always throws a NotSupportedException. + /// + /// The buffer to write data from + /// Offset of first byte to write + /// The maximum number of bytes to write + /// The method to be called when the asynchronous write operation is completed + /// A user-provided object that distinguishes this particular asynchronous write request from other requests + /// An IAsyncResult that references the asynchronous write + /// Any access + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotSupportedException("InflaterInputStream BeginWrite not supported"); + } + + /// + /// Closes the input stream. When + /// is true the underlying stream is also closed. + /// + public override void Close() + { + if ( !isClosed ) { + isClosed = true; + if ( isStreamOwner ) { + baseInputStream.Close(); + } + } + } + + /// + /// Reads decompressed data into the provided buffer byte array + /// + /// + /// The array to read and decompress data into + /// + /// + /// The offset indicating where the data should be placed + /// + /// + /// The number of bytes to decompress + /// + /// The number of bytes read. Zero signals the end of stream + /// + /// Inflater needs a dictionary + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (inf.IsNeedingDictionary) + { + throw new SharpZipBaseException("Need a dictionary"); + } + + int remainingBytes = count; + while (true) { + int bytesRead = inf.Inflate(buffer, offset, remainingBytes); + offset += bytesRead; + remainingBytes -= bytesRead; + + if (remainingBytes == 0 || inf.IsFinished) { + break; + } + + if ( inf.IsNeedingInput ) { + Fill(); + } + else if ( bytesRead == 0 ) { + throw new ZipException("Dont know what to do"); + } + } + return count - remainingBytes; + } + #endregion + + #region Instance Fields + /// + /// Decompressor for this stream + /// + protected Inflater inf; + + /// + /// Input buffer for this stream. + /// + protected InflaterInputBuffer inputBuffer; + + /// + /// Base stream the inflater reads from. + /// + private Stream baseInputStream; + + /// + /// The compressed size + /// + protected long csize; + + /// + /// Flag indicating wether this instance has been closed or not. + /// + bool isClosed; + + /// + /// Flag indicating wether this instance is designated the stream owner. + /// When closing if this flag is true the underlying stream is closed. + /// + bool isStreamOwner = true; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs new file mode 100644 index 000000000..9114d0ca9 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/OutputWindow.cs @@ -0,0 +1,235 @@ +// OutputWindow.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + + /// + /// Contains the output from the Inflation process. + /// We need to have a window so that we can refer backwards into the output stream + /// to repeat stuff.
+ /// Author of the original java version : John Leuner + ///
+ public class OutputWindow + { + #region Constants + const int WindowSize = 1 << 15; + const int WindowMask = WindowSize - 1; + #endregion + + #region Instance Fields + byte[] window = new byte[WindowSize]; //The window is 2^15 bytes + int windowEnd; + int windowFilled; + #endregion + + /// + /// Write a byte to this output window + /// + /// value to write + /// + /// if window is full + /// + public void Write(int value) + { + if (windowFilled++ == WindowSize) { + throw new InvalidOperationException("Window full"); + } + window[windowEnd++] = (byte) value; + windowEnd &= WindowMask; + } + + + private void SlowRepeat(int repStart, int length, int distance) + { + while (length-- > 0) { + window[windowEnd++] = window[repStart++]; + windowEnd &= WindowMask; + repStart &= WindowMask; + } + } + + /// + /// Append a byte pattern already in the window itself + /// + /// length of pattern to copy + /// distance from end of window pattern occurs + /// + /// If the repeated data overflows the window + /// + public void Repeat(int length, int distance) + { + if ((windowFilled += length) > WindowSize) { + throw new InvalidOperationException("Window full"); + } + + int repStart = (windowEnd - distance) & WindowMask; + int border = WindowSize - length; + if ( (repStart <= border) && (windowEnd < border) ) { + if (length <= distance) { + System.Array.Copy(window, repStart, window, windowEnd, length); + windowEnd += length; + } else { + // We have to copy manually, since the repeat pattern overlaps. + while (length-- > 0) { + window[windowEnd++] = window[repStart++]; + } + } + } else { + SlowRepeat(repStart, length, distance); + } + } + + /// + /// Copy from input manipulator to internal window + /// + /// source of data + /// length of data to copy + /// the number of bytes copied + public int CopyStored(StreamManipulator input, int length) + { + length = Math.Min(Math.Min(length, WindowSize - windowFilled), input.AvailableBytes); + int copied; + + int tailLen = WindowSize - windowEnd; + if (length > tailLen) { + copied = input.CopyBytes(window, windowEnd, tailLen); + if (copied == tailLen) { + copied += input.CopyBytes(window, 0, length - tailLen); + } + } else { + copied = input.CopyBytes(window, windowEnd, length); + } + + windowEnd = (windowEnd + copied) & WindowMask; + windowFilled += copied; + return copied; + } + + /// + /// Copy dictionary to window + /// + /// source dictionary + /// offset of start in source dictionary + /// length of dictionary + /// + /// If window isnt empty + /// + public void CopyDict(byte[] dictionary, int offset, int length) + { + if ( dictionary == null ) { + throw new ArgumentNullException("dictionary"); + } + + if (windowFilled > 0) { + throw new InvalidOperationException(); + } + + if (length > WindowSize) { + offset += length - WindowSize; + length = WindowSize; + } + System.Array.Copy(dictionary, offset, window, 0, length); + windowEnd = length & WindowMask; + } + + /// + /// Get remaining unfilled space in window + /// + /// Number of bytes left in window + public int GetFreeSpace() + { + return WindowSize - windowFilled; + } + + /// + /// Get bytes available for output in window + /// + /// Number of bytes filled + public int GetAvailable() + { + return windowFilled; + } + + /// + /// Copy contents of window to output + /// + /// buffer to copy to + /// offset to start at + /// number of bytes to count + /// The number of bytes copied + /// + /// If a window underflow occurs + /// + public int CopyOutput(byte[] output, int offset, int len) + { + int copyEnd = windowEnd; + if (len > windowFilled) { + len = windowFilled; + } else { + copyEnd = (windowEnd - windowFilled + len) & WindowMask; + } + + int copied = len; + int tailLen = len - copyEnd; + + if (tailLen > 0) { + System.Array.Copy(window, WindowSize - tailLen, output, offset, tailLen); + offset += tailLen; + len = copyEnd; + } + System.Array.Copy(window, copyEnd - len, output, offset, len); + windowFilled -= copied; + if (windowFilled < 0) { + throw new InvalidOperationException(); + } + return copied; + } + + /// + /// Reset by clearing window so GetAvailable returns 0 + /// + public void Reset() + { + windowFilled = windowEnd = 0; + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs new file mode 100644 index 000000000..0a30e6228 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/Compression/Streams/StreamManipulator.cs @@ -0,0 +1,297 @@ +// StreamManipulator.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams +{ + + /// + /// This class allows us to retrieve a specified number of bits from + /// the input buffer, as well as copy big byte blocks. + /// + /// It uses an int buffer to store up to 31 bits for direct + /// manipulation. This guarantees that we can get at least 16 bits, + /// but we only need at most 15, so this is all safe. + /// + /// There are some optimizations in this class, for example, you must + /// never peek more than 8 bits more than needed, and you must first + /// peek bits before you may drop them. This is not a general purpose + /// class but optimized for the behaviour of the Inflater. + /// + /// authors of the original java version : John Leuner, Jochen Hoenicke + /// + public class StreamManipulator + { + #region Constructors + /// + /// Constructs a default StreamManipulator with all buffers empty + /// + public StreamManipulator() + { + } + #endregion + + /// + /// Get the next sequence of bits but don't increase input pointer. bitCount must be + /// less or equal 16 and if this call succeeds, you must drop + /// at least n - 8 bits in the next call. + /// + /// The number of bits to peek. + /// + /// the value of the bits, or -1 if not enough bits available. */ + /// + public int PeekBits(int bitCount) + { + if (bitsInBuffer_ < bitCount) { + if (windowStart_ == windowEnd_) { + return -1; // ok + } + buffer_ |= (uint)((window_[windowStart_++] & 0xff | + (window_[windowStart_++] & 0xff) << 8) << bitsInBuffer_); + bitsInBuffer_ += 16; + } + return (int)(buffer_ & ((1 << bitCount) - 1)); + } + + /// + /// Drops the next n bits from the input. You should have called PeekBits + /// with a bigger or equal n before, to make sure that enough bits are in + /// the bit buffer. + /// + /// The number of bits to drop. + public void DropBits(int bitCount) + { + buffer_ >>= bitCount; + bitsInBuffer_ -= bitCount; + } + + /// + /// Gets the next n bits and increases input pointer. This is equivalent + /// to followed by , except for correct error handling. + /// + /// The number of bits to retrieve. + /// + /// the value of the bits, or -1 if not enough bits available. + /// + public int GetBits(int bitCount) + { + int bits = PeekBits(bitCount); + if (bits >= 0) { + DropBits(bitCount); + } + return bits; + } + + /// + /// Gets the number of bits available in the bit buffer. This must be + /// only called when a previous PeekBits() returned -1. + /// + /// + /// the number of bits available. + /// + public int AvailableBits { + get { + return bitsInBuffer_; + } + } + + /// + /// Gets the number of bytes available. + /// + /// + /// The number of bytes available. + /// + public int AvailableBytes { + get { + return windowEnd_ - windowStart_ + (bitsInBuffer_ >> 3); + } + } + + /// + /// Skips to the next byte boundary. + /// + public void SkipToByteBoundary() + { + buffer_ >>= (bitsInBuffer_ & 7); + bitsInBuffer_ &= ~7; + } + + /// + /// Returns true when SetInput can be called + /// + public bool IsNeedingInput { + get { + return windowStart_ == windowEnd_; + } + } + + /// + /// Copies bytes from input buffer to output buffer starting + /// at output[offset]. You have to make sure, that the buffer is + /// byte aligned. If not enough bytes are available, copies fewer + /// bytes. + /// + /// + /// The buffer to copy bytes to. + /// + /// + /// The offset in the buffer at which copying starts + /// + /// + /// The length to copy, 0 is allowed. + /// + /// + /// The number of bytes copied, 0 if no bytes were available. + /// + /// + /// Length is less than zero + /// + /// + /// Bit buffer isnt byte aligned + /// + public int CopyBytes(byte[] output, int offset, int length) + { + if (length < 0) { + throw new ArgumentOutOfRangeException("length"); + } + + if ((bitsInBuffer_ & 7) != 0) { + // bits_in_buffer may only be 0 or a multiple of 8 + throw new InvalidOperationException("Bit buffer is not byte aligned!"); + } + + int count = 0; + while ((bitsInBuffer_ > 0) && (length > 0)) { + output[offset++] = (byte) buffer_; + buffer_ >>= 8; + bitsInBuffer_ -= 8; + length--; + count++; + } + + if (length == 0) { + return count; + } + + int avail = windowEnd_ - windowStart_; + if (length > avail) { + length = avail; + } + System.Array.Copy(window_, windowStart_, output, offset, length); + windowStart_ += length; + + if (((windowStart_ - windowEnd_) & 1) != 0) { + // We always want an even number of bytes in input, see peekBits + buffer_ = (uint)(window_[windowStart_++] & 0xff); + bitsInBuffer_ = 8; + } + return count + length; + } + + /// + /// Resets state and empties internal buffers + /// + public void Reset() + { + buffer_ = 0; + windowStart_ = windowEnd_ = bitsInBuffer_ = 0; + } + + /// + /// Add more input for consumption. + /// Only call when IsNeedingInput returns true + /// + /// data to be input + /// offset of first byte of input + /// number of bytes of input to add. + public void SetInput(byte[] buffer, int offset, int count) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "Cannot be negative"); +#endif + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Cannot be negative"); +#endif + } + + if (windowStart_ < windowEnd_) { + throw new InvalidOperationException("Old input was not completely processed"); + } + + int end = offset + count; + + // We want to throw an ArrayIndexOutOfBoundsException early. + // Note the check also handles integer wrap around. + if ((offset > end) || (end > buffer.Length) ) { + throw new ArgumentOutOfRangeException("count"); + } + + if ((count & 1) != 0) { + // We always want an even number of bytes in input, see PeekBits + buffer_ |= (uint)((buffer[offset++] & 0xff) << bitsInBuffer_); + bitsInBuffer_ += 8; + } + + window_ = buffer; + windowStart_ = offset; + windowEnd_ = end; + } + + #region Instance Fields + private byte[] window_; + private int windowStart_; + private int windowEnd_; + + private uint buffer_; + private int bitsInBuffer_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/FastZip.cs b/src/GitHub.Api/SharpZipLib/Zip/FastZip.cs new file mode 100644 index 000000000..97c063ebd --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/FastZip.cs @@ -0,0 +1,729 @@ +// FastZip.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// FastZipEvents supports all events applicable to FastZip operations. + /// + public class FastZipEvents + { + /// + /// Delegate to invoke when processing directories. + /// + public ProcessDirectoryHandler ProcessDirectory; + + /// + /// Delegate to invoke when processing files. + /// + public ProcessFileHandler ProcessFile; + + /// + /// Delegate to invoke during processing of files. + /// + public ProgressHandler Progress; + + /// + /// Delegate to invoke when processing for a file has been completed. + /// + public CompletedFileHandler CompletedFile; + + /// + /// Delegate to invoke when processing directory failures. + /// + public DirectoryFailureHandler DirectoryFailure; + + /// + /// Delegate to invoke when processing file failures. + /// + public FileFailureHandler FileFailure; + + /// + /// Raise the directory failure event. + /// + /// The directory causing the failure. + /// The exception for this event. + /// A boolean indicating if execution should continue or not. + public bool OnDirectoryFailure(string directory, Exception e) + { + bool result = false; + DirectoryFailureHandler handler = DirectoryFailure; + + if ( handler != null ) { + ScanFailureEventArgs args = new ScanFailureEventArgs(directory, e); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the file failure handler delegate. + /// + /// The file causing the failure. + /// The exception for this failure. + /// A boolean indicating if execution should continue or not. + public bool OnFileFailure(string file, Exception e) + { + FileFailureHandler handler = FileFailure; + bool result = (handler != null); + + if ( result ) { + ScanFailureEventArgs args = new ScanFailureEventArgs(file, e); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the ProcessFile delegate. + /// + /// The file being processed. + /// A boolean indicating if execution should continue or not. + public bool OnProcessFile(string file) + { + bool result = true; + ProcessFileHandler handler = ProcessFile; + + if ( handler != null ) { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the delegate + /// + /// The file whose processing has been completed. + /// A boolean indicating if execution should continue or not. + public bool OnCompletedFile(string file) + { + bool result = true; + CompletedFileHandler handler = CompletedFile; + if ( handler != null ) { + ScanEventArgs args = new ScanEventArgs(file); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// Fires the process directory delegate. + /// + /// The directory being processed. + /// Flag indicating if the directory has matching files as determined by the current filter. + /// A of true if the operation should continue; false otherwise. + public bool OnProcessDirectory(string directory, bool hasMatchingFiles) + { + bool result = true; + ProcessDirectoryHandler handler = ProcessDirectory; + if ( handler != null ) { + DirectoryEventArgs args = new DirectoryEventArgs(directory, hasMatchingFiles); + handler(this, args); + result = args.ContinueRunning; + } + return result; + } + + /// + /// The minimum timespan between events. + /// + /// The minimum period of time between events. + /// + /// The default interval is three seconds. + public TimeSpan ProgressInterval + { + get { return progressInterval_; } + set { progressInterval_ = value; } + } + + #region Instance Fields + TimeSpan progressInterval_ = TimeSpan.FromSeconds(3); + #endregion + } + + /// + /// FastZip provides facilities for creating and extracting zip files. + /// + public class FastZip + { + #region Enumerations + /// + /// Defines the desired handling when overwriting files during extraction. + /// + public enum Overwrite + { + /// + /// Prompt the user to confirm overwriting + /// + Prompt, + /// + /// Never overwrite files. + /// + Never, + /// + /// Always overwrite files. + /// + Always + } + #endregion + + #region Constructors + /// + /// Initialise a default instance of . + /// + public FastZip() + { + } + + /// + /// Initialise a new instance of + /// + /// The events to use during operations. + public FastZip(FastZipEvents events) + { + events_ = events; + } + #endregion + + #region Properties + /// + /// Get/set a value indicating wether empty directories should be created. + /// + public bool CreateEmptyDirectories + { + get { return createEmptyDirectories_; } + set { createEmptyDirectories_ = value; } + } + +#if !NETCF_1_0 + /// + /// Get / set the password value. + /// + public string Password + { + get { return password_; } + set { password_ = value; } + } +#endif + + /// + /// Get or set the active when creating Zip files. + /// + /// + public INameTransform NameTransform + { + get { return entryFactory_.NameTransform; } + set { + entryFactory_.NameTransform = value; + } + } + + /// + /// Get or set the active when creating Zip files. + /// + public IEntryFactory EntryFactory + { + get { return entryFactory_; } + set { + if ( value == null ) { + entryFactory_ = new ZipEntryFactory(); + } + else { + entryFactory_ = value; + } + } + } + + /// + /// Gets or sets the setting for Zip64 handling when writing. + /// + /// + /// The default value is dynamic which is not backwards compatible with old + /// programs and can cause problems with XP's built in compression which cant + /// read Zip64 archives. However it does avoid the situation were a large file + /// is added and cannot be completed correctly. + /// NOTE: Setting the size for entries before they are added is the best solution! + /// By default the EntryFactory used by FastZip will set fhe file size. + /// + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + /// + /// Get/set a value indicating wether file dates and times should + /// be restored when extracting files from an archive. + /// + /// The default value is false. + public bool RestoreDateTimeOnExtract + { + get { + return restoreDateTimeOnExtract_; + } + set { + restoreDateTimeOnExtract_ = value; + } + } + + /// + /// Get/set a value indicating wether file attributes should + /// be restored during extract operations + /// + public bool RestoreAttributesOnExtract + { + get { return restoreAttributesOnExtract_; } + set { restoreAttributesOnExtract_ = value; } + } + #endregion + + #region Delegates + /// + /// Delegate called when confirming overwriting of files. + /// + public delegate bool ConfirmOverwriteDelegate(string fileName); + #endregion + + #region CreateZip + /// + /// Create a zip file. + /// + /// The name of the zip file to create. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, + bool recurse, string fileFilter, string directoryFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter); + } + + /// + /// Create a zip file/archive. + /// + /// The name of the zip file to create. + /// The directory to obtain files and directories from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter) + { + CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, null); + } + + /// + /// Create a zip archive sending output to the passed. + /// + /// The stream to write archive data to. + /// The directory to source files from. + /// True to recurse directories, false for no recursion. + /// The file filter to apply. + /// The directory filter to apply. + /// The is closed after creation. + public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter) + { + NameTransform = new ZipNameTransform(sourceDirectory); + sourceDirectory_ = sourceDirectory; + + using ( outputStream_ = new ZipOutputStream(outputStream) ) { + +#if !NETCF_1_0 + if ( password_ != null ) { + outputStream_.Password = password_; + } +#endif + + outputStream_.UseZip64 = UseZip64; + FileSystemScanner scanner = new FileSystemScanner(fileFilter, directoryFilter); + scanner.ProcessFile += new ProcessFileHandler(ProcessFile); + if ( this.CreateEmptyDirectories ) { + scanner.ProcessDirectory += new ProcessDirectoryHandler(ProcessDirectory); + } + + if (events_ != null) { + if ( events_.FileFailure != null ) { + scanner.FileFailure += events_.FileFailure; + } + + if ( events_.DirectoryFailure != null ) { + scanner.DirectoryFailure += events_.DirectoryFailure; + } + } + + scanner.Scan(sourceDirectory, recurse); + } + } + + #endregion + + #region ExtractZip + /// + /// Extract the contents of a zip file. + /// + /// The zip file to extract from. + /// The directory to save extracted information in. + /// A filter to apply to files. + public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter) + { + ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null, restoreDateTimeOnExtract_); + } + + /// + /// Extract the contents of a zip file. + /// + /// The zip file to extract from. + /// The directory to save extracted information in. + /// The style of overwriting to apply. + /// A delegate to invoke when confirming overwriting. + /// A filter to apply to files. + /// A filter to apply to directories. + /// Flag indicating whether to restore the date and time for extracted files. + public void ExtractZip(string zipFileName, string targetDirectory, + Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, + string fileFilter, string directoryFilter, bool restoreDateTime) + { + Stream inputStream = File.Open(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read); + ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true); + } + + /// + /// Extract the contents of a zip file held in a stream. + /// + /// The seekable input stream containing the zip to extract from. + /// The directory to save extracted information in. + /// The style of overwriting to apply. + /// A delegate to invoke when confirming overwriting. + /// A filter to apply to files. + /// A filter to apply to directories. + /// Flag indicating whether to restore the date and time for extracted files. + /// Flag indicating whether the inputStream will be closed by this method. + public void ExtractZip(Stream inputStream, string targetDirectory, + Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, + string fileFilter, string directoryFilter, bool restoreDateTime, + bool isStreamOwner) + { + if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) { + throw new ArgumentNullException("confirmDelegate"); + } + + continueRunning_ = true; + overwrite_ = overwrite; + confirmDelegate_ = confirmDelegate; + extractNameTransform_ = new WindowsNameTransform(targetDirectory); + + fileFilter_ = new NameFilter(fileFilter); + directoryFilter_ = new NameFilter(directoryFilter); + restoreDateTimeOnExtract_ = restoreDateTime; + + using (zipFile_ = new ZipFile(inputStream)) { + +#if !NETCF_1_0 + if (password_ != null) { + zipFile_.Password = password_; + } +#endif + zipFile_.IsStreamOwner = isStreamOwner; + System.Collections.IEnumerator enumerator = zipFile_.GetEnumerator(); + while (continueRunning_ && enumerator.MoveNext()) { + ZipEntry entry = (ZipEntry)enumerator.Current; + if (entry.IsFile) + { + // TODO Path.GetDirectory can fail here on invalid characters. + if (directoryFilter_.IsMatch(Path.GetDirectoryName(entry.Name)) && fileFilter_.IsMatch(entry.Name)) { + ExtractEntry(entry); + } + } + else if (entry.IsDirectory) { + if (directoryFilter_.IsMatch(entry.Name) && CreateEmptyDirectories) { + ExtractEntry(entry); + } + } + else { + // Do nothing for volume labels etc... + } + } + } + } + #endregion + + #region Internal Processing + void ProcessDirectory(object sender, DirectoryEventArgs e) + { + if ( !e.HasMatchingFiles && CreateEmptyDirectories ) { + if ( events_ != null ) { + events_.OnProcessDirectory(e.Name, e.HasMatchingFiles); + } + + if ( e.ContinueRunning ) { + if (e.Name != sourceDirectory_) { + ZipEntry entry = entryFactory_.MakeDirectoryEntry(e.Name); + outputStream_.PutNextEntry(entry); + } + } + } + } + + void ProcessFile(object sender, ScanEventArgs e) + { + if ( (events_ != null) && (events_.ProcessFile != null) ) { + events_.ProcessFile(sender, e); + } + + if ( e.ContinueRunning ) { + try { + // The open below is equivalent to OpenRead which gaurantees that if opened the + // file will not be changed by subsequent openers, but precludes opening in some cases + // were it could succeed. + using (FileStream stream = File.Open(e.Name, FileMode.Open, FileAccess.Read, FileShare.Read)) { + ZipEntry entry = entryFactory_.MakeFileEntry(e.Name); + outputStream_.PutNextEntry(entry); + AddFileContents(e.Name, stream); + } + } + catch(Exception ex) { + if (events_ != null) { + continueRunning_ = events_.OnFileFailure(e.Name, ex); + } + else { + continueRunning_ = false; + throw; + } + } + } + } + + void AddFileContents(string name, Stream stream) + { + if( stream==null ) { + throw new ArgumentNullException("stream"); + } + + if( buffer_==null ) { + buffer_=new byte[4096]; + } + + if( (events_!=null)&&(events_.Progress!=null) ) { + StreamUtils.Copy(stream, outputStream_, buffer_, + events_.Progress, events_.ProgressInterval, this, name); + } + else { + StreamUtils.Copy(stream, outputStream_, buffer_); + } + + if( events_!=null ) { + continueRunning_=events_.OnCompletedFile(name); + } + } + + void ExtractFileEntry(ZipEntry entry, string targetName) + { + bool proceed = true; + if ( overwrite_ != Overwrite.Always ) { + if ( File.Exists(targetName) ) { + if ( (overwrite_ == Overwrite.Prompt) && (confirmDelegate_ != null) ) { + proceed = confirmDelegate_(targetName); + } + else { + proceed = false; + } + } + } + + if ( proceed ) { + if ( events_ != null ) { + continueRunning_ = events_.OnProcessFile(entry.Name); + } + + if ( continueRunning_ ) { + try { + using ( FileStream outputStream = File.Create(targetName) ) { + if ( buffer_ == null ) { + buffer_ = new byte[4096]; + } + if ((events_ != null) && (events_.Progress != null)) + { + StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_, + events_.Progress, events_.ProgressInterval, this, entry.Name, entry.Size); + } + else + { + StreamUtils.Copy(zipFile_.GetInputStream(entry), outputStream, buffer_); + } + + if (events_ != null) { + continueRunning_ = events_.OnCompletedFile(entry.Name); + } + } + +#if !NETCF_1_0 && !NETCF_2_0 + if ( restoreDateTimeOnExtract_ ) { + File.SetLastWriteTime(targetName, entry.DateTime); + } + + if ( RestoreAttributesOnExtract && entry.IsDOSEntry && (entry.ExternalFileAttributes != -1)) { + FileAttributes fileAttributes = (FileAttributes) entry.ExternalFileAttributes; + // TODO: FastZip - Setting of other file attributes on extraction is a little trickier. + fileAttributes &= (FileAttributes.Archive | FileAttributes.Normal | FileAttributes.ReadOnly | FileAttributes.Hidden); + File.SetAttributes(targetName, fileAttributes); + } +#endif + } + catch(Exception ex) { + if ( events_ != null ) { + continueRunning_ = events_.OnFileFailure(targetName, ex); + } + else { + continueRunning_ = false; + throw; + } + } + } + } + } + + void ExtractEntry(ZipEntry entry) + { + bool doExtraction = entry.IsCompressionMethodSupported(); + string targetName = entry.Name; + + if ( doExtraction ) { + if ( entry.IsFile ) { + targetName = extractNameTransform_.TransformFile(targetName); + } + else if ( entry.IsDirectory ) { + targetName = extractNameTransform_.TransformDirectory(targetName); + } + + doExtraction = !((targetName == null) || (targetName.Length == 0)); + } + + // TODO: Fire delegate/throw exception were compression method not supported, or name is invalid? + + string dirName = null; + + if ( doExtraction ) { + if ( entry.IsDirectory ) { + dirName = targetName; + } + else { + dirName = Path.GetDirectoryName(Path.GetFullPath(targetName)); + } + } + + if ( doExtraction && !Directory.Exists(dirName) ) { + if ( !entry.IsDirectory || CreateEmptyDirectories ) { + try { + Directory.CreateDirectory(dirName); + } + catch (Exception ex) { + doExtraction = false; + if ( events_ != null ) { + if ( entry.IsDirectory ) { + continueRunning_ = events_.OnDirectoryFailure(targetName, ex); + } + else { + continueRunning_ = events_.OnFileFailure(targetName, ex); + } + } + else { + continueRunning_ = false; + throw; + } + } + } + } + + if ( doExtraction && entry.IsFile ) { + ExtractFileEntry(entry, targetName); + } + } + + static int MakeExternalAttributes(FileInfo info) + { + return (int)info.Attributes; + } + +#if NET_1_0 || NET_1_1 || NETCF_1_0 + static bool NameIsValid(string name) + { + return (name != null) && + (name.Length > 0) && + (name.IndexOfAny(Path.InvalidPathChars) < 0); + } +#else + static bool NameIsValid(string name) + { + return (name != null) && + (name.Length > 0) && + (name.IndexOfAny(Path.GetInvalidPathChars()) < 0); + } +#endif + #endregion + + #region Instance Fields + bool continueRunning_; + byte[] buffer_; + ZipOutputStream outputStream_; + ZipFile zipFile_; + string sourceDirectory_; + NameFilter fileFilter_; + NameFilter directoryFilter_; + Overwrite overwrite_; + ConfirmOverwriteDelegate confirmDelegate_; + + bool restoreDateTimeOnExtract_; + bool restoreAttributesOnExtract_; + bool createEmptyDirectories_; + FastZipEvents events_; + IEntryFactory entryFactory_ = new ZipEntryFactory(); + INameTransform extractNameTransform_; + UseZip64 useZip64_=UseZip64.Dynamic; + +#if !NETCF_1_0 + string password_; +#endif + + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs b/src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs new file mode 100644 index 000000000..31c6f40d3 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/IEntryFactory.cs @@ -0,0 +1,82 @@ +// IEntryFactory.cs +// +// Copyright 2006 John Reilly +// +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// Defines factory methods for creating new values. + /// + public interface IEntryFactory + { + /// + /// Create a for a file given its name + /// + /// The name of the file to create an entry for. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName); + + /// + /// Create a for a file given its name + /// + /// The name of the file to create an entry for. + /// If true get details from the file system if the file exists. + /// Returns a file entry based on the passed. + ZipEntry MakeFileEntry(string fileName, bool useFileSystem); + + /// + /// Create a for a directory given its name + /// + /// The name of the directory to create an entry for. + /// Returns a directory entry based on the passed. + ZipEntry MakeDirectoryEntry(string directoryName); + + /// + /// Create a for a directory given its name + /// + /// The name of the directory to create an entry for. + /// If true get details from the file system for this directory if it exists. + /// Returns a directory entry based on the passed. + ZipEntry MakeDirectoryEntry(string directoryName, bool useFileSystem); + + /// + /// Get/set the applicable. + /// + INameTransform NameTransform { get; set; } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs b/src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs new file mode 100644 index 000000000..0ab439375 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/WindowsNameTransform.cs @@ -0,0 +1,272 @@ +// WindowsNameTransform.cs +// +// Copyright 2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; +using System.Text; + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// WindowsNameTransform transforms names to windows compatible ones. + /// + public class WindowsNameTransform : INameTransform + { + /// + /// Initialises a new instance of + /// + /// + public WindowsNameTransform(string baseDirectory) + { + if ( baseDirectory == null ) { + throw new ArgumentNullException("baseDirectory", "Directory name is invalid"); + } + + BaseDirectory = baseDirectory; + } + + /// + /// Initialise a default instance of + /// + public WindowsNameTransform() + { + // Do nothing. + } + + /// + /// Gets or sets a value containing the target directory to prefix values with. + /// + public string BaseDirectory + { + get { return _baseDirectory; } + set { + if ( value == null ) { + throw new ArgumentNullException("value"); + } + + _baseDirectory = Path.GetFullPath(value); + } + } + + /// + /// Gets or sets a value indicating wether paths on incoming values should be removed. + /// + public bool TrimIncomingPaths + { + get { return _trimIncomingPaths; } + set { _trimIncomingPaths = value; } + } + + /// + /// Transform a Zip directory name to a windows directory name. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + if (name.Length > 0) { + while ( name.EndsWith(@"\") ) { + name = name.Remove(name.Length - 1, 1); + } + } + else { + throw new ZipException("Cannot have an empty directory name"); + } + return name; + } + + /// + /// Transform a Zip format file name to a windows style one. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) { + name = MakeValidName(name, _replacementChar); + + if ( _trimIncomingPaths ) { + name = Path.GetFileName(name); + } + + // This may exceed windows length restrictions. + // Combine will throw a PathTooLongException in that case. + if ( _baseDirectory != null ) { + name = Path.Combine(_baseDirectory, name); + } + } + else { + name = string.Empty; + } + return name; + } + + /// + /// Test a name to see if it is a valid name for a windows filename as extracted from a Zip archive. + /// + /// The name to test. + /// Returns true if the name is a valid zip name; false otherwise. + /// The filename isnt a true windows path in some fundamental ways like no absolute paths, no rooted paths etc. + public static bool IsValidName(string name) + { + bool result = + (name != null) && + (name.Length <= MaxPath) && + (string.Compare(name, MakeValidName(name, '_')) == 0) + ; + + return result; + } + + /// + /// Initialise static class information. + /// + static WindowsNameTransform() + { + char[] invalidPathChars; + +#if NET_1_0 || NET_1_1 || NETCF_1_0 + invalidPathChars = Path.InvalidPathChars; +#else + invalidPathChars = Path.GetInvalidPathChars(); +#endif + int howMany = invalidPathChars.Length + 3; + + InvalidEntryChars = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryChars, 0, invalidPathChars.Length); + InvalidEntryChars[howMany - 1] = '*'; + InvalidEntryChars[howMany - 2] = '?'; + InvalidEntryChars[howMany - 3] = ':'; + } + + /// + /// Force a name to be valid by replacing invalid characters with a fixed value + /// + /// The name to make valid + /// The replacement character to use for any invalid characters. + /// Returns a valid name + public static string MakeValidName(string name, char replacement) + { + if ( name == null ) { + throw new ArgumentNullException("name"); + } + + name = WindowsPathUtils.DropPathRoot(name.Replace("/", @"\")); + + // Drop any leading slashes. + while ( (name.Length > 0) && (name[0] == '\\')) { + name = name.Remove(0, 1); + } + + // Drop any trailing slashes. + while ( (name.Length > 0) && (name[name.Length - 1] == '\\')) { + name = name.Remove(name.Length - 1, 1); + } + + // Convert consecutive \\ characters to \ + int index = name.IndexOf(@"\\"); + while (index >= 0) { + name = name.Remove(index, 1); + index = name.IndexOf(@"\\"); + } + + // Convert any invalid characters using the replacement one. + index = name.IndexOfAny(InvalidEntryChars); + if (index >= 0) { + StringBuilder builder = new StringBuilder(name); + + while (index >= 0 ) { + builder[index] = replacement; + + if (index >= name.Length) { + index = -1; + } + else { + index = name.IndexOfAny(InvalidEntryChars, index + 1); + } + } + name = builder.ToString(); + } + + // Check for names greater than MaxPath characters. + // TODO: Were is CLR version of MaxPath defined? Can't find it in Environment. + if ( name.Length > MaxPath ) { + throw new PathTooLongException(); + } + + return name; + } + + /// + /// Gets or set the character to replace invalid characters during transformations. + /// + public char Replacement + { + get { return _replacementChar; } + set { + for ( int i = 0; i < InvalidEntryChars.Length; ++i ) { + if ( InvalidEntryChars[i] == value ) { + throw new ArgumentException("invalid path character"); + } + } + + if ((value == '\\') || (value == '/')) { + throw new ArgumentException("invalid replacement character"); + } + + _replacementChar = value; + } + } + + /// + /// The maximum windows path name permitted. + /// + /// This may not valid for all windows systems - CE?, etc but I cant find the equivalent in the CLR. + const int MaxPath = 260; + + #region Instance Fields + string _baseDirectory; + bool _trimIncomingPaths; + char _replacementChar = '_'; + #endregion + + #region Class Fields + static readonly char[] InvalidEntryChars; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs new file mode 100644 index 000000000..e544eafd3 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipConstants.cs @@ -0,0 +1,632 @@ +// ZipConstants.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 DavidPierson Added AES support + +using System; +using System.Text; +using System.Threading; + +#if NETCF_1_0 || NETCF_2_0 +using System.Globalization; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + #region Enumerations + + /// + /// Determines how entries are tested to see if they should use Zip64 extensions or not. + /// + public enum UseZip64 + { + /// + /// Zip64 will not be forced on entries during processing. + /// + /// An entry can have this overridden if required + Off, + /// + /// Zip64 should always be used. + /// + On, + /// + /// #ZipLib will determine use based on entry values when added to archive. + /// + Dynamic, + } + + /// + /// The kind of compression used for an entry in an archive + /// + public enum CompressionMethod + { + /// + /// A direct copy of the file contents is held in the archive + /// + Stored = 0, + + /// + /// Common Zip compression method using a sliding dictionary + /// of up to 32KB and secondary compression from Huffman/Shannon-Fano trees + /// + Deflated = 8, + + /// + /// An extension to deflate with a 64KB window. Not supported by #Zip currently + /// + Deflate64 = 9, + + /// + /// BZip2 compression. Not supported by #Zip. + /// + BZip2 = 11, + + /// + /// WinZip special for AES encryption, Now supported by #Zip. + /// + WinZipAES = 99, + + } + + /// + /// Identifies the encryption algorithm used for an entry + /// + public enum EncryptionAlgorithm + { + /// + /// No encryption has been used. + /// + None = 0, + /// + /// Encrypted using PKZIP 2.0 or 'classic' encryption. + /// + PkzipClassic = 1, + /// + /// DES encryption has been used. + /// + Des = 0x6601, + /// + /// RCS encryption has been used for encryption. + /// + RC2 = 0x6602, + /// + /// Triple DES encryption with 168 bit keys has been used for this entry. + /// + TripleDes168 = 0x6603, + /// + /// Triple DES with 112 bit keys has been used for this entry. + /// + TripleDes112 = 0x6609, + /// + /// AES 128 has been used for encryption. + /// + Aes128 = 0x660e, + /// + /// AES 192 has been used for encryption. + /// + Aes192 = 0x660f, + /// + /// AES 256 has been used for encryption. + /// + Aes256 = 0x6610, + /// + /// RC2 corrected has been used for encryption. + /// + RC2Corrected = 0x6702, + /// + /// Blowfish has been used for encryption. + /// + Blowfish = 0x6720, + /// + /// Twofish has been used for encryption. + /// + Twofish = 0x6721, + /// + /// RC4 has been used for encryption. + /// + RC4 = 0x6801, + /// + /// An unknown algorithm has been used for encryption. + /// + Unknown = 0xffff + } + + /// + /// Defines the contents of the general bit flags field for an archive entry. + /// + [Flags] + public enum GeneralBitFlags : int + { + /// + /// Bit 0 if set indicates that the file is encrypted + /// + Encrypted = 0x0001, + /// + /// Bits 1 and 2 - Two bits defining the compression method (only for Method 6 Imploding and 8,9 Deflating) + /// + Method = 0x0006, + /// + /// Bit 3 if set indicates a trailing data desciptor is appended to the entry data + /// + Descriptor = 0x0008, + /// + /// Bit 4 is reserved for use with method 8 for enhanced deflation + /// + ReservedPKware4 = 0x0010, + /// + /// Bit 5 if set indicates the file contains Pkzip compressed patched data. + /// Requires version 2.7 or greater. + /// + Patched = 0x0020, + /// + /// Bit 6 if set indicates strong encryption has been used for this entry. + /// + StrongEncryption = 0x0040, + /// + /// Bit 7 is currently unused + /// + Unused7 = 0x0080, + /// + /// Bit 8 is currently unused + /// + Unused8 = 0x0100, + /// + /// Bit 9 is currently unused + /// + Unused9 = 0x0200, + /// + /// Bit 10 is currently unused + /// + Unused10 = 0x0400, + /// + /// Bit 11 if set indicates the filename and + /// comment fields for this file must be encoded using UTF-8. + /// + UnicodeText = 0x0800, + /// + /// Bit 12 is documented as being reserved by PKware for enhanced compression. + /// + EnhancedCompress = 0x1000, + /// + /// Bit 13 if set indicates that values in the local header are masked to hide + /// their actual values, and the central directory is encrypted. + /// + /// + /// Used when encrypting the central directory contents. + /// + HeaderMasked = 0x2000, + /// + /// Bit 14 is documented as being reserved for use by PKware + /// + ReservedPkware14 = 0x4000, + /// + /// Bit 15 is documented as being reserved for use by PKware + /// + ReservedPkware15 = 0x8000 + } + + #endregion + + /// + /// This class contains constants used for Zip format files + /// + public sealed class ZipConstants + { + #region Versions + /// + /// The version made by field for entries in the central header when created by this library + /// + /// + /// This is also the Zip version for the library when comparing against the version required to extract + /// for an entry. See . + /// + public const int VersionMadeBy = 51; // was 45 before AES + + /// + /// The version made by field for entries in the central header when created by this library + /// + /// + /// This is also the Zip version for the library when comparing against the version required to extract + /// for an entry. See ZipInputStream.CanDecompressEntry. + /// + [Obsolete("Use VersionMadeBy instead")] + public const int VERSION_MADE_BY = 51; + + /// + /// The minimum version required to support strong encryption + /// + public const int VersionStrongEncryption = 50; + + /// + /// The minimum version required to support strong encryption + /// + [Obsolete("Use VersionStrongEncryption instead")] + public const int VERSION_STRONG_ENCRYPTION = 50; + + /// + /// Version indicating AES encryption + /// + public const int VERSION_AES = 51; + + /// + /// The version required for Zip64 extensions (4.5 or higher) + /// + public const int VersionZip64 = 45; + #endregion + + #region Header Sizes + /// + /// Size of local entry header (excluding variable length fields at end) + /// + public const int LocalHeaderBaseSize = 30; + + /// + /// Size of local entry header (excluding variable length fields at end) + /// + [Obsolete("Use LocalHeaderBaseSize instead")] + public const int LOCHDR = 30; + + /// + /// Size of Zip64 data descriptor + /// + public const int Zip64DataDescriptorSize = 24; + + /// + /// Size of data descriptor + /// + public const int DataDescriptorSize = 16; + + /// + /// Size of data descriptor + /// + [Obsolete("Use DataDescriptorSize instead")] + public const int EXTHDR = 16; + + /// + /// Size of central header entry (excluding variable fields) + /// + public const int CentralHeaderBaseSize = 46; + + /// + /// Size of central header entry + /// + [Obsolete("Use CentralHeaderBaseSize instead")] + public const int CENHDR = 46; + + /// + /// Size of end of central record (excluding variable fields) + /// + public const int EndOfCentralRecordBaseSize = 22; + + /// + /// Size of end of central record (excluding variable fields) + /// + [Obsolete("Use EndOfCentralRecordBaseSize instead")] + public const int ENDHDR = 22; + + /// + /// Size of 'classic' cryptographic header stored before any entry data + /// + public const int CryptoHeaderSize = 12; + + /// + /// Size of cryptographic header stored before entry data + /// + [Obsolete("Use CryptoHeaderSize instead")] + public const int CRYPTO_HEADER_SIZE = 12; + #endregion + + #region Header Signatures + + /// + /// Signature for local entry header + /// + public const int LocalHeaderSignature = 'P' | ('K' << 8) | (3 << 16) | (4 << 24); + + /// + /// Signature for local entry header + /// + [Obsolete("Use LocalHeaderSignature instead")] + public const int LOCSIG = 'P' | ('K' << 8) | (3 << 16) | (4 << 24); + + /// + /// Signature for spanning entry + /// + public const int SpanningSignature = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for spanning entry + /// + [Obsolete("Use SpanningSignature instead")] + public const int SPANNINGSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for temporary spanning entry + /// + public const int SpanningTempSignature = 'P' | ('K' << 8) | ('0' << 16) | ('0' << 24); + + /// + /// Signature for temporary spanning entry + /// + [Obsolete("Use SpanningTempSignature instead")] + public const int SPANTEMPSIG = 'P' | ('K' << 8) | ('0' << 16) | ('0' << 24); + + /// + /// Signature for data descriptor + /// + /// + /// This is only used where the length, Crc, or compressed size isnt known when the + /// entry is created and the output stream doesnt support seeking. + /// The local entry cannot be 'patched' with the correct values in this case + /// so the values are recorded after the data prefixed by this header, as well as in the central directory. + /// + public const int DataDescriptorSignature = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for data descriptor + /// + /// + /// This is only used where the length, Crc, or compressed size isnt known when the + /// entry is created and the output stream doesnt support seeking. + /// The local entry cannot be 'patched' with the correct values in this case + /// so the values are recorded after the data prefixed by this header, as well as in the central directory. + /// + [Obsolete("Use DataDescriptorSignature instead")] + public const int EXTSIG = 'P' | ('K' << 8) | (7 << 16) | (8 << 24); + + /// + /// Signature for central header + /// + [Obsolete("Use CentralHeaderSignature instead")] + public const int CENSIG = 'P' | ('K' << 8) | (1 << 16) | (2 << 24); + + /// + /// Signature for central header + /// + public const int CentralHeaderSignature = 'P' | ('K' << 8) | (1 << 16) | (2 << 24); + + /// + /// Signature for Zip64 central file header + /// + public const int Zip64CentralFileHeaderSignature = 'P' | ('K' << 8) | (6 << 16) | (6 << 24); + + /// + /// Signature for Zip64 central file header + /// + [Obsolete("Use Zip64CentralFileHeaderSignature instead")] + public const int CENSIG64 = 'P' | ('K' << 8) | (6 << 16) | (6 << 24); + + /// + /// Signature for Zip64 central directory locator + /// + public const int Zip64CentralDirLocatorSignature = 'P' | ('K' << 8) | (6 << 16) | (7 << 24); + + /// + /// Signature for archive extra data signature (were headers are encrypted). + /// + public const int ArchiveExtraDataSignature = 'P' | ('K' << 8) | (6 << 16) | (7 << 24); + + /// + /// Central header digitial signature + /// + public const int CentralHeaderDigitalSignature = 'P' | ('K' << 8) | (5 << 16) | (5 << 24); + + /// + /// Central header digitial signature + /// + [Obsolete("Use CentralHeaderDigitalSignaure instead")] + public const int CENDIGITALSIG = 'P' | ('K' << 8) | (5 << 16) | (5 << 24); + + /// + /// End of central directory record signature + /// + public const int EndOfCentralDirectorySignature = 'P' | ('K' << 8) | (5 << 16) | (6 << 24); + + /// + /// End of central directory record signature + /// + [Obsolete("Use EndOfCentralDirectorySignature instead")] + public const int ENDSIG = 'P' | ('K' << 8) | (5 << 16) | (6 << 24); + #endregion + +#if NETCF_1_0 || NETCF_2_0 + // This isnt so great but is better than nothing. + // Trying to work out an appropriate OEM code page would be good. + // 850 is a good default for english speakers particularly in Europe. + static int defaultCodePage = CultureInfo.CurrentCulture.TextInfo.ANSICodePage; +#else + static int defaultCodePage = Thread.CurrentThread.CurrentCulture.TextInfo.OEMCodePage; +#endif + + /// + /// Default encoding used for string conversion. 0 gives the default system OEM code page. + /// Dont use unicode encodings if you want to be Zip compatible! + /// Using the default code page isnt the full solution neccessarily + /// there are many variable factors, codepage 850 is often a good choice for + /// European users, however be careful about compatability. + /// + public static int DefaultCodePage { + get { + return defaultCodePage; + } + set { + defaultCodePage = value; + } + } + + /// + /// Convert a portion of a byte array to a string. + /// + /// + /// Data to convert to string + /// + /// + /// Number of bytes to convert starting from index 0 + /// + /// + /// data[0]..data[length - 1] converted to a string + /// + public static string ConvertToString(byte[] data, int count) + { + if ( data == null ) { + return string.Empty; + } + + return Encoding.GetEncoding(DefaultCodePage).GetString(data, 0, count); + } + + /// + /// Convert a byte array to string + /// + /// + /// Byte array to convert + /// + /// + /// dataconverted to a string + /// + public static string ConvertToString(byte[] data) + { + if ( data == null ) { + return string.Empty; + } + return ConvertToString(data, data.Length); + } + + /// + /// Convert a byte array to string + /// + /// The applicable general purpose bits flags + /// + /// Byte array to convert + /// + /// The number of bytes to convert. + /// + /// dataconverted to a string + /// + public static string ConvertToStringExt(int flags, byte[] data, int count) + { + if ( data == null ) { + return string.Empty; + } + + if ( (flags & (int)GeneralBitFlags.UnicodeText) != 0 ) { + return Encoding.UTF8.GetString(data, 0, count); + } + else { + return ConvertToString(data, count); + } + } + + /// + /// Convert a byte array to string + /// + /// + /// Byte array to convert + /// + /// The applicable general purpose bits flags + /// + /// dataconverted to a string + /// + public static string ConvertToStringExt(int flags, byte[] data) + { + if ( data == null ) { + return string.Empty; + } + + if ( (flags & (int)GeneralBitFlags.UnicodeText) != 0 ) { + return Encoding.UTF8.GetString(data, 0, data.Length); + } + else { + return ConvertToString(data, data.Length); + } + } + + /// + /// Convert a string to a byte array + /// + /// + /// String to convert to an array + /// + /// Converted array + public static byte[] ConvertToArray(string str) + { + if ( str == null ) { + return new byte[0]; + } + + return Encoding.GetEncoding(DefaultCodePage).GetBytes(str); + } + + /// + /// Convert a string to a byte array + /// + /// The applicable general purpose bits flags + /// + /// String to convert to an array + /// + /// Converted array + public static byte[] ConvertToArray(int flags, string str) + { + if (str == null) { + return new byte[0]; + } + + if ((flags & (int)GeneralBitFlags.UnicodeText) != 0) { + return Encoding.UTF8.GetBytes(str); + } + else { + return ConvertToArray(str); + } + } + + + /// + /// Initialise default instance of ZipConstants + /// + /// + /// Private to prevent instances being created. + /// + ZipConstants() + { + // Do nothing + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs new file mode 100644 index 000000000..b1464c035 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipEntry.cs @@ -0,0 +1,1252 @@ +// ZipEntry.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 DavidPierson Added AES support +// 02-02-2010 DavidPierson Changed NTFS Extra Data min length to 4 + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + /// + /// Defines known values for the property. + /// + public enum HostSystemID + { + /// + /// Host system = MSDOS + /// + Msdos = 0, + /// + /// Host system = Amiga + /// + Amiga = 1, + /// + /// Host system = Open VMS + /// + OpenVms = 2, + /// + /// Host system = Unix + /// + Unix = 3, + /// + /// Host system = VMCms + /// + VMCms = 4, + /// + /// Host system = Atari ST + /// + AtariST = 5, + /// + /// Host system = OS2 + /// + OS2 = 6, + /// + /// Host system = Macintosh + /// + Macintosh = 7, + /// + /// Host system = ZSystem + /// + ZSystem = 8, + /// + /// Host system = Cpm + /// + Cpm = 9, + /// + /// Host system = Windows NT + /// + WindowsNT = 10, + /// + /// Host system = MVS + /// + MVS = 11, + /// + /// Host system = VSE + /// + Vse = 12, + /// + /// Host system = Acorn RISC + /// + AcornRisc = 13, + /// + /// Host system = VFAT + /// + Vfat = 14, + /// + /// Host system = Alternate MVS + /// + AlternateMvs = 15, + /// + /// Host system = BEOS + /// + BeOS = 16, + /// + /// Host system = Tandem + /// + Tandem = 17, + /// + /// Host system = OS400 + /// + OS400 = 18, + /// + /// Host system = OSX + /// + OSX = 19, + /// + /// Host system = WinZIP AES + /// + WinZipAES = 99, + } + + /// + /// This class represents an entry in a zip archive. This can be a file + /// or a directory + /// ZipFile and ZipInputStream will give you instances of this class as + /// information about the members in an archive. ZipOutputStream + /// uses an instance of this class when creating an entry in a Zip file. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ public class ZipEntry : ICloneable + { + [Flags] + enum Known : byte + { + None = 0, + Size = 0x01, + CompressedSize = 0x02, + Crc = 0x04, + Time = 0x08, + ExternalAttributes = 0x10, + } + + #region Constructors + /// + /// Creates a zip entry with the given name. + /// + /// + /// The name for this entry. Can include directory components. + /// The convention for names is 'unix' style paths with relative names only. + /// There are with no device names and path elements are separated by '/' characters. + /// + /// + /// The name passed is null + /// + public ZipEntry(string name) + : this(name, 0, ZipConstants.VersionMadeBy, CompressionMethod.Deflated) + { + } + + /// + /// Creates a zip entry with the given name and version required to extract + /// + /// + /// The name for this entry. Can include directory components. + /// The convention for names is 'unix' style paths with no device names and + /// path elements separated by '/' characters. This is not enforced see CleanName + /// on how to ensure names are valid if this is desired. + /// + /// + /// The minimum 'feature version' required this entry + /// + /// + /// The name passed is null + /// + internal ZipEntry(string name, int versionRequiredToExtract) + : this(name, versionRequiredToExtract, ZipConstants.VersionMadeBy, + CompressionMethod.Deflated) + { + } + + /// + /// Initializes an entry with the given name and made by information + /// + /// Name for this entry + /// Version and HostSystem Information + /// Minimum required zip feature version required to extract this entry + /// Compression method for this entry. + /// + /// The name passed is null + /// + /// + /// versionRequiredToExtract should be 0 (auto-calculate) or > 10 + /// + /// + /// This constructor is used by the ZipFile class when reading from the central header + /// It is not generally useful, use the constructor specifying the name only. + /// + internal ZipEntry(string name, int versionRequiredToExtract, int madeByInfo, + CompressionMethod method) + { + if (name == null) { + throw new System.ArgumentNullException("name"); + } + + if ( name.Length > 0xffff ) { + throw new ArgumentException("Name is too long", "name"); + } + + if ( (versionRequiredToExtract != 0) && (versionRequiredToExtract < 10) ) { + throw new ArgumentOutOfRangeException("versionRequiredToExtract"); + } + + this.DateTime = System.DateTime.Now; + this.name = name; + this.versionMadeBy = (ushort)madeByInfo; + this.versionToExtract = (ushort)versionRequiredToExtract; + this.method = method; + } + + /// + /// Creates a deep copy of the given zip entry. + /// + /// + /// The entry to copy. + /// + [Obsolete("Use Clone instead")] + public ZipEntry(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + known = entry.known; + name = entry.name; + size = entry.size; + compressedSize = entry.compressedSize; + crc = entry.crc; + dosTime = entry.dosTime; + method = entry.method; + comment = entry.comment; + versionToExtract = entry.versionToExtract; + versionMadeBy = entry.versionMadeBy; + externalFileAttributes = entry.externalFileAttributes; + flags = entry.flags; + + zipFileIndex = entry.zipFileIndex; + offset = entry.offset; + + forceZip64_ = entry.forceZip64_; + + if ( entry.extra != null ) { + extra = new byte[entry.extra.Length]; + Array.Copy(entry.extra, 0, extra, 0, entry.extra.Length); + } + } + + #endregion + + /// + /// Get a value indicating wether the entry has a CRC value available. + /// + public bool HasCrc + { + get { + return (known & Known.Crc) != 0; + } + } + + /// + /// Get/Set flag indicating if entry is encrypted. + /// A simple helper routine to aid interpretation of flags + /// + /// This is an assistant that interprets the flags property. + public bool IsCrypted + { + get { + return (flags & 1) != 0; + } + set { + if (value) { + flags |= 1; + } + else { + flags &= ~1; + } + } + } + + /// + /// Get / set a flag indicating wether entry name and comment text are + /// encoded in unicode UTF8. + /// + /// This is an assistant that interprets the flags property. + public bool IsUnicodeText + { + get { + return ( flags & (int)GeneralBitFlags.UnicodeText ) != 0; + } + set { + if ( value ) { + flags |= (int)GeneralBitFlags.UnicodeText; + } + else { + flags &= ~(int)GeneralBitFlags.UnicodeText; + } + } + } + + /// + /// Value used during password checking for PKZIP 2.0 / 'classic' encryption. + /// + internal byte CryptoCheckValue + { + get { + return cryptoCheckValue_; + } + + set { + cryptoCheckValue_ = value; + } + } + + /// + /// Get/Set general purpose bit flag for entry + /// + /// + /// General purpose bit flag
+ ///
+ /// Bit 0: If set, indicates the file is encrypted
+ /// Bit 1-2 Only used for compression type 6 Imploding, and 8, 9 deflating
+ /// Imploding:
+ /// Bit 1 if set indicates an 8K sliding dictionary was used. If clear a 4k dictionary was used
+ /// Bit 2 if set indicates 3 Shannon-Fanno trees were used to encode the sliding dictionary, 2 otherwise
+ ///
+ /// Deflating:
+ /// Bit 2 Bit 1
+ /// 0 0 Normal compression was used
+ /// 0 1 Maximum compression was used
+ /// 1 0 Fast compression was used
+ /// 1 1 Super fast compression was used
+ ///
+ /// Bit 3: If set, the fields crc-32, compressed size + /// and uncompressed size are were not able to be written during zip file creation + /// The correct values are held in a data descriptor immediately following the compressed data.
+ /// Bit 4: Reserved for use by PKZIP for enhanced deflating
+ /// Bit 5: If set indicates the file contains compressed patch data
+ /// Bit 6: If set indicates strong encryption was used.
+ /// Bit 7-10: Unused or reserved
+ /// Bit 11: If set the name and comments for this entry are in
unicode.
+ /// Bit 12-15: Unused or reserved
+ /// + /// + /// + public int Flags + { + get { + return flags; + } + set { + flags = value; + } + } + + /// + /// Get/Set index of this entry in Zip file + /// + /// This is only valid when the entry is part of a + public long ZipFileIndex + { + get { + return zipFileIndex; + } + set { + zipFileIndex = value; + } + } + + /// + /// Get/set offset for use in central header + /// + public long Offset + { + get { + return offset; + } + set { + offset = value; + } + } + + /// + /// Get/Set external file attributes as an integer. + /// The values of this are operating system dependant see + /// HostSystem for details + /// + public int ExternalFileAttributes + { + get { + if ((known & Known.ExternalAttributes) == 0) { + return -1; + } + else { + return externalFileAttributes; + } + } + + set { + externalFileAttributes = value; + known |= Known.ExternalAttributes; + } + } + + /// + /// Get the version made by for this entry or zero if unknown. + /// The value / 10 indicates the major version number, and + /// the value mod 10 is the minor version number + /// + public int VersionMadeBy + { + get { + return (versionMadeBy & 0xff); + } + } + + /// + /// Get a value indicating this entry is for a DOS/Windows system. + /// + public bool IsDOSEntry + { + get { + return ((HostSystem == ( int )HostSystemID.Msdos) || + (HostSystem == ( int )HostSystemID.WindowsNT)); + } + } + + /// + /// Test the external attributes for this to + /// see if the external attributes are Dos based (including WINNT and variants) + /// and match the values + /// + /// The attributes to test. + /// Returns true if the external attributes are known to be DOS/Windows + /// based and have the same attributes set as the value passed. + bool HasDosAttributes(int attributes) + { + bool result = false; + if ( (known & Known.ExternalAttributes) != 0 ) { + if ( ((HostSystem == (int)HostSystemID.Msdos) || + (HostSystem == (int)HostSystemID.WindowsNT)) && + (ExternalFileAttributes & attributes) == attributes) { + result = true; + } + } + return result; + } + + /// + /// Gets the compatability information for the external file attribute + /// If the external file attributes are compatible with MS-DOS and can be read + /// by PKZIP for DOS version 2.04g then this value will be zero. Otherwise the value + /// will be non-zero and identify the host system on which the attributes are compatible. + /// + /// + /// + /// The values for this as defined in the Zip File format and by others are shown below. The values are somewhat + /// misleading in some cases as they are not all used as shown. You should consult the relevant documentation + /// to obtain up to date and correct information. The modified appnote by the infozip group is + /// particularly helpful as it documents a lot of peculiarities. The document is however a little dated. + /// + /// 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) + /// 1 - Amiga + /// 2 - OpenVMS + /// 3 - Unix + /// 4 - VM/CMS + /// 5 - Atari ST + /// 6 - OS/2 HPFS + /// 7 - Macintosh + /// 8 - Z-System + /// 9 - CP/M + /// 10 - Windows NTFS + /// 11 - MVS (OS/390 - Z/OS) + /// 12 - VSE + /// 13 - Acorn Risc + /// 14 - VFAT + /// 15 - Alternate MVS + /// 16 - BeOS + /// 17 - Tandem + /// 18 - OS/400 + /// 19 - OS/X (Darwin) + /// 99 - WinZip AES + /// remainder - unused + /// + /// + public int HostSystem + { + get { + return (versionMadeBy >> 8) & 0xff; + } + + set { + versionMadeBy &= 0xff; + versionMadeBy |= (ushort)((value & 0xff) << 8); + } + } + + /// + /// Get minimum Zip feature version required to extract this entry + /// + /// + /// Minimum features are defined as:
+ /// 1.0 - Default value
+ /// 1.1 - File is a volume label
+ /// 2.0 - File is a folder/directory
+ /// 2.0 - File is compressed using Deflate compression
+ /// 2.0 - File is encrypted using traditional encryption
+ /// 2.1 - File is compressed using Deflate64
+ /// 2.5 - File is compressed using PKWARE DCL Implode
+ /// 2.7 - File is a patch data set
+ /// 4.5 - File uses Zip64 format extensions
+ /// 4.6 - File is compressed using BZIP2 compression
+ /// 5.0 - File is encrypted using DES
+ /// 5.0 - File is encrypted using 3DES
+ /// 5.0 - File is encrypted using original RC2 encryption
+ /// 5.0 - File is encrypted using RC4 encryption
+ /// 5.1 - File is encrypted using AES encryption
+ /// 5.1 - File is encrypted using corrected RC2 encryption
+ /// 5.1 - File is encrypted using corrected RC2-64 encryption
+ /// 6.1 - File is encrypted using non-OAEP key wrapping
+ /// 6.2 - Central directory encryption (not confirmed yet)
+ /// 6.3 - File is compressed using LZMA
+ /// 6.3 - File is compressed using PPMD+
+ /// 6.3 - File is encrypted using Blowfish
+ /// 6.3 - File is encrypted using Twofish
+ ///
+ /// + public int Version + { + get { + // Return recorded version if known. + if (versionToExtract != 0) { + return versionToExtract; + } + else { + int result = 10; + if (AESKeySize > 0) { + result = ZipConstants.VERSION_AES; // Ver 5.1 = AES + } + else if (CentralHeaderRequiresZip64) { + result = ZipConstants.VersionZip64; + } + else if (CompressionMethod.Deflated == method) { + result = 20; + } + else if (IsDirectory == true) { + result = 20; + } + else if (IsCrypted == true) { + result = 20; + } + else if (HasDosAttributes(0x08) ) { + result = 11; + } + return result; + } + } + } + + /// + /// Get a value indicating whether this entry can be decompressed by the library. + /// + /// This is based on the and + /// wether the compression method is supported. + public bool CanDecompress + { + get { + return (Version <= ZipConstants.VersionMadeBy) && + ((Version == 10) || + (Version == 11) || + (Version == 20) || + (Version == 45) || + (Version == 51)) && + IsCompressionMethodSupported(); + } + } + + /// + /// Force this entry to be recorded using Zip64 extensions. + /// + public void ForceZip64() + { + forceZip64_ = true; + } + + /// + /// Get a value indicating wether Zip64 extensions were forced. + /// + /// A value of true if Zip64 extensions have been forced on; false if not. + public bool IsZip64Forced() + { + return forceZip64_; + } + + /// + /// Gets a value indicating if the entry requires Zip64 extensions + /// to store the full entry values. + /// + /// A value of true if a local header requires Zip64 extensions; false if not. + public bool LocalHeaderRequiresZip64 + { + get { + bool result = forceZip64_; + + if ( !result ) { + ulong trueCompressedSize = compressedSize; + + if ( (versionToExtract == 0) && IsCrypted ) { + trueCompressedSize += ZipConstants.CryptoHeaderSize; + } + + // TODO: A better estimation of the true limit based on compression overhead should be used + // to determine when an entry should use Zip64. + result = + ((this.size >= uint.MaxValue) || (trueCompressedSize >= uint.MaxValue)) && + ((versionToExtract == 0) || (versionToExtract >= ZipConstants.VersionZip64)); + } + + return result; + } + } + + /// + /// Get a value indicating wether the central directory entry requires Zip64 extensions to be stored. + /// + public bool CentralHeaderRequiresZip64 + { + get { + return LocalHeaderRequiresZip64 || (offset >= uint.MaxValue); + } + } + + /// + /// Get/Set DosTime value. + /// + /// + /// The MS-DOS date format can only represent dates between 1/1/1980 and 12/31/2107. + /// + public long DosTime + { + get { + if ((known & Known.Time) == 0) { + return 0; + } + else { + return dosTime; + } + } + + set { + unchecked { + dosTime = (uint)value; + } + + known |= Known.Time; + } + } + + /// + /// Gets/Sets the time of last modification of the entry. + /// + /// + /// The property is updated to match this as far as possible. + /// + public DateTime DateTime + { + get { + uint sec = Math.Min(59, 2 * (dosTime & 0x1f)); + uint min = Math.Min(59, (dosTime >> 5) & 0x3f); + uint hrs = Math.Min(23, (dosTime >> 11) & 0x1f); + uint mon = Math.Max(1, Math.Min(12, ((dosTime >> 21) & 0xf))); + uint year = ((dosTime >> 25) & 0x7f) + 1980; + int day = Math.Max(1, Math.Min(DateTime.DaysInMonth((int)year, (int)mon), (int)((dosTime >> 16) & 0x1f))); + return new System.DateTime((int)year, (int)mon, day, (int)hrs, (int)min, (int)sec); + } + + set { + uint year = (uint) value.Year; + uint month = (uint) value.Month; + uint day = (uint) value.Day; + uint hour = (uint) value.Hour; + uint minute = (uint) value.Minute; + uint second = (uint) value.Second; + + if ( year < 1980 ) { + year = 1980; + month = 1; + day = 1; + hour = 0; + minute = 0; + second = 0; + } + else if ( year > 2107 ) { + year = 2107; + month = 12; + day = 31; + hour = 23; + minute = 59; + second = 59; + } + + DosTime = ((year - 1980) & 0x7f) << 25 | + (month << 21) | + (day << 16) | + (hour << 11) | + (minute << 5) | + (second >> 1); + } + } + + /// + /// Returns the entry name. + /// + /// + /// The unix naming convention is followed. + /// Path components in the entry should always separated by forward slashes ('/'). + /// Dos device names like C: should also be removed. + /// See the class, or + /// + public string Name + { + get { + return name; + } + } + + /// + /// Gets/Sets the size of the uncompressed data. + /// + /// + /// The size or -1 if unknown. + /// + /// Setting the size before adding an entry to an archive can help + /// avoid compatability problems with some archivers which dont understand Zip64 extensions. + public long Size + { + get { + return (known & Known.Size) != 0 ? (long)size : -1L; + } + set { + this.size = (ulong)value; + this.known |= Known.Size; + } + } + + /// + /// Gets/Sets the size of the compressed data. + /// + /// + /// The compressed entry size or -1 if unknown. + /// + public long CompressedSize + { + get { + return (known & Known.CompressedSize) != 0 ? (long)compressedSize : -1L; + } + set { + this.compressedSize = (ulong)value; + this.known |= Known.CompressedSize; + } + } + + /// + /// Gets/Sets the crc of the uncompressed data. + /// + /// + /// Crc is not in the range 0..0xffffffffL + /// + /// + /// The crc value or -1 if unknown. + /// + public long Crc + { + get { + return (known & Known.Crc) != 0 ? crc & 0xffffffffL : -1L; + } + set { + if (((ulong)crc & 0xffffffff00000000L) != 0) { + throw new ArgumentOutOfRangeException("value"); + } + this.crc = (uint)value; + this.known |= Known.Crc; + } + } + + /// + /// Gets/Sets the compression method. Only Deflated and Stored are supported. + /// + /// + /// The compression method for this entry + /// + /// + /// + public CompressionMethod CompressionMethod { + get { + return method; + } + + set { + if ( !IsCompressionMethodSupported(value) ) { + throw new NotSupportedException("Compression method not supported"); + } + this.method = value; + } + } + + /// + /// Gets the compression method for outputting to the local or central header. + /// Returns same value as CompressionMethod except when AES encrypting, which + /// places 99 in the method and places the real method in the extra data. + /// + internal CompressionMethod CompressionMethodForHeader { + get { + return (AESKeySize > 0) ? CompressionMethod.WinZipAES : method; + } + } + + /// + /// Gets/Sets the extra data. + /// + /// + /// Extra data is longer than 64KB (0xffff) bytes. + /// + /// + /// Extra data or null if not set. + /// + public byte[] ExtraData { + + get { +// TODO: This is slightly safer but less efficient. Think about wether it should change. +// return (byte[]) extra.Clone(); + return extra; + } + + set { + if (value == null) { + extra = null; + } + else { + if (value.Length > 0xffff) { + throw new System.ArgumentOutOfRangeException("value"); + } + + extra = new byte[value.Length]; + Array.Copy(value, 0, extra, 0, value.Length); + } + } + } + + +#if !NET_1_1 && !NETCF_2_0 + /// + /// For AES encrypted files returns or sets the number of bits of encryption (128, 192 or 256). + /// When setting, only 0 (off), 128 or 256 is supported. + /// + public int AESKeySize { + get { + // the strength (1 or 3) is in the entry header + switch (_aesEncryptionStrength) { + case 0: return 0; // Not AES + case 1: return 128; + case 2: return 192; // Not used by WinZip + case 3: return 256; + default: throw new ZipException("Invalid AESEncryptionStrength " + _aesEncryptionStrength); + } + } + set { + switch (value) { + case 0: _aesEncryptionStrength = 0; break; + case 128: _aesEncryptionStrength = 1; break; + case 256: _aesEncryptionStrength = 3; break; + default: throw new ZipException("AESKeySize must be 0, 128 or 256: " + value); + } + } + } + + /// + /// AES Encryption strength for storage in extra data in entry header. + /// 1 is 128 bit, 2 is 192 bit, 3 is 256 bit. + /// + internal byte AESEncryptionStrength { + get { + return (byte)_aesEncryptionStrength; + } + } +#else + /// + /// AES unsupported prior to .NET 2.0 + /// + internal int AESKeySize; +#endif + + /// + /// Returns the length of the salt, in bytes + /// + internal int AESSaltLen { + get { + // Key size -> Salt length: 128 bits = 8 bytes, 192 bits = 12 bytes, 256 bits = 16 bytes. + return AESKeySize / 16; + } + } + + /// + /// Number of extra bytes required to hold the AES Header fields (Salt, Pwd verify, AuthCode) + /// + internal int AESOverheadSize { + get { + // File format: + // Bytes Content + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + return 12 + AESSaltLen; + } + } + + /// + /// Process extra data fields updating the entry based on the contents. + /// + /// True if the extra data fields should be handled + /// for a local header, rather than for a central header. + /// + internal void ProcessExtraData(bool localHeader) + { + ZipExtraData extraData = new ZipExtraData(this.extra); + + if ( extraData.Find(0x0001) ) { + // Version required to extract is ignored here as some archivers dont set it correctly + // in theory it should be version 45 or higher + + // The recorded size will change but remember that this is zip64. + forceZip64_ = true; + + if ( extraData.ValueLength < 4 ) { + throw new ZipException("Extra data extended Zip64 information length is invalid"); + } + + if ( localHeader || (size == uint.MaxValue) ) { + size = (ulong)extraData.ReadLong(); + } + + if ( localHeader || (compressedSize == uint.MaxValue) ) { + compressedSize = (ulong)extraData.ReadLong(); + } + + if ( !localHeader && (offset == uint.MaxValue) ) { + offset = extraData.ReadLong(); + } + + // Disk number on which file starts is ignored + } + else { + if ( + ((versionToExtract & 0xff) >= ZipConstants.VersionZip64) && + ((size == uint.MaxValue) || (compressedSize == uint.MaxValue)) + ) { + throw new ZipException("Zip64 Extended information required but is missing."); + } + } + + if ( extraData.Find(10) ) { + // No room for any tags. + if ( extraData.ValueLength < 4 ) { + throw new ZipException("NTFS Extra data invalid"); + } + + extraData.ReadInt(); // Reserved + + while ( extraData.UnreadCount >= 4 ) { + int ntfsTag = extraData.ReadShort(); + int ntfsLength = extraData.ReadShort(); + if ( ntfsTag == 1 ) { + if ( ntfsLength >= 24 ) { + long lastModification = extraData.ReadLong(); + long lastAccess = extraData.ReadLong(); + long createTime = extraData.ReadLong(); + + DateTime = System.DateTime.FromFileTime(lastModification); + } + break; + } + else { + // An unknown NTFS tag so simply skip it. + extraData.Skip(ntfsLength); + } + } + } + else if ( extraData.Find(0x5455) ) { + int length = extraData.ValueLength; + int flags = extraData.ReadByte(); + + // Can include other times but these are ignored. Length of data should + // actually be 1 + 4 * no of bits in flags. + if ( ((flags & 1) != 0) && (length >= 5) ) { + int iTime = extraData.ReadInt(); + + DateTime = (new System.DateTime ( 1970, 1, 1, 0, 0, 0 ).ToUniversalTime() + + new TimeSpan ( 0, 0, 0, iTime, 0 )).ToLocalTime(); + } + } + if (method == CompressionMethod.WinZipAES) { + ProcessAESExtraData(extraData); + } + } + + // For AES the method in the entry is 99, and the real compression method is in the extradata + // + private void ProcessAESExtraData(ZipExtraData extraData) { + +#if !NET_1_1 && !NETCF_2_0 + if (extraData.Find(0x9901)) { + // Set version and flag for Zipfile.CreateAndInitDecryptionStream + versionToExtract = ZipConstants.VERSION_AES; // Ver 5.1 = AES see "Version" getter + // Set StrongEncryption flag for ZipFile.CreateAndInitDecryptionStream + Flags = Flags | (int)GeneralBitFlags.StrongEncryption; + // + // Unpack AES extra data field see http://www.winzip.com/aes_info.htm + int length = extraData.ValueLength; // Data size currently 7 + if (length < 7) + throw new ZipException("AES Extra Data Length " + length + " invalid."); + int ver = extraData.ReadShort(); // Version number (1=AE-1 2=AE-2) + int vendorId = extraData.ReadShort(); // 2-character vendor ID 0x4541 = "AE" + int encrStrength = extraData.ReadByte(); // encryption strength 1 = 128 2 = 192 3 = 256 + int actualCompress = extraData.ReadShort(); // The actual compression method used to compress the file + _aesVer = ver; + _aesEncryptionStrength = encrStrength; + method = (CompressionMethod)actualCompress; + } else + throw new ZipException("AES Extra Data missing"); +#else + throw new ZipException("AES unsupported"); +#endif + } + + /// + /// Gets/Sets the entry comment. + /// + /// + /// If comment is longer than 0xffff. + /// + /// + /// The comment or null if not set. + /// + /// + /// A comment is only available for entries when read via the class. + /// The class doesnt have the comment data available. + /// + public string Comment { + get { + return comment; + } + set { + // This test is strictly incorrect as the length is in characters + // while the storage limit is in bytes. + // While the test is partially correct in that a comment of this length or greater + // is definitely invalid, shorter comments may also have an invalid length + // where there are multi-byte characters + // The full test is not possible here however as the code page to apply conversions with + // isnt available. + if ( (value != null) && (value.Length > 0xffff) ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "cannot exceed 65535"); +#endif + } + + comment = value; + } + } + + /// + /// Gets a value indicating if the entry is a directory. + /// however. + /// + /// + /// A directory is determined by an entry name with a trailing slash '/'. + /// The external file attributes can also indicate an entry is for a directory. + /// Currently only dos/windows attributes are tested in this manner. + /// The trailing slash convention should always be followed. + /// + public bool IsDirectory + { + get { + int nameLength = name.Length; + bool result = + ((nameLength > 0) && + ((name[nameLength - 1] == '/') || (name[nameLength - 1] == '\\'))) || + HasDosAttributes(16) + ; + return result; + } + } + + /// + /// Get a value of true if the entry appears to be a file; false otherwise + /// + /// + /// This only takes account of DOS/Windows attributes. Other operating systems are ignored. + /// For linux and others the result may be incorrect. + /// + public bool IsFile + { + get { + return !IsDirectory && !HasDosAttributes(8); + } + } + + /// + /// Test entry to see if data can be extracted. + /// + /// Returns true if data can be extracted for this entry; false otherwise. + public bool IsCompressionMethodSupported() + { + return IsCompressionMethodSupported(CompressionMethod); + } + + #region ICloneable Members + /// + /// Creates a copy of this zip entry. + /// + /// An that is a copy of the current instance. + public object Clone() + { + ZipEntry result = (ZipEntry)this.MemberwiseClone(); + + // Ensure extra data is unique if it exists. + if ( extra != null ) { + result.extra = new byte[extra.Length]; + Array.Copy(extra, 0, result.extra, 0, extra.Length); + } + + return result; + } + + #endregion + + /// + /// Gets a string representation of this ZipEntry. + /// + /// A readable textual representation of this + public override string ToString() + { + return name; + } + + /// + /// Test a compression method to see if this library + /// supports extracting data compressed with that method + /// + /// The compression method to test. + /// Returns true if the compression method is supported; false otherwise + public static bool IsCompressionMethodSupported(CompressionMethod method) + { + return + ( method == CompressionMethod.Deflated ) || + ( method == CompressionMethod.Stored ); + } + + /// + /// Cleans a name making it conform to Zip file conventions. + /// Devices names ('c:\') and UNC share names ('\\server\share') are removed + /// and forward slashes ('\') are converted to back slashes ('/'). + /// Names are made relative by trimming leading slashes which is compatible + /// with the ZIP naming convention. + /// + /// The name to clean + /// The 'cleaned' name. + /// + /// The Zip name transform class is more flexible. + /// + public static string CleanName(string name) + { + if (name == null) { + return string.Empty; + } + + if (Path.IsPathRooted(name) == true) { + // NOTE: + // for UNC names... \\machine\share\zoom\beet.txt gives \zoom\beet.txt + name = name.Substring(Path.GetPathRoot(name).Length); + } + + name = name.Replace(@"\", "/"); + + while ( (name.Length > 0) && (name[0] == '/')) { + name = name.Remove(0, 1); + } + return name; + } + + #region Instance Fields + Known known; + int externalFileAttributes = -1; // contains external attributes (O/S dependant) + + ushort versionMadeBy; // Contains host system and version information + // only relevant for central header entries + + string name; + ulong size; + ulong compressedSize; + ushort versionToExtract; // Version required to extract (library handles <= 2.0) + uint crc; + uint dosTime; + + CompressionMethod method = CompressionMethod.Deflated; + byte[] extra; + string comment; + + int flags; // general purpose bit flags + + long zipFileIndex = -1; // used by ZipFile + long offset; // used by ZipFile and ZipOutputStream + + bool forceZip64_; + byte cryptoCheckValue_; +#if !NET_1_1 && !NETCF_2_0 + int _aesVer; // Version number (2 = AE-2 ?). Assigned but not used. + int _aesEncryptionStrength; // Encryption strength 1 = 128 2 = 192 3 = 256 +#endif + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs new file mode 100644 index 000000000..4e5e1c1e8 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipEntryFactory.cs @@ -0,0 +1,413 @@ +// ZipEntryFactory.cs +// +// Copyright 2006 John Reilly +// +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// Basic implementation of + /// + public class ZipEntryFactory : IEntryFactory + { + #region Enumerations + /// + /// Defines the possible values to be used for the . + /// + public enum TimeSetting + { + /// + /// Use the recorded LastWriteTime value for the file. + /// + LastWriteTime, + /// + /// Use the recorded LastWriteTimeUtc value for the file + /// + LastWriteTimeUtc, + /// + /// Use the recorded CreateTime value for the file. + /// + CreateTime, + /// + /// Use the recorded CreateTimeUtc value for the file. + /// + CreateTimeUtc, + /// + /// Use the recorded LastAccessTime value for the file. + /// + LastAccessTime, + /// + /// Use the recorded LastAccessTimeUtc value for the file. + /// + LastAccessTimeUtc, + /// + /// Use a fixed value. + /// + /// The actual value used can be + /// specified via the constructor or + /// using the with the setting set + /// to which will use the when this class was constructed. + /// The property can also be used to set this value. + Fixed, + } + #endregion + + #region Constructors + /// + /// Initialise a new instance of the class. + /// + /// A default , and the LastWriteTime for files is used. + public ZipEntryFactory() + { + nameTransform_ = new ZipNameTransform(); + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time setting to use when creating Zip entries. + public ZipEntryFactory(TimeSetting timeSetting) + { + timeSetting_ = timeSetting; + nameTransform_ = new ZipNameTransform(); + } + + /// + /// Initialise a new instance of using the specified + /// + /// The time to set all values to. + public ZipEntryFactory(DateTime time) + { + timeSetting_ = TimeSetting.Fixed; + FixedDateTime = time; + nameTransform_ = new ZipNameTransform(); + } + + #endregion + + #region Properties + /// + /// Get / set the to be used when creating new values. + /// + /// + /// Setting this property to null will cause a default name transform to be used. + /// + public INameTransform NameTransform + { + get { return nameTransform_; } + set + { + if (value == null) { + nameTransform_ = new ZipNameTransform(); + } + else { + nameTransform_ = value; + } + } + } + + /// + /// Get / set the in use. + /// + public TimeSetting Setting + { + get { return timeSetting_; } + set { timeSetting_ = value; } + } + + /// + /// Get / set the value to use when is set to + /// + public DateTime FixedDateTime + { + get { return fixedDateTime_; } + set + { + if (value.Year < 1970) { + throw new ArgumentException("Value is too old to be valid", "value"); + } + fixedDateTime_ = value; + } + } + + /// + /// A bitmask defining the attributes to be retrieved from the actual file. + /// + /// The default is to get all possible attributes from the actual file. + public int GetAttributes + { + get { return getAttributes_; } + set { getAttributes_ = value; } + } + + /// + /// A bitmask defining which attributes are to be set on. + /// + /// By default no attributes are set on. + public int SetAttributes + { + get { return setAttributes_; } + set { setAttributes_ = value; } + } + + /// + /// Get set a value indicating wether unidoce text should be set on. + /// + public bool IsUnicodeText + { + get { return isUnicodeText_; } + set { isUnicodeText_ = value; } + } + + #endregion + + #region IEntryFactory Members + + /// + /// Make a new for a file. + /// + /// The name of the file to create a new entry for. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName) + { + return MakeFileEntry(fileName, true); + } + + /// + /// Make a new from a name. + /// + /// The name of the file to create a new entry for. + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new based on the . + public ZipEntry MakeFileEntry(string fileName, bool useFileSystem) + { + ZipEntry result = new ZipEntry(nameTransform_.TransformFile(fileName)); + result.IsUnicodeText = isUnicodeText_; + + int externalAttributes = 0; + bool useAttributes = (setAttributes_ != 0); + + FileInfo fi = null; + if (useFileSystem) + { + fi = new FileInfo(fileName); + } + + if ((fi != null) && fi.Exists) + { + switch (timeSetting_) + { + case TimeSetting.CreateTime: + result.DateTime = fi.CreationTime; + break; + + case TimeSetting.CreateTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = fi.CreationTime.ToUniversalTime(); +#else + result.DateTime = fi.CreationTimeUtc; +#endif + break; + + case TimeSetting.LastAccessTime: + result.DateTime = fi.LastAccessTime; + break; + + case TimeSetting.LastAccessTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = fi.LastAccessTime.ToUniversalTime(); +#else + result.DateTime = fi.LastAccessTimeUtc; +#endif + break; + + case TimeSetting.LastWriteTime: + result.DateTime = fi.LastWriteTime; + break; + + case TimeSetting.LastWriteTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = fi.LastWriteTime.ToUniversalTime(); +#else + result.DateTime = fi.LastWriteTimeUtc; +#endif + break; + + case TimeSetting.Fixed: + result.DateTime = fixedDateTime_; + break; + + default: + throw new ZipException("Unhandled time setting in MakeFileEntry"); + } + + result.Size = fi.Length; + + useAttributes = true; + externalAttributes = ((int)fi.Attributes & getAttributes_); + } + else + { + if (timeSetting_ == TimeSetting.Fixed) + { + result.DateTime = fixedDateTime_; + } + } + + if (useAttributes) + { + externalAttributes |= setAttributes_; + result.ExternalFileAttributes = externalAttributes; + } + + return result; + } + + /// + /// Make a new for a directory. + /// + /// The raw untransformed name for the new directory + /// Returns a new representing a directory. + public ZipEntry MakeDirectoryEntry(string directoryName) + { + return MakeDirectoryEntry(directoryName, true); + } + + /// + /// Make a new for a directory. + /// + /// The raw untransformed name for the new directory + /// If true entry detail is retrieved from the file system if the file exists. + /// Returns a new representing a directory. + public ZipEntry MakeDirectoryEntry(string directoryName, bool useFileSystem) + { + + ZipEntry result = new ZipEntry(nameTransform_.TransformDirectory(directoryName)); + result.IsUnicodeText = isUnicodeText_; + result.Size = 0; + + int externalAttributes = 0; + + DirectoryInfo di = null; + + if (useFileSystem) + { + di = new DirectoryInfo(directoryName); + } + + + if ((di != null) && di.Exists) + { + switch (timeSetting_) + { + case TimeSetting.CreateTime: + result.DateTime = di.CreationTime; + break; + + case TimeSetting.CreateTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = di.CreationTime.ToUniversalTime(); +#else + result.DateTime = di.CreationTimeUtc; +#endif + break; + + case TimeSetting.LastAccessTime: + result.DateTime = di.LastAccessTime; + break; + + case TimeSetting.LastAccessTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = di.LastAccessTime.ToUniversalTime(); +#else + result.DateTime = di.LastAccessTimeUtc; +#endif + break; + + case TimeSetting.LastWriteTime: + result.DateTime = di.LastWriteTime; + break; + + case TimeSetting.LastWriteTimeUtc: +#if NETCF_1_0 || NETCF_2_0 + result.DateTime = di.LastWriteTime.ToUniversalTime(); +#else + result.DateTime = di.LastWriteTimeUtc; +#endif + break; + + case TimeSetting.Fixed: + result.DateTime = fixedDateTime_; + break; + + default: + throw new ZipException("Unhandled time setting in MakeDirectoryEntry"); + } + + externalAttributes = ((int)di.Attributes & getAttributes_); + } + else + { + if (timeSetting_ == TimeSetting.Fixed) + { + result.DateTime = fixedDateTime_; + } + } + + // Always set directory attribute on. + externalAttributes |= (setAttributes_ | 16); + result.ExternalFileAttributes = externalAttributes; + + return result; + } + + #endregion + + #region Instance Fields + INameTransform nameTransform_; + DateTime fixedDateTime_ = DateTime.Now; + TimeSetting timeSetting_; + bool isUnicodeText_; + + int getAttributes_ = -1; + int setAttributes_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipException.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipException.cs new file mode 100644 index 000000000..7ceb4a063 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipException.cs @@ -0,0 +1,94 @@ +// ZipException.cs +// +// Copyright (C) 2001 Mike Krueger +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 1998, 1999, 2000, 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; + +#if !NETCF_1_0 && !NETCF_2_0 +using System.Runtime.Serialization; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + /// + /// Represents exception conditions specific to Zip archive handling + /// +#if !NETCF_1_0 && !NETCF_2_0 + [Serializable] +#endif + public class ZipException : SharpZipBaseException + { +#if !NETCF_1_0 && !NETCF_2_0 + /// + /// Deserialization constructor + /// + /// for this constructor + /// for this constructor + protected ZipException(SerializationInfo info, StreamingContext context ) + : base( info, context ) + { + } +#endif + + /// + /// Initializes a new instance of the ZipException class. + /// + public ZipException() + { + } + + /// + /// Initializes a new instance of the ZipException class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public ZipException(string message) + : base(message) + { + } + + /// + /// Initialise a new instance of ZipException. + /// + /// A message describing the error. + /// The exception that is the cause of the current exception. + public ZipException(string message, Exception exception) + : base(message, exception) + { + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs new file mode 100644 index 000000000..533ba9c33 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipExtraData.cs @@ -0,0 +1,987 @@ +// +// ZipExtraData.cs +// +// Copyright 2004-2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + // TODO: Sort out wether tagged data is useful and what a good implementation might look like. + // Its just a sketch of an idea at the moment. + + /// + /// ExtraData tagged value interface. + /// + public interface ITaggedData + { + /// + /// Get the ID for this tagged data value. + /// + short TagID { get; } + + /// + /// Set the contents of this instance from the data passed. + /// + /// The data to extract contents from. + /// The offset to begin extracting data from. + /// The number of bytes to extract. + void SetData(byte[] data, int offset, int count); + + /// + /// Get the data representing this instance. + /// + /// Returns the data for this instance. + byte[] GetData(); + } + + /// + /// A raw binary tagged value + /// + public class RawTaggedData : ITaggedData + { + /// + /// Initialise a new instance. + /// + /// The tag ID. + public RawTaggedData(short tag) + { + _tag = tag; + } + + #region ITaggedData Members + + /// + /// Get the ID for this tagged data value. + /// + public short TagID + { + get { return _tag; } + set { _tag = value; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int offset, int count) + { + if( data==null ) + { + throw new ArgumentNullException("data"); + } + + _data=new byte[count]; + Array.Copy(data, offset, _data, 0, count); + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + return _data; + } + + #endregion + + /// + /// Get /set the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] Data + { + get { return _data; } + set { _data=value; } + } + + #region Instance Fields + /// + /// The tag ID for this instance. + /// + short _tag; + + byte[] _data; + #endregion + } + + /// + /// Class representing extended unix date time values. + /// + public class ExtendedUnixData : ITaggedData + { + /// + /// Flags indicate which values are included in this instance. + /// + [Flags] + public enum Flags : byte + { + /// + /// The modification time is included + /// + ModificationTime = 0x01, + + /// + /// The access time is included + /// + AccessTime = 0x02, + + /// + /// The create time is included. + /// + CreateTime = 0x04, + } + + #region ITaggedData Members + + /// + /// Get the ID + /// + public short TagID + { + get { return 0x5455; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int index, int count) + { + using (MemoryStream ms = new MemoryStream(data, index, count, false)) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + // bit 0 if set, modification time is present + // bit 1 if set, access time is present + // bit 2 if set, creation time is present + + _flags = (Flags)helperStream.ReadByte(); + if (((_flags & Flags.ModificationTime) != 0) && (count >= 5)) + { + int iTime = helperStream.ReadLEInt(); + + _modificationTime = (new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime() + + new TimeSpan(0, 0, 0, iTime, 0)).ToLocalTime(); + } + + if ((_flags & Flags.AccessTime) != 0) + { + int iTime = helperStream.ReadLEInt(); + + _lastAccessTime = (new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime() + + new TimeSpan(0, 0, 0, iTime, 0)).ToLocalTime(); + } + + if ((_flags & Flags.CreateTime) != 0) + { + int iTime = helperStream.ReadLEInt(); + + _createTime = (new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime() + + new TimeSpan(0, 0, 0, iTime, 0)).ToLocalTime(); + } + } + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + using (MemoryStream ms = new MemoryStream()) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + helperStream.IsStreamOwner = false; + helperStream.WriteByte((byte)_flags); // Flags + if ( (_flags & Flags.ModificationTime) != 0) { + TimeSpan span = _modificationTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime(); + int seconds = (int)span.TotalSeconds; + helperStream.WriteLEInt(seconds); + } + if ( (_flags & Flags.AccessTime) != 0) { + TimeSpan span = _lastAccessTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime(); + int seconds = (int)span.TotalSeconds; + helperStream.WriteLEInt(seconds); + } + if ( (_flags & Flags.CreateTime) != 0) { + TimeSpan span = _createTime.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0).ToUniversalTime(); + int seconds = (int)span.TotalSeconds; + helperStream.WriteLEInt(seconds); + } + return ms.ToArray(); + } + } + + #endregion + + /// + /// Test a value to see if is valid and can be represented here. + /// + /// The value to test. + /// Returns true if the value is valid and can be represented; false if not. + /// The standard Unix time is a signed integer data type, directly encoding the Unix time number, + /// which is the number of seconds since 1970-01-01. + /// Being 32 bits means the values here cover a range of about 136 years. + /// The minimum representable time is 1901-12-13 20:45:52, + /// and the maximum representable time is 2038-01-19 03:14:07. + /// + public static bool IsValidValue(DateTime value) + { + return (( value >= new DateTime(1901, 12, 13, 20, 45, 52)) || + ( value <= new DateTime(2038, 1, 19, 03, 14, 07) )); + } + + /// + /// Get /set the Modification Time + /// + /// + /// + public DateTime ModificationTime + { + get { return _modificationTime; } + set + { + if ( !IsValidValue(value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + _flags |= Flags.ModificationTime; + _modificationTime=value; + } + } + + /// + /// Get / set the Access Time + /// + /// + /// + public DateTime AccessTime + { + get { return _lastAccessTime; } + set { + if ( !IsValidValue(value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + _flags |= Flags.AccessTime; + _lastAccessTime=value; + } + } + + /// + /// Get / Set the Create Time + /// + /// + /// + public DateTime CreateTime + { + get { return _createTime; } + set { + if ( !IsValidValue(value) ) { + throw new ArgumentOutOfRangeException("value"); + } + + _flags |= Flags.CreateTime; + _createTime=value; + } + } + + /// + /// Get/set the values to include. + /// + Flags Include + { + get { return _flags; } + set { _flags = value; } + } + + #region Instance Fields + Flags _flags; + DateTime _modificationTime = new DateTime(1970,1,1); + DateTime _lastAccessTime = new DateTime(1970, 1, 1); + DateTime _createTime = new DateTime(1970, 1, 1); + #endregion + } + + /// + /// Class handling NT date time values. + /// + public class NTTaggedData : ITaggedData + { + /// + /// Get the ID for this tagged data value. + /// + public short TagID + { + get { return 10; } + } + + /// + /// Set the data from the raw values provided. + /// + /// The raw data to extract values from. + /// The index to start extracting values from. + /// The number of bytes available. + public void SetData(byte[] data, int index, int count) + { + using (MemoryStream ms = new MemoryStream(data, index, count, false)) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + helperStream.ReadLEInt(); // Reserved + while (helperStream.Position < helperStream.Length) + { + int ntfsTag = helperStream.ReadLEShort(); + int ntfsLength = helperStream.ReadLEShort(); + if (ntfsTag == 1) + { + if (ntfsLength >= 24) + { + long lastModificationTicks = helperStream.ReadLELong(); + _lastModificationTime = DateTime.FromFileTime(lastModificationTicks); + + long lastAccessTicks = helperStream.ReadLELong(); + _lastAccessTime = DateTime.FromFileTime(lastAccessTicks); + + long createTimeTicks = helperStream.ReadLELong(); + _createTime = DateTime.FromFileTime(createTimeTicks); + } + break; + } + else + { + // An unknown NTFS tag so simply skip it. + helperStream.Seek(ntfsLength, SeekOrigin.Current); + } + } + } + } + + /// + /// Get the binary data representing this instance. + /// + /// The raw binary data representing this instance. + public byte[] GetData() + { + using (MemoryStream ms = new MemoryStream()) + using (ZipHelperStream helperStream = new ZipHelperStream(ms)) + { + helperStream.IsStreamOwner = false; + helperStream.WriteLEInt(0); // Reserved + helperStream.WriteLEShort(1); // Tag + helperStream.WriteLEShort(24); // Length = 3 x 8. + helperStream.WriteLELong(_lastModificationTime.ToFileTime()); + helperStream.WriteLELong(_lastAccessTime.ToFileTime()); + helperStream.WriteLELong(_createTime.ToFileTime()); + return ms.ToArray(); + } + } + + /// + /// Test a valuie to see if is valid and can be represented here. + /// + /// The value to test. + /// Returns true if the value is valid and can be represented; false if not. + /// + /// NTFS filetimes are 64-bit unsigned integers, stored in Intel + /// (least significant byte first) byte order. They determine the + /// number of 1.0E-07 seconds (1/10th microseconds!) past WinNT "epoch", + /// which is "01-Jan-1601 00:00:00 UTC". 28 May 60056 is the upper limit + /// + public static bool IsValidValue(DateTime value) + { + bool result = true; + try + { + value.ToFileTimeUtc(); + } + catch + { + result = false; + } + return result; + } + + /// + /// Get/set the last modification time. + /// + public DateTime LastModificationTime + { + get { return _lastModificationTime; } + set { + if (! IsValidValue(value)) + { + throw new ArgumentOutOfRangeException("value"); + } + _lastModificationTime = value; + } + } + + /// + /// Get /set the create time + /// + public DateTime CreateTime + { + get { return _createTime; } + set { + if ( !IsValidValue(value)) { + throw new ArgumentOutOfRangeException("value"); + } + _createTime = value; + } + } + + /// + /// Get /set the last access time. + /// + public DateTime LastAccessTime + { + get { return _lastAccessTime; } + set { + if (!IsValidValue(value)) { + throw new ArgumentOutOfRangeException("value"); + } + _lastAccessTime = value; + } + } + + #region Instance Fields + DateTime _lastAccessTime = DateTime.FromFileTime(0); + DateTime _lastModificationTime = DateTime.FromFileTime(0); + DateTime _createTime = DateTime.FromFileTime(0); + #endregion + } + + /// + /// A factory that creates tagged data instances. + /// + interface ITaggedDataFactory + { + /// + /// Get data for a specific tag value. + /// + /// The tag ID to find. + /// The data to search. + /// The offset to begin extracting data from. + /// The number of bytes to extract. + /// The located value found, or null if not found. + ITaggedData Create(short tag, byte[] data, int offset, int count); + } + + /// + /// + /// A class to handle the extra data field for Zip entries + /// + /// + /// Extra data contains 0 or more values each prefixed by a header tag and length. + /// They contain zero or more bytes of actual data. + /// The data is held internally using a copy on write strategy. This is more efficient but + /// means that for extra data created by passing in data can have the values modified by the caller + /// in some circumstances. + /// + sealed public class ZipExtraData : IDisposable + { + #region Constructors + /// + /// Initialise a default instance. + /// + public ZipExtraData() + { + Clear(); + } + + /// + /// Initialise with known extra data. + /// + /// The extra data. + public ZipExtraData(byte[] data) + { + if ( data == null ) + { + _data = new byte[0]; + } + else + { + _data = data; + } + } + #endregion + + /// + /// Get the raw extra data value + /// + /// Returns the raw byte[] extra data this instance represents. + public byte[] GetEntryData() + { + if ( Length > ushort.MaxValue ) { + throw new ZipException("Data exceeds maximum length"); + } + + return (byte[])_data.Clone(); + } + + /// + /// Clear the stored data. + /// + public void Clear() + { + if ( (_data == null) || (_data.Length != 0) ) { + _data = new byte[0]; + } + } + + /// + /// Gets the current extra data length. + /// + public int Length + { + get { return _data.Length; } + } + + /// + /// Get a read-only for the associated tag. + /// + /// The tag to locate data for. + /// Returns a containing tag data or null if no tag was found. + public Stream GetStreamForTag(int tag) + { + Stream result = null; + if ( Find(tag) ) { + result = new MemoryStream(_data, _index, _readValueLength, false); + } + return result; + } + + /// + /// Get the tagged data for a tag. + /// + /// The tag to search for. + /// Returns a tagged value or null if none found. + private ITaggedData GetData(short tag) + { + ITaggedData result = null; + if (Find(tag)) + { + result = Create(tag, _data, _readValueStart, _readValueLength); + } + return result; + } + + static ITaggedData Create(short tag, byte[] data, int offset, int count) + { + ITaggedData result = null; + switch ( tag ) + { + case 0x000A: + result = new NTTaggedData(); + break; + case 0x5455: + result = new ExtendedUnixData(); + break; + default: + result = new RawTaggedData(tag); + break; + } + result.SetData(data, offset, count); + return result; + } + + /// + /// Get the length of the last value found by + /// + /// This is only valid if has previously returned true. + public int ValueLength + { + get { return _readValueLength; } + } + + /// + /// Get the index for the current read value. + /// + /// This is only valid if has previously returned true. + /// Initially the result will be the index of the first byte of actual data. The value is updated after calls to + /// , and . + public int CurrentReadIndex + { + get { return _index; } + } + + /// + /// Get the number of bytes remaining to be read for the current value; + /// + public int UnreadCount + { + get + { + if ((_readValueStart > _data.Length) || + (_readValueStart < 4) ) { + throw new ZipException("Find must be called before calling a Read method"); + } + + return _readValueStart + _readValueLength - _index; + } + } + + /// + /// Find an extra data value + /// + /// The identifier for the value to find. + /// Returns true if the value was found; false otherwise. + public bool Find(int headerID) + { + _readValueStart = _data.Length; + _readValueLength = 0; + _index = 0; + + int localLength = _readValueStart; + int localTag = headerID - 1; + + // Trailing bytes that cant make up an entry (as there arent enough + // bytes for a tag and length) are ignored! + while ( (localTag != headerID) && (_index < _data.Length - 3) ) { + localTag = ReadShortInternal(); + localLength = ReadShortInternal(); + if ( localTag != headerID ) { + _index += localLength; + } + } + + bool result = (localTag == headerID) && ((_index + localLength) <= _data.Length); + + if ( result ) { + _readValueStart = _index; + _readValueLength = localLength; + } + + return result; + } + + /// + /// Add a new entry to extra data. + /// + /// The value to add. + public void AddEntry(ITaggedData taggedData) + { + if (taggedData == null) + { + throw new ArgumentNullException("taggedData"); + } + AddEntry(taggedData.TagID, taggedData.GetData()); + } + + /// + /// Add a new entry to extra data + /// + /// The ID for this entry. + /// The data to add. + /// If the ID already exists its contents are replaced. + public void AddEntry(int headerID, byte[] fieldData) + { + if ( (headerID > ushort.MaxValue) || (headerID < 0)) { + throw new ArgumentOutOfRangeException("headerID"); + } + + int addLength = (fieldData == null) ? 0 : fieldData.Length; + + if ( addLength > ushort.MaxValue ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("fieldData"); +#else + throw new ArgumentOutOfRangeException("fieldData", "exceeds maximum length"); +#endif + } + + // Test for new length before adjusting data. + int newLength = _data.Length + addLength + 4; + + if ( Find(headerID) ) + { + newLength -= (ValueLength + 4); + } + + if ( newLength > ushort.MaxValue ) { + throw new ZipException("Data exceeds maximum length"); + } + + Delete(headerID); + + byte[] newData = new byte[newLength]; + _data.CopyTo(newData, 0); + int index = _data.Length; + _data = newData; + SetShort(ref index, headerID); + SetShort(ref index, addLength); + if ( fieldData != null ) { + fieldData.CopyTo(newData, index); + } + } + + /// + /// Start adding a new entry. + /// + /// Add data using , , , or . + /// The new entry is completed and actually added by calling + /// + public void StartNewEntry() + { + _newEntry = new MemoryStream(); + } + + /// + /// Add entry data added since using the ID passed. + /// + /// The identifier to use for this entry. + public void AddNewEntry(int headerID) + { + byte[] newData = _newEntry.ToArray(); + _newEntry = null; + AddEntry(headerID, newData); + } + + /// + /// Add a byte of data to the pending new entry. + /// + /// The byte to add. + /// + public void AddData(byte data) + { + _newEntry.WriteByte(data); + } + + /// + /// Add data to a pending new entry. + /// + /// The data to add. + /// + public void AddData(byte[] data) + { + if ( data == null ) { + throw new ArgumentNullException("data"); + } + + _newEntry.Write(data, 0, data.Length); + } + + /// + /// Add a short value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeShort(int toAdd) + { + unchecked { + _newEntry.WriteByte(( byte )toAdd); + _newEntry.WriteByte(( byte )(toAdd >> 8)); + } + } + + /// + /// Add an integer value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeInt(int toAdd) + { + unchecked { + AddLeShort(( short )toAdd); + AddLeShort(( short )(toAdd >> 16)); + } + } + + /// + /// Add a long value in little endian order to the pending new entry. + /// + /// The data to add. + /// + public void AddLeLong(long toAdd) + { + unchecked { + AddLeInt(( int )(toAdd & 0xffffffff)); + AddLeInt(( int )(toAdd >> 32)); + } + } + + /// + /// Delete an extra data field. + /// + /// The identifier of the field to delete. + /// Returns true if the field was found and deleted. + public bool Delete(int headerID) + { + bool result = false; + + if ( Find(headerID) ) { + result = true; + int trueStart = _readValueStart - 4; + + byte[] newData = new byte[_data.Length - (ValueLength + 4)]; + Array.Copy(_data, 0, newData, 0, trueStart); + + int trueEnd = trueStart + ValueLength + 4; + Array.Copy(_data, trueEnd, newData, trueStart, _data.Length - trueEnd); + _data = newData; + } + return result; + } + + #region Reading Support + /// + /// Read a long in little endian form from the last found data value + /// + /// Returns the long value read. + public long ReadLong() + { + ReadCheck(8); + return (ReadInt() & 0xffffffff) | ((( long )ReadInt()) << 32); + } + + /// + /// Read an integer in little endian form from the last found data value. + /// + /// Returns the integer read. + public int ReadInt() + { + ReadCheck(4); + + int result = _data[_index] + (_data[_index + 1] << 8) + + (_data[_index + 2] << 16) + (_data[_index + 3] << 24); + _index += 4; + return result; + } + + /// + /// Read a short value in little endian form from the last found data value. + /// + /// Returns the short value read. + public int ReadShort() + { + ReadCheck(2); + int result = _data[_index] + (_data[_index + 1] << 8); + _index += 2; + return result; + } + + /// + /// Read a byte from an extra data + /// + /// The byte value read or -1 if the end of data has been reached. + public int ReadByte() + { + int result = -1; + if ( (_index < _data.Length) && (_readValueStart + _readValueLength > _index) ) { + result = _data[_index]; + _index += 1; + } + return result; + } + + /// + /// Skip data during reading. + /// + /// The number of bytes to skip. + public void Skip(int amount) + { + ReadCheck(amount); + _index += amount; + } + + void ReadCheck(int length) + { + if ((_readValueStart > _data.Length) || + (_readValueStart < 4) ) { + throw new ZipException("Find must be called before calling a Read method"); + } + + if (_index > _readValueStart + _readValueLength - length ) { + throw new ZipException("End of extra data"); + } + + if ( _index + length < 4 ) { + throw new ZipException("Cannot read before start of tag"); + } + } + + /// + /// Internal form of that reads data at any location. + /// + /// Returns the short value read. + int ReadShortInternal() + { + if ( _index > _data.Length - 2) { + throw new ZipException("End of extra data"); + } + + int result = _data[_index] + (_data[_index + 1] << 8); + _index += 2; + return result; + } + + void SetShort(ref int index, int source) + { + _data[index] = (byte)source; + _data[index + 1] = (byte)(source >> 8); + index += 2; + } + + #endregion + + #region IDisposable Members + + /// + /// Dispose of this instance. + /// + public void Dispose() + { + if ( _newEntry != null ) { + _newEntry.Close(); + } + } + + #endregion + + #region Instance Fields + int _index; + int _readValueStart; + int _readValueLength; + + MemoryStream _newEntry; + byte[] _data; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs new file mode 100644 index 000000000..b896c1b3a --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipFile.cs @@ -0,0 +1,4486 @@ +// ZipFile.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 2009-12-22 Z-1649 Added AES support +// 2010-03-02 Z-1650 Fixed updating ODT archives in memory. Exposed exceptions in updating. +// 2010-05-25 Z-1663 Fixed exception when testing local header compressed size of -1 + +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Globalization; + +#if !NETCF_1_0 +using System.Security.Cryptography; +using GitHub.ICSharpCode.SharpZipLib.Encryption; +#endif + +using GitHub.ICSharpCode.SharpZipLib.Core; +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + #region Keys Required Event Args + /// + /// Arguments used with KeysRequiredEvent + /// + public class KeysRequiredEventArgs : EventArgs + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The name of the file for which keys are required. + public KeysRequiredEventArgs(string name) + { + fileName = name; + } + + /// + /// Initialise a new instance of + /// + /// The name of the file for which keys are required. + /// The current key value. + public KeysRequiredEventArgs(string name, byte[] keyValue) + { + fileName = name; + key = keyValue; + } + + #endregion + #region Properties + /// + /// Gets the name of the file for which keys are required. + /// + public string FileName + { + get { return fileName; } + } + + /// + /// Gets or sets the key value + /// + public byte[] Key + { + get { return key; } + set { key = value; } + } + #endregion + + #region Instance Fields + string fileName; + byte[] key; + #endregion + } + #endregion + + #region Test Definitions + /// + /// The strategy to apply to testing. + /// + public enum TestStrategy + { + /// + /// Find the first error only. + /// + FindFirstError, + /// + /// Find all possible errors. + /// + FindAllErrors, + } + + /// + /// The operation in progress reported by a during testing. + /// + /// TestArchive + public enum TestOperation + { + /// + /// Setting up testing. + /// + Initialising, + + /// + /// Testing an individual entries header + /// + EntryHeader, + + /// + /// Testing an individual entries data + /// + EntryData, + + /// + /// Testing an individual entry has completed. + /// + EntryComplete, + + /// + /// Running miscellaneous tests + /// + MiscellaneousTests, + + /// + /// Testing is complete + /// + Complete, + } + + /// + /// Status returned returned by during testing. + /// + /// TestArchive + public class TestStatus + { + #region Constructors + /// + /// Initialise a new instance of + /// + /// The this status applies to. + public TestStatus(ZipFile file) + { + file_ = file; + } + #endregion + + #region Properties + + /// + /// Get the current in progress. + /// + public TestOperation Operation + { + get { return operation_; } + } + + /// + /// Get the this status is applicable to. + /// + public ZipFile File + { + get { return file_; } + } + + /// + /// Get the current/last entry tested. + /// + public ZipEntry Entry + { + get { return entry_; } + } + + /// + /// Get the number of errors detected so far. + /// + public int ErrorCount + { + get { return errorCount_; } + } + + /// + /// Get the number of bytes tested so far for the current entry. + /// + public long BytesTested + { + get { return bytesTested_; } + } + + /// + /// Get a value indicating wether the last entry test was valid. + /// + public bool EntryValid + { + get { return entryValid_; } + } + #endregion + + #region Internal API + internal void AddError() + { + errorCount_++; + entryValid_ = false; + } + + internal void SetOperation(TestOperation operation) + { + operation_ = operation; + } + + internal void SetEntry(ZipEntry entry) + { + entry_ = entry; + entryValid_ = true; + bytesTested_ = 0; + } + + internal void SetBytesTested(long value) + { + bytesTested_ = value; + } + #endregion + + #region Instance Fields + ZipFile file_; + ZipEntry entry_; + bool entryValid_; + int errorCount_; + long bytesTested_; + TestOperation operation_; + #endregion + } + + /// + /// Delegate invoked during testing if supplied indicating current progress and status. + /// + /// If the message is non-null an error has occured. If the message is null + /// the operation as found in status has started. + public delegate void ZipTestResultHandler(TestStatus status, string message); + #endregion + + #region Update Definitions + /// + /// The possible ways of applying updates to an archive. + /// + public enum FileUpdateMode + { + /// + /// Perform all updates on temporary files ensuring that the original file is saved. + /// + Safe, + /// + /// Update the archive directly, which is faster but less safe. + /// + Direct, + } + #endregion + + #region ZipFile Class + /// + /// This class represents a Zip archive. You can ask for the contained + /// entries, or get an input stream for a file entry. The entry is + /// automatically decompressed. + /// + /// You can also update the archive adding or deleting entries. + /// + /// This class is thread safe for input: You can open input streams for arbitrary + /// entries in different threads. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// + /// + /// using System; + /// using System.Text; + /// using System.Collections; + /// using System.IO; + /// + /// using GitHub.ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// static public void Main(string[] args) + /// { + /// using (ZipFile zFile = new ZipFile(args[0])) { + /// Console.WriteLine("Listing of : " + zFile.Name); + /// Console.WriteLine(""); + /// Console.WriteLine("Raw Size Size Date Time Name"); + /// Console.WriteLine("-------- -------- -------- ------ ---------"); + /// foreach (ZipEntry e in zFile) { + /// if ( e.IsFile ) { + /// DateTime d = e.DateTime; + /// Console.WriteLine("{0, -10}{1, -10}{2} {3} {4}", e.Size, e.CompressedSize, + /// d.ToString("dd-MM-yy"), d.ToString("HH:mm"), + /// e.Name); + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipFile : IEnumerable, IDisposable + { + #region KeyHandling + + /// + /// Delegate for handling keys/password setting during compresion/decompression. + /// + public delegate void KeysRequiredEventHandler( + object sender, + KeysRequiredEventArgs e + ); + + /// + /// Event handler for handling encryption keys. + /// + public KeysRequiredEventHandler KeysRequired; + + /// + /// Handles getting of encryption keys when required. + /// + /// The file for which encryption keys are required. + void OnKeysRequired(string fileName) + { + if (KeysRequired != null) { + KeysRequiredEventArgs krea = new KeysRequiredEventArgs(fileName, key); + KeysRequired(this, krea); + key = krea.Key; + } + } + + /// + /// Get/set the encryption key value. + /// + byte[] Key + { + get { return key; } + set { key = value; } + } + +#if !NETCF_1_0 + /// + /// Password to be used for encrypting/decrypting files. + /// + /// Set to null if no password is required. + public string Password + { + set + { + if ( (value == null) || (value.Length == 0) ) { + key = null; + } + else { + rawPassword_ = value; + key = PkzipClassic.GenerateKeys(ZipConstants.ConvertToArray(value)); + } + } + } +#endif + + /// + /// Get a value indicating wether encryption keys are currently available. + /// + bool HaveKeys + { + get { return key != null; } + } + #endregion + + #region Constructors + /// + /// Opens a Zip file with the given name for reading. + /// + /// The name of the file to open. + /// The argument supplied is null. + /// + /// An i/o error occurs + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(string name) + { + if ( name == null ) { + throw new ArgumentNullException("name"); + } + + name_ = name; + + baseStream_ = File.Open(name, FileMode.Open, FileAccess.Read, FileShare.Read); + isStreamOwner = true; + + try { + ReadEntries(); + } + catch { + DisposeInternal(true); + throw; + } + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// The supplied argument is null. + /// + /// An i/o error occurs. + /// + /// + /// The file doesn't contain a valid zip archive. + /// + public ZipFile(FileStream file) + { + if ( file == null ) { + throw new ArgumentNullException("file"); + } + + if ( !file.CanSeek ) { + throw new ArgumentException("Stream is not seekable", "file"); + } + + baseStream_ = file; + name_ = file.Name; + isStreamOwner = true; + + try { + ReadEntries(); + } + catch { + DisposeInternal(true); + throw; + } + } + + /// + /// Opens a Zip file reading the given . + /// + /// The to read archive data from. + /// + /// An i/o error occurs + /// + /// + /// The stream doesn't contain a valid zip archive.
+ ///
+ /// + /// The stream doesnt support seeking. + /// + /// + /// The stream argument is null. + /// + public ZipFile(Stream stream) + { + if ( stream == null ) { + throw new ArgumentNullException("stream"); + } + + if ( !stream.CanSeek ) { + throw new ArgumentException("Stream is not seekable", "stream"); + } + + baseStream_ = stream; + isStreamOwner = true; + + if ( baseStream_.Length > 0 ) { + try { + ReadEntries(); + } + catch { + DisposeInternal(true); + throw; + } + } else { + entries_ = new ZipEntry[0]; + isNewArchive_ = true; + } + } + + /// + /// Initialises a default instance with no entries and no file storage. + /// + internal ZipFile() + { + entries_ = new ZipEntry[0]; + isNewArchive_ = true; + } + + #endregion + + #region Destructors and Closing + /// + /// Finalize this instance. + /// + ~ZipFile() + { + Dispose(false); + } + + /// + /// Closes the ZipFile. If the stream is owned then this also closes the underlying input stream. + /// Once closed, no further instance methods should be called. + /// + /// + /// An i/o error occurs. + /// + public void Close() + { + DisposeInternal(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region Creators + /// + /// Create a new whose data will be stored in a file. + /// + /// The name of the archive to create. + /// Returns the newly created + /// is null + public static ZipFile Create(string fileName) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + FileStream fs = File.Create(fileName); + + ZipFile result = new ZipFile(); + result.name_ = fileName; + result.baseStream_ = fs; + result.isStreamOwner = true; + return result; + } + + /// + /// Create a new whose data will be stored on a stream. + /// + /// The stream providing data storage. + /// Returns the newly created + /// is null + /// doesnt support writing. + public static ZipFile Create(Stream outStream) + { + if ( outStream == null ) { + throw new ArgumentNullException("outStream"); + } + + if ( !outStream.CanWrite ) { + throw new ArgumentException("Stream is not writeable", "outStream"); + } + + if ( !outStream.CanSeek ) { + throw new ArgumentException("Stream is not seekable", "outStream"); + } + + ZipFile result = new ZipFile(); + result.baseStream_ = outStream; + return result; + } + + #endregion + + #region Properties + /// + /// Get/set a flag indicating if the underlying stream is owned by the ZipFile instance. + /// If the flag is true then the stream will be closed when Close is called. + /// + /// + /// The default value is true in all cases. + /// + public bool IsStreamOwner + { + get { return isStreamOwner; } + set { isStreamOwner = value; } + } + + /// + /// Get a value indicating wether + /// this archive is embedded in another file or not. + /// + public bool IsEmbeddedArchive + { + // Not strictly correct in all circumstances currently + get { return offsetOfFirstEntry > 0; } + } + + /// + /// Get a value indicating that this archive is a new one. + /// + public bool IsNewArchive + { + get { return isNewArchive_; } + } + + /// + /// Gets the comment for the zip file. + /// + public string ZipFileComment + { + get { return comment_; } + } + + /// + /// Gets the name of this zip file. + /// + public string Name + { + get { return name_; } + } + + /// + /// Gets the number of entries in this zip file. + /// + /// + /// The Zip file has been closed. + /// + [Obsolete("Use the Count property instead")] + public int Size + { + get + { + return entries_.Length; + } + } + + /// + /// Get the number of entries contained in this . + /// + public long Count + { + get + { + return entries_.Length; + } + } + + /// + /// Indexer property for ZipEntries + /// + [System.Runtime.CompilerServices.IndexerNameAttribute("EntryByIndex")] + public ZipEntry this[int index] + { + get { + return (ZipEntry) entries_[index].Clone(); + } + } + + #endregion + + #region Input Handling + /// + /// Gets an enumerator for the Zip entries in this Zip file. + /// + /// Returns an for this archive. + /// + /// The Zip file has been closed. + /// + public IEnumerator GetEnumerator() + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + return new ZipEntryEnumerator(entries_); + } + + /// + /// Return the index of the entry with a matching name + /// + /// Entry name to find + /// If true the comparison is case insensitive + /// The index position of the matching entry or -1 if not found + /// + /// The Zip file has been closed. + /// + public int FindEntry(string name, bool ignoreCase) + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + // TODO: This will be slow as the next ice age for huge archives! + for (int i = 0; i < entries_.Length; i++) { + if (string.Compare(name, entries_[i].Name, ignoreCase, CultureInfo.InvariantCulture) == 0) { + return i; + } + } + return -1; + } + + /// + /// Searches for a zip entry in this archive with the given name. + /// String comparisons are case insensitive + /// + /// + /// The name to find. May contain directory components separated by slashes ('/'). + /// + /// + /// A clone of the zip entry, or null if no entry with that name exists. + /// + /// + /// The Zip file has been closed. + /// + public ZipEntry GetEntry(string name) + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + int index = FindEntry(name, true); + return (index >= 0) ? (ZipEntry) entries_[index].Clone() : null; + } + + /// + /// Gets an input stream for reading the given zip entry data in an uncompressed form. + /// Normally the should be an entry returned by GetEntry(). + /// + /// The to obtain a data for + /// An input containing data for this + /// + /// The ZipFile has already been closed + /// + /// + /// The compression method for the entry is unknown + /// + /// + /// The entry is not found in the ZipFile + /// + public Stream GetInputStream(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + long index = entry.ZipFileIndex; + if ( (index < 0) || (index >= entries_.Length) || (entries_[index].Name != entry.Name) ) { + index = FindEntry(entry.Name, true); + if (index < 0) { + throw new ZipException("Entry cannot be found"); + } + } + return GetInputStream(index); + } + + /// + /// Creates an input stream reading a zip entry + /// + /// The index of the entry to obtain an input stream for. + /// + /// An input containing data for this + /// + /// + /// The ZipFile has already been closed + /// + /// + /// The compression method for the entry is unknown + /// + /// + /// The entry is not found in the ZipFile + /// + public Stream GetInputStream(long entryIndex) + { + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + long start = LocateEntry(entries_[entryIndex]); + CompressionMethod method = entries_[entryIndex].CompressionMethod; + Stream result = new PartialInputStream(this, start, entries_[entryIndex].CompressedSize); + + if (entries_[entryIndex].IsCrypted == true) { +#if NETCF_1_0 + throw new ZipException("decryption not supported for Compact Framework 1.0"); +#else + result = CreateAndInitDecryptionStream(result, entries_[entryIndex]); + if (result == null) { + throw new ZipException("Unable to decrypt this entry"); + } +#endif + } + + switch (method) { + case CompressionMethod.Stored: + // read as is. + break; + + case CompressionMethod.Deflated: + // No need to worry about ownership and closing as underlying stream close does nothing. + result = new InflaterInputStream(result, new Inflater(true)); + break; + + default: + throw new ZipException("Unsupported compression method " + method); + } + + return result; + } + + #endregion + + #region Archive Testing + /// + /// Test an archive for integrity/validity + /// + /// Perform low level data Crc check + /// true if all tests pass, false otherwise + /// Testing will terminate on the first error found. + public bool TestArchive(bool testData) + { + return TestArchive(testData, TestStrategy.FindFirstError, null); + } + + /// + /// Test an archive for integrity/validity + /// + /// Perform low level data Crc check + /// The to apply. + /// The handler to call during testing. + /// true if all tests pass, false otherwise + /// The object has already been closed. + public bool TestArchive(bool testData, TestStrategy strategy, ZipTestResultHandler resultHandler) + { + if (isDisposed_) { + throw new ObjectDisposedException("ZipFile"); + } + + TestStatus status = new TestStatus(this); + + if ( resultHandler != null ) { + resultHandler(status, null); + } + + HeaderTest test = testData ? (HeaderTest.Header | HeaderTest.Extract) : HeaderTest.Header; + + bool testing = true; + + try { + int entryIndex = 0; + + while ( testing && (entryIndex < Count) ) { + if ( resultHandler != null ) { + status.SetEntry(this[entryIndex]); + status.SetOperation(TestOperation.EntryHeader); + resultHandler(status, null); + } + + try { + TestLocalHeader(this[entryIndex], test); + } + catch(ZipException ex) { + status.AddError(); + + if ( resultHandler != null ) { + resultHandler(status, + string.Format("Exception during test - '{0}'", ex.Message)); + } + + if ( strategy == TestStrategy.FindFirstError ) { + testing = false; + } + } + + if ( testing && testData && this[entryIndex].IsFile ) { + if ( resultHandler != null ) { + status.SetOperation(TestOperation.EntryData); + resultHandler(status, null); + } + + Crc32 crc = new Crc32(); + + using (Stream entryStream = this.GetInputStream(this[entryIndex])) + { + + byte[] buffer = new byte[4096]; + long totalBytes = 0; + int bytesRead; + while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0) + { + crc.Update(buffer, 0, bytesRead); + + if (resultHandler != null) + { + totalBytes += bytesRead; + status.SetBytesTested(totalBytes); + resultHandler(status, null); + } + } + } + + if (this[entryIndex].Crc != crc.Value) { + status.AddError(); + + if ( resultHandler != null ) { + resultHandler(status, "CRC mismatch"); + } + + if ( strategy == TestStrategy.FindFirstError ) { + testing = false; + } + } + + if (( this[entryIndex].Flags & (int)GeneralBitFlags.Descriptor) != 0 ) { + ZipHelperStream helper = new ZipHelperStream(baseStream_); + DescriptorData data = new DescriptorData(); + helper.ReadDataDescriptor(this[entryIndex].LocalHeaderRequiresZip64, data); + if (this[entryIndex].Crc != data.Crc) { + status.AddError(); + } + + if (this[entryIndex].CompressedSize != data.CompressedSize) { + status.AddError(); + } + + if (this[entryIndex].Size != data.Size) { + status.AddError(); + } + } + } + + if ( resultHandler != null ) { + status.SetOperation(TestOperation.EntryComplete); + resultHandler(status, null); + } + + entryIndex += 1; + } + + if ( resultHandler != null ) { + status.SetOperation(TestOperation.MiscellaneousTests); + resultHandler(status, null); + } + + // TODO: the 'Corrina Johns' test where local headers are missing from + // the central directory. They are therefore invisible to many archivers. + } + catch (Exception ex) { + status.AddError(); + + if ( resultHandler != null ) { + resultHandler(status, string.Format("Exception during test - '{0}'", ex.Message)); + } + } + + if ( resultHandler != null ) { + status.SetOperation(TestOperation.Complete); + status.SetEntry(null); + resultHandler(status, null); + } + + return (status.ErrorCount == 0); + } + + [Flags] + enum HeaderTest + { + Extract = 0x01, // Check that this header represents an entry whose data can be extracted + Header = 0x02, // Check that this header contents are valid + } + + /// + /// Test a local header against that provided from the central directory + /// + /// + /// The entry to test against + /// + /// The type of tests to carry out. + /// The offset of the entries data in the file + long TestLocalHeader(ZipEntry entry, HeaderTest tests) + { + lock(baseStream_) + { + bool testHeader = (tests & HeaderTest.Header) != 0; + bool testData = (tests & HeaderTest.Extract) != 0; + + baseStream_.Seek(offsetOfFirstEntry + entry.Offset, SeekOrigin.Begin); + if ((int)ReadLEUint() != ZipConstants.LocalHeaderSignature) { + throw new ZipException(string.Format("Wrong local header signature @{0:X}", offsetOfFirstEntry + entry.Offset)); + } + + short extractVersion = ( short )ReadLEUshort(); + short localFlags = ( short )ReadLEUshort(); + short compressionMethod = ( short )ReadLEUshort(); + short fileTime = ( short )ReadLEUshort(); + short fileDate = ( short )ReadLEUshort(); + uint crcValue = ReadLEUint(); + long compressedSize = ReadLEUint(); + long size = ReadLEUint(); + int storedNameLength = ReadLEUshort(); + int extraDataLength = ReadLEUshort(); + + byte[] nameData = new byte[storedNameLength]; + StreamUtils.ReadFully(baseStream_, nameData); + + byte[] extraData = new byte[extraDataLength]; + StreamUtils.ReadFully(baseStream_, extraData); + + ZipExtraData localExtraData = new ZipExtraData(extraData); + + // Extra data / zip64 checks + if (localExtraData.Find(1)) + { + // 2010-03-04 Forum 10512: removed checks for version >= ZipConstants.VersionZip64 + // and size or compressedSize = MaxValue, due to rogue creators. + + size = localExtraData.ReadLong(); + compressedSize = localExtraData.ReadLong(); + + if ((localFlags & (int)GeneralBitFlags.Descriptor) != 0) + { + // These may be valid if patched later + if ( (size != -1) && (size != entry.Size)) { + throw new ZipException("Size invalid for descriptor"); + } + + if ((compressedSize != -1) && (compressedSize != entry.CompressedSize)) { + throw new ZipException("Compressed size invalid for descriptor"); + } + } + } + else + { + // No zip64 extra data but entry requires it. + if ((extractVersion >= ZipConstants.VersionZip64) && + (((uint)size == uint.MaxValue) || ((uint)compressedSize == uint.MaxValue))) + { + throw new ZipException("Required Zip64 extended information missing"); + } + } + + if ( testData ) { + if ( entry.IsFile ) { + if ( !entry.IsCompressionMethodSupported() ) { + throw new ZipException("Compression method not supported"); + } + + if ( (extractVersion > ZipConstants.VersionMadeBy) + || ((extractVersion > 20) && (extractVersion < ZipConstants.VersionZip64)) ) { + throw new ZipException(string.Format("Version required to extract this entry not supported ({0})", extractVersion)); + } + + if ( (localFlags & ( int )(GeneralBitFlags.Patched | GeneralBitFlags.StrongEncryption | GeneralBitFlags.EnhancedCompress | GeneralBitFlags.HeaderMasked)) != 0 ) { + throw new ZipException("The library does not support the zip version required to extract this entry"); + } + } + } + + if (testHeader) + { + if ((extractVersion <= 63) && // Ignore later versions as we dont know about them.. + (extractVersion != 10) && + (extractVersion != 11) && + (extractVersion != 20) && + (extractVersion != 21) && + (extractVersion != 25) && + (extractVersion != 27) && + (extractVersion != 45) && + (extractVersion != 46) && + (extractVersion != 50) && + (extractVersion != 51) && + (extractVersion != 52) && + (extractVersion != 61) && + (extractVersion != 62) && + (extractVersion != 63) + ) + { + throw new ZipException(string.Format("Version required to extract this entry is invalid ({0})", extractVersion)); + } + + // Local entry flags dont have reserved bit set on. + if ((localFlags & (int)(GeneralBitFlags.ReservedPKware4 | GeneralBitFlags.ReservedPkware14 | GeneralBitFlags.ReservedPkware15)) != 0) + { + throw new ZipException("Reserved bit flags cannot be set."); + } + + // Encryption requires extract version >= 20 + if (((localFlags & (int)GeneralBitFlags.Encrypted) != 0) && (extractVersion < 20)) + { + throw new ZipException(string.Format("Version required to extract this entry is too low for encryption ({0})", extractVersion)); + } + + // Strong encryption requires encryption flag to be set and extract version >= 50. + if ((localFlags & (int)GeneralBitFlags.StrongEncryption) != 0) + { + if ((localFlags & (int)GeneralBitFlags.Encrypted) == 0) + { + throw new ZipException("Strong encryption flag set but encryption flag is not set"); + } + + if (extractVersion < 50) + { + throw new ZipException(string.Format("Version required to extract this entry is too low for encryption ({0})", extractVersion)); + } + } + + // Patched entries require extract version >= 27 + if (((localFlags & (int)GeneralBitFlags.Patched) != 0) && (extractVersion < 27)) + { + throw new ZipException(string.Format("Patched data requires higher version than ({0})", extractVersion)); + } + + // Central header flags match local entry flags. + if (localFlags != entry.Flags) + { + throw new ZipException("Central header/local header flags mismatch"); + } + + // Central header compression method matches local entry + if (entry.CompressionMethod != (CompressionMethod)compressionMethod) + { + throw new ZipException("Central header/local header compression method mismatch"); + } + + if (entry.Version != extractVersion) + { + throw new ZipException("Extract version mismatch"); + } + + // Strong encryption and extract version match + if ((localFlags & (int)GeneralBitFlags.StrongEncryption) != 0) + { + if (extractVersion < 62) + { + throw new ZipException("Strong encryption flag set but version not high enough"); + } + } + + if ((localFlags & (int)GeneralBitFlags.HeaderMasked) != 0) + { + if ((fileTime != 0) || (fileDate != 0)) + { + throw new ZipException("Header masked set but date/time values non-zero"); + } + } + + if ((localFlags & (int)GeneralBitFlags.Descriptor) == 0) + { + if (crcValue != (uint)entry.Crc) + { + throw new ZipException("Central header/local header crc mismatch"); + } + } + + // Crc valid for empty entry. + // This will also apply to streamed entries where size isnt known and the header cant be patched + if ((size == 0) && (compressedSize == 0)) + { + if (crcValue != 0) + { + throw new ZipException("Invalid CRC for empty entry"); + } + } + + // TODO: make test more correct... can't compare lengths as was done originally as this can fail for MBCS strings + // Assuming a code page at this point is not valid? Best is to store the name length in the ZipEntry probably + if (entry.Name.Length > storedNameLength) + { + throw new ZipException("File name length mismatch"); + } + + // Name data has already been read convert it and compare. + string localName = ZipConstants.ConvertToStringExt(localFlags, nameData); + + // Central directory and local entry name match + if (localName != entry.Name) + { + throw new ZipException("Central header and local header file name mismatch"); + } + + // Directories have zero actual size but can have compressed size + if (entry.IsDirectory) + { + if (size > 0) + { + throw new ZipException("Directory cannot have size"); + } + + // There may be other cases where the compressed size can be greater than this? + // If so until details are known we will be strict. + if (entry.IsCrypted) + { + if (compressedSize > ZipConstants.CryptoHeaderSize + 2) + { + throw new ZipException("Directory compressed size invalid"); + } + } + else if (compressedSize > 2) + { + // When not compressed the directory size can validly be 2 bytes + // if the true size wasnt known when data was originally being written. + // NOTE: Versions of the library 0.85.4 and earlier always added 2 bytes + throw new ZipException("Directory compressed size invalid"); + } + } + + if (!ZipNameTransform.IsValidName(localName, true)) + { + throw new ZipException("Name is invalid"); + } + } + + // Tests that apply to both data and header. + + // Size can be verified only if it is known in the local header. + // it will always be known in the central header. + if (((localFlags & (int)GeneralBitFlags.Descriptor) == 0) || + ((size > 0) || (compressedSize > 0))) { + + if (size != entry.Size) { + throw new ZipException( + string.Format("Size mismatch between central header({0}) and local header({1})", + entry.Size, size)); + } + + if (compressedSize != entry.CompressedSize && + compressedSize != 0xFFFFFFFF && compressedSize != -1) { + throw new ZipException( + string.Format("Compressed size mismatch between central header({0}) and local header({1})", + entry.CompressedSize, compressedSize)); + } + } + + int extraLength = storedNameLength + extraDataLength; + return offsetOfFirstEntry + entry.Offset + ZipConstants.LocalHeaderBaseSize + extraLength; + } + } + + #endregion + + #region Updating + + const int DefaultBufferSize = 4096; + + /// + /// The kind of update to apply. + /// + enum UpdateCommand + { + Copy, // Copy original file contents. + Modify, // Change encryption, compression, attributes, name, time etc, of an existing file. + Add, // Add a new file to the archive. + } + + #region Properties + /// + /// Get / set the to apply to names when updating. + /// + public INameTransform NameTransform + { + get { + return updateEntryFactory_.NameTransform; + } + + set { + updateEntryFactory_.NameTransform = value; + } + } + + /// + /// Get/set the used to generate values + /// during updates. + /// + public IEntryFactory EntryFactory + { + get { + return updateEntryFactory_; + } + + set { + if (value == null) { + updateEntryFactory_ = new ZipEntryFactory(); + } + else { + updateEntryFactory_ = value; + } + } + } + + /// + /// Get /set the buffer size to be used when updating this zip file. + /// + public int BufferSize + { + get { return bufferSize_; } + set { + if ( value < 1024 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("value"); +#else + throw new ArgumentOutOfRangeException("value", "cannot be below 1024"); +#endif + } + + if ( bufferSize_ != value ) { + bufferSize_ = value; + copyBuffer_ = null; + } + } + } + + /// + /// Get a value indicating an update has been started. + /// + public bool IsUpdating + { + get { return updates_ != null; } + } + + /// + /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries. + /// + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + #endregion + + #region Immediate updating +// TBD: Direct form of updating +// +// public void Update(IEntryMatcher deleteMatcher) +// { +// } +// +// public void Update(IScanner addScanner) +// { +// } + #endregion + + #region Deferred Updating + /// + /// Begin updating this archive. + /// + /// The archive storage for use during the update. + /// The data source to utilise during updating. + /// ZipFile has been closed. + /// One of the arguments provided is null + /// ZipFile has been closed. + public void BeginUpdate(IArchiveStorage archiveStorage, IDynamicDataSource dataSource) + { + if ( archiveStorage == null ) { + throw new ArgumentNullException("archiveStorage"); + } + + if ( dataSource == null ) { + throw new ArgumentNullException("dataSource"); + } + + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + if ( IsEmbeddedArchive ) { + throw new ZipException ("Cannot update embedded/SFX archives"); + } + + archiveStorage_ = archiveStorage; + updateDataSource_ = dataSource; + + // NOTE: the baseStream_ may not currently support writing or seeking. + + updateIndex_ = new Hashtable(); + + updates_ = new ArrayList(entries_.Length); + foreach(ZipEntry entry in entries_) { + int index = updates_.Add(new ZipUpdate(entry)); + updateIndex_.Add(entry.Name, index); + } + + // We must sort by offset before using offset's calculated sizes + updates_.Sort(new UpdateComparer()); + + int idx = 0; + foreach (ZipUpdate update in updates_) { + //If last entry, there is no next entry offset to use + if (idx == updates_.Count - 1) + break; + + update.OffsetBasedSize = ((ZipUpdate)updates_[idx + 1]).Entry.Offset - update.Entry.Offset; + idx++; + } + updateCount_ = updates_.Count; + + contentsEdited_ = false; + commentEdited_ = false; + newComment_ = null; + } + + /// + /// Begin updating to this archive. + /// + /// The storage to use during the update. + public void BeginUpdate(IArchiveStorage archiveStorage) + { + BeginUpdate(archiveStorage, new DynamicDiskDataSource()); + } + + /// + /// Begin updating this archive. + /// + /// + /// + /// + public void BeginUpdate() + { + if ( Name == null ) { + BeginUpdate(new MemoryArchiveStorage(), new DynamicDiskDataSource()); + } + else { + BeginUpdate(new DiskArchiveStorage(this), new DynamicDiskDataSource()); + } + } + + /// + /// Commit current updates, updating this archive. + /// + /// + /// + /// ZipFile has been closed. + public void CommitUpdate() + { + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + CheckUpdating(); + + try { + updateIndex_.Clear(); + updateIndex_=null; + + if( contentsEdited_ ) { + RunUpdates(); + } + else if( commentEdited_ ) { + UpdateCommentOnly(); + } + else { + // Create an empty archive if none existed originally. + if( entries_.Length==0 ) { + byte[] theComment=(newComment_!=null)?newComment_.RawComment:ZipConstants.ConvertToArray(comment_); + using( ZipHelperStream zhs=new ZipHelperStream(baseStream_) ) { + zhs.WriteEndOfCentralDirectory(0, 0, 0, theComment); + } + } + } + + } + finally { + PostUpdateCleanup(); + } + } + + /// + /// Abort updating leaving the archive unchanged. + /// + /// + /// + public void AbortUpdate() + { + PostUpdateCleanup(); + } + + /// + /// Set the file comment to be recorded when the current update is commited. + /// + /// The comment to record. + /// ZipFile has been closed. + public void SetComment(string comment) + { + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + CheckUpdating(); + + newComment_ = new ZipString(comment); + + if ( newComment_.RawLength > 0xffff ) { + newComment_ = null; + throw new ZipException("Comment length exceeds maximum - 65535"); + } + + // We dont take account of the original and current comment appearing to be the same + // as encoding may be different. + commentEdited_ = true; + } + + #endregion + + #region Adding Entries + + void AddUpdate(ZipUpdate update) + { + contentsEdited_ = true; + + int index = FindExistingUpdate(update.Entry.Name); + + if (index >= 0) { + if ( updates_[index] == null ) { + updateCount_ += 1; + } + + // Direct replacement is faster than delete and add. + updates_[index] = update; + } + else { + index = updates_.Add(update); + updateCount_ += 1; + updateIndex_.Add(update.Entry.Name, index); + } + } + + /// + /// Add a new entry to the archive. + /// + /// The name of the file to add. + /// The compression method to use. + /// Ensure Unicode text is used for name and comment for this entry. + /// Argument supplied is null. + /// ZipFile has been closed. + /// Compression method is not supported. + public void Add(string fileName, CompressionMethod compressionMethod, bool useUnicodeText ) + { + if (fileName == null) { + throw new ArgumentNullException("fileName"); + } + + if ( isDisposed_ ) { + throw new ObjectDisposedException("ZipFile"); + } + + if (!ZipEntry.IsCompressionMethodSupported(compressionMethod)) { + throw new ArgumentOutOfRangeException("compressionMethod"); + } + + CheckUpdating(); + contentsEdited_ = true; + + ZipEntry entry = EntryFactory.MakeFileEntry(fileName); + entry.IsUnicodeText = useUnicodeText; + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(fileName, entry)); + } + + /// + /// Add a new entry to the archive. + /// + /// The name of the file to add. + /// The compression method to use. + /// ZipFile has been closed. + /// The compression method is not supported. + public void Add(string fileName, CompressionMethod compressionMethod) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + if ( !ZipEntry.IsCompressionMethodSupported(compressionMethod) ) { + throw new ArgumentOutOfRangeException("compressionMethod"); + } + + CheckUpdating(); + contentsEdited_ = true; + + ZipEntry entry = EntryFactory.MakeFileEntry(fileName); + entry.CompressionMethod = compressionMethod; + AddUpdate(new ZipUpdate(fileName, entry)); + } + + /// + /// Add a file to the archive. + /// + /// The name of the file to add. + /// Argument supplied is null. + public void Add(string fileName) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(fileName, EntryFactory.MakeFileEntry(fileName))); + } + + /// + /// Add a file to the archive. + /// + /// The name of the file to add. + /// The name to use for the on the Zip file created. + /// Argument supplied is null. + public void Add(string fileName, string entryName) + { + if (fileName == null) { + throw new ArgumentNullException("fileName"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(fileName, EntryFactory.MakeFileEntry(entryName))); + } + + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + public void Add(IStaticDataSource dataSource, string entryName) + { + if ( dataSource == null ) { + throw new ArgumentNullException("dataSource"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + AddUpdate(new ZipUpdate(dataSource, EntryFactory.MakeFileEntry(entryName, false))); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + /// The compression method to use. + public void Add(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod) + { + if ( dataSource == null ) { + throw new ArgumentNullException("dataSource"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + + ZipEntry entry = EntryFactory.MakeFileEntry(entryName, false); + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a file entry with data. + /// + /// The source of the data for this entry. + /// The name to give to the entry. + /// The compression method to use. + /// Ensure Unicode text is used for name and comments for this entry. + public void Add(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod, bool useUnicodeText) + { + if (dataSource == null) { + throw new ArgumentNullException("dataSource"); + } + + if ( entryName == null ) { + throw new ArgumentNullException("entryName"); + } + + CheckUpdating(); + + ZipEntry entry = EntryFactory.MakeFileEntry(entryName, false); + entry.IsUnicodeText = useUnicodeText; + entry.CompressionMethod = compressionMethod; + + AddUpdate(new ZipUpdate(dataSource, entry)); + } + + /// + /// Add a that contains no data. + /// + /// The entry to add. + /// This can be used to add directories, volume labels, or empty file entries. + public void Add(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + CheckUpdating(); + + if ( (entry.Size != 0) || (entry.CompressedSize != 0) ) { + throw new ZipException("Entry cannot have any data"); + } + + AddUpdate(new ZipUpdate(UpdateCommand.Add, entry)); + } + + /// + /// Add a directory entry to the archive. + /// + /// The directory to add. + public void AddDirectory(string directoryName) + { + if ( directoryName == null ) { + throw new ArgumentNullException("directoryName"); + } + + CheckUpdating(); + + ZipEntry dirEntry = EntryFactory.MakeDirectoryEntry(directoryName); + AddUpdate(new ZipUpdate(UpdateCommand.Add, dirEntry)); + } + + #endregion + + #region Modifying Entries +/* Modify not yet ready for public consumption. + Direct modification of an entry should not overwrite original data before its read. + Safe mode is trivial in this sense. + public void Modify(ZipEntry original, ZipEntry updated) + { + if ( original == null ) { + throw new ArgumentNullException("original"); + } + + if ( updated == null ) { + throw new ArgumentNullException("updated"); + } + + CheckUpdating(); + contentsEdited_ = true; + updates_.Add(new ZipUpdate(original, updated)); + } +*/ + #endregion + + #region Deleting Entries + /// + /// Delete an entry by name + /// + /// The filename to delete + /// True if the entry was found and deleted; false otherwise. + public bool Delete(string fileName) + { + if ( fileName == null ) { + throw new ArgumentNullException("fileName"); + } + + CheckUpdating(); + + bool result = false; + int index = FindExistingUpdate(fileName); + if ( (index >= 0) && (updates_[index] != null) ) { + result = true; + contentsEdited_ = true; + updates_[index] = null; + updateCount_ -= 1; + } + else { + throw new ZipException("Cannot find entry to delete"); + } + return result; + } + + /// + /// Delete a from the archive. + /// + /// The entry to delete. + public void Delete(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + CheckUpdating(); + + int index = FindExistingUpdate(entry); + if ( index >= 0 ) { + contentsEdited_ = true; + updates_[index] = null; + updateCount_ -= 1; + } + else { + throw new ZipException("Cannot find entry to delete"); + } + } + + #endregion + + #region Update Support + + #region Writing Values/Headers + void WriteLEShort(int value) + { + baseStream_.WriteByte(( byte )(value & 0xff)); + baseStream_.WriteByte(( byte )((value >> 8) & 0xff)); + } + + /// + /// Write an unsigned short in little endian byte order. + /// + void WriteLEUshort(ushort value) + { + baseStream_.WriteByte(( byte )(value & 0xff)); + baseStream_.WriteByte(( byte )(value >> 8)); + } + + /// + /// Write an int in little endian byte order. + /// + void WriteLEInt(int value) + { + WriteLEShort(value & 0xffff); + WriteLEShort(value >> 16); + } + + /// + /// Write an unsigned int in little endian byte order. + /// + void WriteLEUint(uint value) + { + WriteLEUshort((ushort)(value & 0xffff)); + WriteLEUshort((ushort)(value >> 16)); + } + + /// + /// Write a long in little endian byte order. + /// + void WriteLeLong(long value) + { + WriteLEInt(( int )(value & 0xffffffff)); + WriteLEInt(( int )(value >> 32)); + } + + void WriteLEUlong(ulong value) + { + WriteLEUint(( uint )(value & 0xffffffff)); + WriteLEUint(( uint )(value >> 32)); + } + + void WriteLocalEntryHeader(ZipUpdate update) + { + ZipEntry entry = update.OutEntry; + + // TODO: Local offset will require adjusting for multi-disk zip files. + entry.Offset = baseStream_.Position; + + // TODO: Need to clear any entry flags that dont make sense or throw an exception here. + if (update.Command != UpdateCommand.Copy) { + if (entry.CompressionMethod == CompressionMethod.Deflated) { + if (entry.Size == 0) { + // No need to compress - no data. + entry.CompressedSize = entry.Size; + entry.Crc = 0; + entry.CompressionMethod = CompressionMethod.Stored; + } + } + else if (entry.CompressionMethod == CompressionMethod.Stored) { + entry.Flags &= ~(int)GeneralBitFlags.Descriptor; + } + + if (HaveKeys) { + entry.IsCrypted = true; + if (entry.Crc < 0) { + entry.Flags |= (int)GeneralBitFlags.Descriptor; + } + } + else { + entry.IsCrypted = false; + } + + switch (useZip64_) { + case UseZip64.Dynamic: + if (entry.Size < 0) { + entry.ForceZip64(); + } + break; + + case UseZip64.On: + entry.ForceZip64(); + break; + + case UseZip64.Off: + // Do nothing. The entry itself may be using Zip64 independantly. + break; + } + } + + // Write the local file header + WriteLEInt(ZipConstants.LocalHeaderSignature); + + WriteLEShort(entry.Version); + WriteLEShort(entry.Flags); + + WriteLEShort((byte)entry.CompressionMethod); + WriteLEInt(( int )entry.DosTime); + + if ( !entry.HasCrc ) { + // Note patch address for updating CRC later. + update.CrcPatchOffset = baseStream_.Position; + WriteLEInt(( int )0); + } + else { + WriteLEInt(unchecked(( int )entry.Crc)); + } + + if (entry.LocalHeaderRequiresZip64) { + WriteLEInt(-1); + WriteLEInt(-1); + } + else { + if ( (entry.CompressedSize < 0) || (entry.Size < 0) ) { + update.SizePatchOffset = baseStream_.Position; + } + + WriteLEInt(( int )entry.CompressedSize); + WriteLEInt(( int )entry.Size); + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if ( name.Length > 0xFFFF ) { + throw new ZipException("Entry name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if ( entry.LocalHeaderRequiresZip64 ) { + ed.StartNewEntry(); + + // Local entry header always includes size and compressed size. + // NOTE the order of these fields is reversed when compared to the normal headers! + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + ed.AddNewEntry(1); + } + else { + ed.Delete(1); + } + + entry.ExtraData = ed.GetEntryData(); + + WriteLEShort(name.Length); + WriteLEShort(entry.ExtraData.Length); + + if ( name.Length > 0 ) { + baseStream_.Write(name, 0, name.Length); + } + + if ( entry.LocalHeaderRequiresZip64 ) { + if ( !ed.Find(1) ) { + throw new ZipException("Internal error cannot find extra data"); + } + + update.SizePatchOffset = baseStream_.Position + ed.CurrentReadIndex; + } + + if ( entry.ExtraData.Length > 0 ) { + baseStream_.Write(entry.ExtraData, 0, entry.ExtraData.Length); + } + } + + int WriteCentralDirectoryHeader(ZipEntry entry) + { + if ( entry.CompressedSize < 0 ) { + throw new ZipException("Attempt to write central directory entry with unknown csize"); + } + + if ( entry.Size < 0 ) { + throw new ZipException("Attempt to write central directory entry with unknown size"); + } + + if ( entry.Crc < 0 ) { + throw new ZipException("Attempt to write central directory entry with unknown crc"); + } + + // Write the central file header + WriteLEInt(ZipConstants.CentralHeaderSignature); + + // Version made by + WriteLEShort(ZipConstants.VersionMadeBy); + + // Version required to extract + WriteLEShort(entry.Version); + + WriteLEShort(entry.Flags); + + unchecked { + WriteLEShort((byte)entry.CompressionMethod); + WriteLEInt((int)entry.DosTime); + WriteLEInt((int)entry.Crc); + } + + if ( (entry.IsZip64Forced()) || (entry.CompressedSize >= 0xffffffff) ) { + WriteLEInt(-1); + } + else { + WriteLEInt((int)(entry.CompressedSize & 0xffffffff)); + } + + if ( (entry.IsZip64Forced()) || (entry.Size >= 0xffffffff) ) { + WriteLEInt(-1); + } + else { + WriteLEInt((int)entry.Size); + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if ( name.Length > 0xFFFF ) { + throw new ZipException("Entry name is too long."); + } + + WriteLEShort(name.Length); + + // Central header extra data is different to local header version so regenerate. + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if ( entry.CentralHeaderRequiresZip64 ) { + ed.StartNewEntry(); + + if ( (entry.Size >= 0xffffffff) || (useZip64_ == UseZip64.On) ) + { + ed.AddLeLong(entry.Size); + } + + if ( (entry.CompressedSize >= 0xffffffff) || (useZip64_ == UseZip64.On) ) + { + ed.AddLeLong(entry.CompressedSize); + } + + if ( entry.Offset >= 0xffffffff ) { + ed.AddLeLong(entry.Offset); + } + + // Number of disk on which this file starts isnt supported and is never written here. + ed.AddNewEntry(1); + } + else { + // Should have already be done when local header was added. + ed.Delete(1); + } + + byte[] centralExtraData = ed.GetEntryData(); + + WriteLEShort(centralExtraData.Length); + WriteLEShort(entry.Comment != null ? entry.Comment.Length : 0); + + WriteLEShort(0); // disk number + WriteLEShort(0); // internal file attributes + + // External file attributes... + if ( entry.ExternalFileAttributes != -1 ) { + WriteLEInt(entry.ExternalFileAttributes); + } + else { + if ( entry.IsDirectory ) { + WriteLEUint(16); + } + else { + WriteLEUint(0); + } + } + + if ( entry.Offset >= 0xffffffff ) { + WriteLEUint(0xffffffff); + } + else { + WriteLEUint((uint)(int)entry.Offset); + } + + if ( name.Length > 0 ) { + baseStream_.Write(name, 0, name.Length); + } + + if ( centralExtraData.Length > 0 ) { + baseStream_.Write(centralExtraData, 0, centralExtraData.Length); + } + + byte[] rawComment = (entry.Comment != null) ? Encoding.ASCII.GetBytes(entry.Comment) : new byte[0]; + + if ( rawComment.Length > 0 ) { + baseStream_.Write(rawComment, 0, rawComment.Length); + } + + return ZipConstants.CentralHeaderBaseSize + name.Length + centralExtraData.Length + rawComment.Length; + } + #endregion + + void PostUpdateCleanup() + { + updateDataSource_ = null; + updates_ = null; + updateIndex_ = null; + + if (archiveStorage_ != null) + { + archiveStorage_.Dispose(); + archiveStorage_=null; + } + } + + string GetTransformedFileName(string name) + { + INameTransform transform = NameTransform; + return (transform != null) ? + transform.TransformFile(name) : + name; + } + + string GetTransformedDirectoryName(string name) + { + INameTransform transform = NameTransform; + return (transform != null) ? + transform.TransformDirectory(name) : + name; + } + + /// + /// Get a raw memory buffer. + /// + /// Returns a raw memory buffer. + byte[] GetBuffer() + { + if ( copyBuffer_ == null ) { + copyBuffer_ = new byte[bufferSize_]; + } + return copyBuffer_; + } + + void CopyDescriptorBytes(ZipUpdate update, Stream dest, Stream source) + { + int bytesToCopy = GetDescriptorSize(update); + + if ( bytesToCopy > 0 ) { + byte[] buffer = GetBuffer(); + + while ( bytesToCopy > 0 ) { + int readSize = Math.Min(buffer.Length, bytesToCopy); + + int bytesRead = source.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + dest.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + else { + throw new ZipException("Unxpected end of stream"); + } + } + } + } + + void CopyBytes(ZipUpdate update, Stream destination, Stream source, + long bytesToCopy, bool updateCrc) + { + if ( destination == source ) { + throw new InvalidOperationException("Destination and source are the same"); + } + + // NOTE: Compressed size is updated elsewhere. + Crc32 crc = new Crc32(); + byte[] buffer = GetBuffer(); + + long targetBytes = bytesToCopy; + long totalBytesRead = 0; + + int bytesRead; + do { + int readSize = buffer.Length; + + if ( bytesToCopy < readSize ) { + readSize = (int)bytesToCopy; + } + + bytesRead = source.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + if ( updateCrc ) { + crc.Update(buffer, 0, bytesRead); + } + destination.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + totalBytesRead += bytesRead; + } + } + while ( (bytesRead > 0) && (bytesToCopy > 0) ); + + if ( totalBytesRead != targetBytes ) { + throw new ZipException(string.Format("Failed to copy bytes expected {0} read {1}", targetBytes, totalBytesRead)); + } + + if ( updateCrc ) { + update.OutEntry.Crc = crc.Value; + } + } + + /// + /// Get the size of the source descriptor for a . + /// + /// The update to get the size for. + /// The descriptor size, zero if there isnt one. + int GetDescriptorSize(ZipUpdate update) + { + int result = 0; + if ( (update.Entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) { + result = ZipConstants.DataDescriptorSize - 4; + if ( update.Entry.LocalHeaderRequiresZip64 ) { + result = ZipConstants.Zip64DataDescriptorSize - 4; + } + } + return result; + } + + void CopyDescriptorBytesDirect(ZipUpdate update, Stream stream, ref long destinationPosition, long sourcePosition) + { + int bytesToCopy = GetDescriptorSize(update); + + while ( bytesToCopy > 0 ) { + int readSize = (int)bytesToCopy; + byte[] buffer = GetBuffer(); + + stream.Position = sourcePosition; + int bytesRead = stream.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + stream.Position = destinationPosition; + stream.Write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + destinationPosition += bytesRead; + sourcePosition += bytesRead; + } + else { + throw new ZipException("Unxpected end of stream"); + } + } + } + + void CopyEntryDataDirect(ZipUpdate update, Stream stream, bool updateCrc, ref long destinationPosition, ref long sourcePosition) + { + long bytesToCopy = update.Entry.CompressedSize; + + // NOTE: Compressed size is updated elsewhere. + Crc32 crc = new Crc32(); + byte[] buffer = GetBuffer(); + + long targetBytes = bytesToCopy; + long totalBytesRead = 0; + + int bytesRead; + do + { + int readSize = buffer.Length; + + if ( bytesToCopy < readSize ) { + readSize = (int)bytesToCopy; + } + + stream.Position = sourcePosition; + bytesRead = stream.Read(buffer, 0, readSize); + if ( bytesRead > 0 ) { + if ( updateCrc ) { + crc.Update(buffer, 0, bytesRead); + } + stream.Position = destinationPosition; + stream.Write(buffer, 0, bytesRead); + + destinationPosition += bytesRead; + sourcePosition += bytesRead; + bytesToCopy -= bytesRead; + totalBytesRead += bytesRead; + } + } + while ( (bytesRead > 0) && (bytesToCopy > 0) ); + + if ( totalBytesRead != targetBytes ) { + throw new ZipException(string.Format("Failed to copy bytes expected {0} read {1}", targetBytes, totalBytesRead)); + } + + if ( updateCrc ) { + update.OutEntry.Crc = crc.Value; + } + } + + int FindExistingUpdate(ZipEntry entry) + { + int result = -1; + string convertedName = GetTransformedFileName(entry.Name); + + if (updateIndex_.ContainsKey(convertedName)) { + result = (int)updateIndex_[convertedName]; + } +/* + // This is slow like the coming of the next ice age but takes less storage and may be useful + // for CF? + for (int index = 0; index < updates_.Count; ++index) + { + ZipUpdate zu = ( ZipUpdate )updates_[index]; + if ( (zu.Entry.ZipFileIndex == entry.ZipFileIndex) && + (string.Compare(convertedName, zu.Entry.Name, true, CultureInfo.InvariantCulture) == 0) ) { + result = index; + break; + } + } + */ + return result; + } + + int FindExistingUpdate(string fileName) + { + int result = -1; + + string convertedName = GetTransformedFileName(fileName); + + if (updateIndex_.ContainsKey(convertedName)) { + result = (int)updateIndex_[convertedName]; + } + +/* + // This is slow like the coming of the next ice age but takes less storage and may be useful + // for CF? + for ( int index = 0; index < updates_.Count; ++index ) { + if ( string.Compare(convertedName, (( ZipUpdate )updates_[index]).Entry.Name, + true, CultureInfo.InvariantCulture) == 0 ) { + result = index; + break; + } + } + */ + + return result; + } + + /// + /// Get an output stream for the specified + /// + /// The entry to get an output stream for. + /// The output stream obtained for the entry. + Stream GetOutputStream(ZipEntry entry) + { + Stream result = baseStream_; + + if ( entry.IsCrypted == true ) { +#if NETCF_1_0 + throw new ZipException("Encryption not supported for Compact Framework 1.0"); +#else + result = CreateAndInitEncryptionStream(result, entry); +#endif + } + + switch ( entry.CompressionMethod ) { + case CompressionMethod.Stored: + result = new UncompressedStream(result); + break; + + case CompressionMethod.Deflated: + DeflaterOutputStream dos = new DeflaterOutputStream(result, new Deflater(9, true)); + dos.IsStreamOwner = false; + result = dos; + break; + + default: + throw new ZipException("Unknown compression method " + entry.CompressionMethod); + } + return result; + } + + void AddEntry(ZipFile workFile, ZipUpdate update) + { + Stream source = null; + + if ( update.Entry.IsFile ) { + source = update.GetSource(); + + if ( source == null ) { + source = updateDataSource_.GetSource(update.Entry, update.Filename); + } + } + + if ( source != null ) { + using ( source ) { + long sourceStreamLength = source.Length; + if ( update.OutEntry.Size < 0 ) { + update.OutEntry.Size = sourceStreamLength; + } + else { + // Check for errant entries. + if ( update.OutEntry.Size != sourceStreamLength ) { + throw new ZipException("Entry size/stream size mismatch"); + } + } + + workFile.WriteLocalEntryHeader(update); + + long dataStart = workFile.baseStream_.Position; + + using ( Stream output = workFile.GetOutputStream(update.OutEntry) ) { + CopyBytes(update, output, source, sourceStreamLength, true); + } + + long dataEnd = workFile.baseStream_.Position; + update.OutEntry.CompressedSize = dataEnd - dataStart; + + if ((update.OutEntry.Flags & (int)GeneralBitFlags.Descriptor) == (int)GeneralBitFlags.Descriptor) + { + ZipHelperStream helper = new ZipHelperStream(workFile.baseStream_); + helper.WriteDataDescriptor(update.OutEntry); + } + } + } + else { + workFile.WriteLocalEntryHeader(update); + update.OutEntry.CompressedSize = 0; + } + + } + + void ModifyEntry(ZipFile workFile, ZipUpdate update) + { + workFile.WriteLocalEntryHeader(update); + long dataStart = workFile.baseStream_.Position; + + // TODO: This is slow if the changes don't effect the data!! + if ( update.Entry.IsFile && (update.Filename != null) ) { + using ( Stream output = workFile.GetOutputStream(update.OutEntry) ) { + using ( Stream source = this.GetInputStream(update.Entry) ) { + CopyBytes(update, output, source, source.Length, true); + } + } + } + + long dataEnd = workFile.baseStream_.Position; + update.Entry.CompressedSize = dataEnd - dataStart; + } + + void CopyEntryDirect(ZipFile workFile, ZipUpdate update, ref long destinationPosition) + { + bool skipOver = false; + if ( update.Entry.Offset == destinationPosition ) { + skipOver = true; + } + + if ( !skipOver ) { + baseStream_.Position = destinationPosition; + workFile.WriteLocalEntryHeader(update); + destinationPosition = baseStream_.Position; + } + + long sourcePosition = 0; + + const int NameLengthOffset = 26; + + // TODO: Add base for SFX friendly handling + long entryDataOffset = update.Entry.Offset + NameLengthOffset; + + baseStream_.Seek(entryDataOffset, SeekOrigin.Begin); + + // Clumsy way of handling retrieving the original name and extra data length for now. + // TODO: Stop re-reading name and data length in CopyEntryDirect. + uint nameLength = ReadLEUshort(); + uint extraLength = ReadLEUshort(); + + sourcePosition = baseStream_.Position + nameLength + extraLength; + + if (skipOver) { + if (update.OffsetBasedSize != -1) + destinationPosition += update.OffsetBasedSize; + else + // TODO: Find out why this calculation comes up 4 bytes short on some entries in ODT (Office Document Text) archives. + // WinZip produces a warning on these entries: + // "caution: value of lrec.csize (compressed size) changed from ..." + destinationPosition += + (sourcePosition - entryDataOffset) + NameLengthOffset + // Header size + update.Entry.CompressedSize + GetDescriptorSize(update); + } + else { + if ( update.Entry.CompressedSize > 0 ) { + CopyEntryDataDirect(update, baseStream_, false, ref destinationPosition, ref sourcePosition ); + } + CopyDescriptorBytesDirect(update, baseStream_, ref destinationPosition, sourcePosition); + } + } + + void CopyEntry(ZipFile workFile, ZipUpdate update) + { + workFile.WriteLocalEntryHeader(update); + + if ( update.Entry.CompressedSize > 0 ) { + const int NameLengthOffset = 26; + + long entryDataOffset = update.Entry.Offset + NameLengthOffset; + + // TODO: This wont work for SFX files! + baseStream_.Seek(entryDataOffset, SeekOrigin.Begin); + + uint nameLength = ReadLEUshort(); + uint extraLength = ReadLEUshort(); + + baseStream_.Seek(nameLength + extraLength, SeekOrigin.Current); + + CopyBytes(update, workFile.baseStream_, baseStream_, update.Entry.CompressedSize, false); + } + CopyDescriptorBytes(update, workFile.baseStream_, baseStream_); + } + + void Reopen(Stream source) + { + if ( source == null ) { + throw new ZipException("Failed to reopen archive - no source"); + } + + isNewArchive_ = false; + baseStream_ = source; + ReadEntries(); + } + + void Reopen() + { + if (Name == null) { + throw new InvalidOperationException("Name is not known cannot Reopen"); + } + + Reopen(File.Open(Name, FileMode.Open, FileAccess.Read, FileShare.Read)); + } + + void UpdateCommentOnly() + { + long baseLength = baseStream_.Length; + + ZipHelperStream updateFile = null; + + if ( archiveStorage_.UpdateMode == FileUpdateMode.Safe ) { + Stream copyStream = archiveStorage_.MakeTemporaryCopy(baseStream_); + updateFile = new ZipHelperStream(copyStream); + updateFile.IsStreamOwner = true; + + baseStream_.Close(); + baseStream_ = null; + } + else { + if (archiveStorage_.UpdateMode == FileUpdateMode.Direct) { + // TODO: archiveStorage wasnt originally intended for this use. + // Need to revisit this to tidy up handling as archive storage currently doesnt + // handle the original stream well. + // The problem is when using an existing zip archive with an in memory archive storage. + // The open stream wont support writing but the memory storage should open the same file not an in memory one. + + // Need to tidy up the archive storage interface and contract basically. + baseStream_ = archiveStorage_.OpenForDirectUpdate(baseStream_); + updateFile = new ZipHelperStream(baseStream_); + } + else { + baseStream_.Close(); + baseStream_ = null; + updateFile = new ZipHelperStream(Name); + } + } + + using ( updateFile ) { + long locatedCentralDirOffset = + updateFile.LocateBlockWithSignature(ZipConstants.EndOfCentralDirectorySignature, + baseLength, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + if ( locatedCentralDirOffset < 0 ) { + throw new ZipException("Cannot find central directory"); + } + + const int CentralHeaderCommentSizeOffset = 16; + updateFile.Position += CentralHeaderCommentSizeOffset; + + byte[] rawComment = newComment_.RawComment; + + updateFile.WriteLEShort(rawComment.Length); + updateFile.Write(rawComment, 0, rawComment.Length); + updateFile.SetLength(updateFile.Position); + } + + if ( archiveStorage_.UpdateMode == FileUpdateMode.Safe ) { + Reopen(archiveStorage_.ConvertTemporaryToFinal()); + } + else { + ReadEntries(); + } + } + + /// + /// Class used to sort updates. + /// + class UpdateComparer : IComparer + { + /// + /// Compares two objects and returns a value indicating whether one is + /// less than, equal to or greater than the other. + /// + /// First object to compare + /// Second object to compare. + /// Compare result. + public int Compare( + object x, + object y) + { + ZipUpdate zx = x as ZipUpdate; + ZipUpdate zy = y as ZipUpdate; + + int result; + + if (zx == null) { + if (zy == null) { + result = 0; + } + else { + result = -1; + } + } + else if (zy == null) { + result = 1; + } + else { + int xCmdValue = ((zx.Command == UpdateCommand.Copy) || (zx.Command == UpdateCommand.Modify)) ? 0 : 1; + int yCmdValue = ((zy.Command == UpdateCommand.Copy) || (zy.Command == UpdateCommand.Modify)) ? 0 : 1; + + result = xCmdValue - yCmdValue; + if (result == 0) { + long offsetDiff = zx.Entry.Offset - zy.Entry.Offset; + if (offsetDiff < 0) { + result = -1; + } + else if (offsetDiff == 0) { + result = 0; + } + else { + result = 1; + } + } + } + return result; + } + } + + void RunUpdates() + { + long sizeEntries = 0; + long endOfStream = 0; + bool directUpdate = false; + long destinationPosition = 0; // NOT SFX friendly + + ZipFile workFile; + + if ( IsNewArchive ) { + workFile = this; + workFile.baseStream_.Position = 0; + directUpdate = true; + } + else if ( archiveStorage_.UpdateMode == FileUpdateMode.Direct ) { + workFile = this; + workFile.baseStream_.Position = 0; + directUpdate = true; + + // Sort the updates by offset within copies/modifies, then adds. + // This ensures that data required by copies will not be overwritten. + updates_.Sort(new UpdateComparer()); + } + else { + workFile = ZipFile.Create(archiveStorage_.GetTemporaryOutput()); + workFile.UseZip64 = UseZip64; + + if (key != null) { + workFile.key = (byte[])key.Clone(); + } + } + + try { + foreach ( ZipUpdate update in updates_ ) { + if (update != null) { + switch (update.Command) { + case UpdateCommand.Copy: + if (directUpdate) { + CopyEntryDirect(workFile, update, ref destinationPosition); + } + else { + CopyEntry(workFile, update); + } + break; + + case UpdateCommand.Modify: + // TODO: Direct modifying of an entry will take some legwork. + ModifyEntry(workFile, update); + break; + + case UpdateCommand.Add: + if (!IsNewArchive && directUpdate) { + workFile.baseStream_.Position = destinationPosition; + } + + AddEntry(workFile, update); + + if (directUpdate) { + destinationPosition = workFile.baseStream_.Position; + } + break; + } + } + } + + if ( !IsNewArchive && directUpdate ) { + workFile.baseStream_.Position = destinationPosition; + } + + long centralDirOffset = workFile.baseStream_.Position; + + foreach ( ZipUpdate update in updates_ ) { + if (update != null) { + sizeEntries += workFile.WriteCentralDirectoryHeader(update.OutEntry); + } + } + + byte[] theComment = (newComment_ != null) ? newComment_.RawComment : ZipConstants.ConvertToArray(comment_); + using ( ZipHelperStream zhs = new ZipHelperStream(workFile.baseStream_) ) { + zhs.WriteEndOfCentralDirectory(updateCount_, sizeEntries, centralDirOffset, theComment); + } + + endOfStream = workFile.baseStream_.Position; + + // And now patch entries... + foreach ( ZipUpdate update in updates_ ) { + if (update != null) + { + // If the size of the entry is zero leave the crc as 0 as well. + // The calculated crc will be all bits on... + if ((update.CrcPatchOffset > 0) && (update.OutEntry.CompressedSize > 0)) { + workFile.baseStream_.Position = update.CrcPatchOffset; + workFile.WriteLEInt((int)update.OutEntry.Crc); + } + + if (update.SizePatchOffset > 0) { + workFile.baseStream_.Position = update.SizePatchOffset; + if (update.OutEntry.LocalHeaderRequiresZip64) { + workFile.WriteLeLong(update.OutEntry.Size); + workFile.WriteLeLong(update.OutEntry.CompressedSize); + } + else { + workFile.WriteLEInt((int)update.OutEntry.CompressedSize); + workFile.WriteLEInt((int)update.OutEntry.Size); + } + } + } + } + } + catch { + workFile.Close(); + if (!directUpdate && (workFile.Name != null)) { + File.Delete(workFile.Name); + } + throw; + } + + if (directUpdate) { + workFile.baseStream_.SetLength(endOfStream); + workFile.baseStream_.Flush(); + isNewArchive_ = false; + ReadEntries(); + } + else { + baseStream_.Close(); + Reopen(archiveStorage_.ConvertTemporaryToFinal()); + } + } + + void CheckUpdating() + { + if ( updates_ == null ) { + throw new InvalidOperationException("BeginUpdate has not been called"); + } + } + + #endregion + + #region ZipUpdate class + /// + /// Represents a pending update to a Zip file. + /// + class ZipUpdate + { + #region Constructors + public ZipUpdate(string fileName, ZipEntry entry) + { + command_ = UpdateCommand.Add; + entry_ = entry; + filename_ = fileName; + } + + [Obsolete] + public ZipUpdate(string fileName, string entryName, CompressionMethod compressionMethod) + { + command_ = UpdateCommand.Add; + entry_ = new ZipEntry(entryName); + entry_.CompressionMethod = compressionMethod; + filename_ = fileName; + } + + [Obsolete] + public ZipUpdate(string fileName, string entryName) + : this(fileName, entryName, CompressionMethod.Deflated) + { + // Do nothing. + } + + [Obsolete] + public ZipUpdate(IStaticDataSource dataSource, string entryName, CompressionMethod compressionMethod) + { + command_ = UpdateCommand.Add; + entry_ = new ZipEntry(entryName); + entry_.CompressionMethod = compressionMethod; + dataSource_ = dataSource; + } + + public ZipUpdate(IStaticDataSource dataSource, ZipEntry entry) + { + command_ = UpdateCommand.Add; + entry_ = entry; + dataSource_ = dataSource; + } + + public ZipUpdate(ZipEntry original, ZipEntry updated) + { + throw new ZipException("Modify not currently supported"); + /* + command_ = UpdateCommand.Modify; + entry_ = ( ZipEntry )original.Clone(); + outEntry_ = ( ZipEntry )updated.Clone(); + */ + } + + public ZipUpdate(UpdateCommand command, ZipEntry entry) + { + command_ = command; + entry_ = ( ZipEntry )entry.Clone(); + } + + + /// + /// Copy an existing entry. + /// + /// The existing entry to copy. + public ZipUpdate(ZipEntry entry) + : this(UpdateCommand.Copy, entry) + { + // Do nothing. + } + #endregion + + /// + /// Get the for this update. + /// + /// This is the source or original entry. + public ZipEntry Entry + { + get { return entry_; } + } + + /// + /// Get the that will be written to the updated/new file. + /// + public ZipEntry OutEntry + { + get { + if ( outEntry_ == null ) { + outEntry_ = (ZipEntry)entry_.Clone(); + } + + return outEntry_; + } + } + + /// + /// Get the command for this update. + /// + public UpdateCommand Command + { + get { return command_; } + } + + /// + /// Get the filename if any for this update. Null if none exists. + /// + public string Filename + { + get { return filename_; } + } + + /// + /// Get/set the location of the size patch for this update. + /// + public long SizePatchOffset + { + get { return sizePatchOffset_; } + set { sizePatchOffset_ = value; } + } + + /// + /// Get /set the location of the crc patch for this update. + /// + public long CrcPatchOffset + { + get { return crcPatchOffset_; } + set { crcPatchOffset_ = value; } + } + + /// + /// Get/set the size calculated by offset. + /// Specifically, the difference between this and next entry's starting offset. + /// + public long OffsetBasedSize + { + get { return _offsetBasedSize; } + set { _offsetBasedSize = value; } + } + + public Stream GetSource() + { + Stream result = null; + if ( dataSource_ != null ) { + result = dataSource_.GetSource(); + } + + return result; + } + + #region Instance Fields + ZipEntry entry_; + ZipEntry outEntry_; + UpdateCommand command_; + IStaticDataSource dataSource_; + string filename_; + long sizePatchOffset_ = -1; + long crcPatchOffset_ = -1; + long _offsetBasedSize = -1; + #endregion + } + + #endregion + #endregion + + #region Disposing + + #region IDisposable Members + void IDisposable.Dispose() + { + Close(); + } + #endregion + + void DisposeInternal(bool disposing) + { + if ( !isDisposed_ ) { + isDisposed_ = true; + entries_ = new ZipEntry[0]; + + if ( IsStreamOwner && (baseStream_ != null) ) { + lock(baseStream_) { + baseStream_.Close(); + } + } + + PostUpdateCleanup(); + } + } + + /// + /// Releases the unmanaged resources used by the this instance and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; + /// false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + DisposeInternal(disposing); + } + + #endregion + + #region Internal routines + #region Reading + /// + /// Read an unsigned short in little endian byte order. + /// + /// Returns the value read. + /// + /// The stream ends prematurely + /// + ushort ReadLEUshort() + { + int data1 = baseStream_.ReadByte(); + + if ( data1 < 0 ) { + throw new EndOfStreamException("End of stream"); + } + + int data2 = baseStream_.ReadByte(); + + if ( data2 < 0 ) { + throw new EndOfStreamException("End of stream"); + } + + + return unchecked((ushort)((ushort)data1 | (ushort)(data2 << 8))); + } + + /// + /// Read a uint in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + uint ReadLEUint() + { + return (uint)(ReadLEUshort() | (ReadLEUshort() << 16)); + } + + ulong ReadLEUlong() + { + return ReadLEUint() | ((ulong)ReadLEUint() << 32); + } + + #endregion + // NOTE this returns the offset of the first byte after the signature. + long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + { + using ( ZipHelperStream les = new ZipHelperStream(baseStream_) ) { + return les.LocateBlockWithSignature(signature, endLocation, minimumBlockSize, maximumVariableData); + } + } + + /// + /// Search for and read the central directory of a zip file filling the entries array. + /// + /// + /// An i/o error occurs. + /// + /// + /// The central directory is malformed or cannot be found + /// + void ReadEntries() + { + // Search for the End Of Central Directory. When a zip comment is + // present the directory will start earlier + // + // The search is limited to 64K which is the maximum size of a trailing comment field to aid speed. + // This should be compatible with both SFX and ZIP files but has only been tested for Zip files + // If a SFX file has the Zip data attached as a resource and there are other resources occuring later then + // this could be invalid. + // Could also speed this up by reading memory in larger blocks. + + if (baseStream_.CanSeek == false) { + throw new ZipException("ZipFile stream must be seekable"); + } + + long locatedEndOfCentralDir = LocateBlockWithSignature(ZipConstants.EndOfCentralDirectorySignature, + baseStream_.Length, ZipConstants.EndOfCentralRecordBaseSize, 0xffff); + + if (locatedEndOfCentralDir < 0) { + throw new ZipException("Cannot find central directory"); + } + + // Read end of central directory record + ushort thisDiskNumber = ReadLEUshort(); + ushort startCentralDirDisk = ReadLEUshort(); + ulong entriesForThisDisk = ReadLEUshort(); + ulong entriesForWholeCentralDir = ReadLEUshort(); + ulong centralDirSize = ReadLEUint(); + long offsetOfCentralDir = ReadLEUint(); + uint commentSize = ReadLEUshort(); + + if ( commentSize > 0 ) { + byte[] comment = new byte[commentSize]; + + StreamUtils.ReadFully(baseStream_, comment); + comment_ = ZipConstants.ConvertToString(comment); + } + else { + comment_ = string.Empty; + } + + bool isZip64 = false; + + // Check if zip64 header information is required. + if ( (thisDiskNumber == 0xffff) || + (startCentralDirDisk == 0xffff) || + (entriesForThisDisk == 0xffff) || + (entriesForWholeCentralDir == 0xffff) || + (centralDirSize == 0xffffffff) || + (offsetOfCentralDir == 0xffffffff) ) { + isZip64 = true; + + long offset = LocateBlockWithSignature(ZipConstants.Zip64CentralDirLocatorSignature, locatedEndOfCentralDir, 0, 0x1000); + if ( offset < 0 ) { + throw new ZipException("Cannot find Zip64 locator"); + } + + // number of the disk with the start of the zip64 end of central directory 4 bytes + // relative offset of the zip64 end of central directory record 8 bytes + // total number of disks 4 bytes + ReadLEUint(); // startDisk64 is not currently used + ulong offset64 = ReadLEUlong(); + uint totalDisks = ReadLEUint(); + + baseStream_.Position = (long)offset64; + long sig64 = ReadLEUint(); + + if ( sig64 != ZipConstants.Zip64CentralFileHeaderSignature ) { + throw new ZipException(string.Format("Invalid Zip64 Central directory signature at {0:X}", offset64)); + } + + // NOTE: Record size = SizeOfFixedFields + SizeOfVariableData - 12. + ulong recordSize = ReadLEUlong(); + int versionMadeBy = ReadLEUshort(); + int versionToExtract = ReadLEUshort(); + uint thisDisk = ReadLEUint(); + uint centralDirDisk = ReadLEUint(); + entriesForThisDisk = ReadLEUlong(); + entriesForWholeCentralDir = ReadLEUlong(); + centralDirSize = ReadLEUlong(); + offsetOfCentralDir = (long)ReadLEUlong(); + + // NOTE: zip64 extensible data sector (variable size) is ignored. + } + + entries_ = new ZipEntry[entriesForThisDisk]; + + // SFX/embedded support, find the offset of the first entry vis the start of the stream + // This applies to Zip files that are appended to the end of an SFX stub. + // Or are appended as a resource to an executable. + // Zip files created by some archivers have the offsets altered to reflect the true offsets + // and so dont require any adjustment here... + // TODO: Difficulty with Zip64 and SFX offset handling needs resolution - maths? + if ( !isZip64 && (offsetOfCentralDir < locatedEndOfCentralDir - (4 + (long)centralDirSize)) ) { + offsetOfFirstEntry = locatedEndOfCentralDir - (4 + (long)centralDirSize + offsetOfCentralDir); + if (offsetOfFirstEntry <= 0) { + throw new ZipException("Invalid embedded zip archive"); + } + } + + baseStream_.Seek(offsetOfFirstEntry + offsetOfCentralDir, SeekOrigin.Begin); + + for (ulong i = 0; i < entriesForThisDisk; i++) { + if (ReadLEUint() != ZipConstants.CentralHeaderSignature) { + throw new ZipException("Wrong Central Directory signature"); + } + + int versionMadeBy = ReadLEUshort(); + int versionToExtract = ReadLEUshort(); + int bitFlags = ReadLEUshort(); + int method = ReadLEUshort(); + uint dostime = ReadLEUint(); + uint crc = ReadLEUint(); + long csize = (long)ReadLEUint(); + long size = (long)ReadLEUint(); + int nameLen = ReadLEUshort(); + int extraLen = ReadLEUshort(); + int commentLen = ReadLEUshort(); + + int diskStartNo = ReadLEUshort(); // Not currently used + int internalAttributes = ReadLEUshort(); // Not currently used + + uint externalAttributes = ReadLEUint(); + long offset = ReadLEUint(); + + byte[] buffer = new byte[Math.Max(nameLen, commentLen)]; + + StreamUtils.ReadFully(baseStream_, buffer, 0, nameLen); + string name = ZipConstants.ConvertToStringExt(bitFlags, buffer, nameLen); + + ZipEntry entry = new ZipEntry(name, versionToExtract, versionMadeBy, (CompressionMethod)method); + entry.Crc = crc & 0xffffffffL; + entry.Size = size & 0xffffffffL; + entry.CompressedSize = csize & 0xffffffffL; + entry.Flags = bitFlags; + entry.DosTime = (uint)dostime; + entry.ZipFileIndex = (long)i; + entry.Offset = offset; + entry.ExternalFileAttributes = (int)externalAttributes; + + if ((bitFlags & 8) == 0) { + entry.CryptoCheckValue = (byte)(crc >> 24); + } + else { + entry.CryptoCheckValue = (byte)((dostime >> 8) & 0xff); + } + + if (extraLen > 0) { + byte[] extra = new byte[extraLen]; + StreamUtils.ReadFully(baseStream_, extra); + entry.ExtraData = extra; + } + + entry.ProcessExtraData(false); + + if (commentLen > 0) { + StreamUtils.ReadFully(baseStream_, buffer, 0, commentLen); + entry.Comment = ZipConstants.ConvertToStringExt(bitFlags, buffer, commentLen); + } + + entries_[i] = entry; + } + } + + /// + /// Locate the data for a given entry. + /// + /// + /// The start offset of the data. + /// + /// + /// The stream ends prematurely + /// + /// + /// The local header signature is invalid, the entry and central header file name lengths are different + /// or the local and entry compression methods dont match + /// + long LocateEntry(ZipEntry entry) + { + return TestLocalHeader(entry, HeaderTest.Extract); + } + +#if !NETCF_1_0 + Stream CreateAndInitDecryptionStream(Stream baseStream, ZipEntry entry) + { + CryptoStream result = null; + + if ( (entry.Version < ZipConstants.VersionStrongEncryption) + || (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0) { + PkzipClassicManaged classicManaged = new PkzipClassicManaged(); + + OnKeysRequired(entry.Name); + if (HaveKeys == false) { + throw new ZipException("No password available for encrypted stream"); + } + + result = new CryptoStream(baseStream, classicManaged.CreateDecryptor(key, null), CryptoStreamMode.Read); + CheckClassicPassword(result, entry); + } + else { +#if !NET_1_1 && !NETCF_2_0 + if (entry.Version == ZipConstants.VERSION_AES) { + // + OnKeysRequired(entry.Name); + if (HaveKeys == false) { + throw new ZipException("No password available for AES encrypted stream"); + } + int saltLen = entry.AESSaltLen; + byte[] saltBytes = new byte[saltLen]; + int saltIn = baseStream.Read(saltBytes, 0, saltLen); + if (saltIn != saltLen) + throw new ZipException("AES Salt expected " + saltLen + " got " + saltIn); + // + byte[] pwdVerifyRead = new byte[2]; + baseStream.Read(pwdVerifyRead, 0, 2); + int blockSize = entry.AESKeySize / 8; // bits to bytes + + ZipAESTransform decryptor = new ZipAESTransform(rawPassword_, saltBytes, blockSize, false); + byte[] pwdVerifyCalc = decryptor.PwdVerifier; + if (pwdVerifyCalc[0] != pwdVerifyRead[0] || pwdVerifyCalc[1] != pwdVerifyRead[1]) + throw new Exception("Invalid password for AES"); + result = new ZipAESStream(baseStream, decryptor, CryptoStreamMode.Read); + } + else +#endif + { + throw new ZipException("Decryption method not supported"); + } + } + + return result; + } + + Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry) + { + CryptoStream result = null; + if ( (entry.Version < ZipConstants.VersionStrongEncryption) + || (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0) { + PkzipClassicManaged classicManaged = new PkzipClassicManaged(); + + OnKeysRequired(entry.Name); + if (HaveKeys == false) { + throw new ZipException("No password available for encrypted stream"); + } + + // Closing a CryptoStream will close the base stream as well so wrap it in an UncompressedStream + // which doesnt do this. + result = new CryptoStream(new UncompressedStream(baseStream), + classicManaged.CreateEncryptor(key, null), CryptoStreamMode.Write); + + if ( (entry.Crc < 0) || (entry.Flags & 8) != 0) { + WriteEncryptionHeader(result, entry.DosTime << 16); + } + else { + WriteEncryptionHeader(result, entry.Crc); + } + } + return result; + } + + static void CheckClassicPassword(CryptoStream classicCryptoStream, ZipEntry entry) + { + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + StreamUtils.ReadFully(classicCryptoStream, cryptbuffer); + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) { + throw new ZipException("Invalid password"); + } + } +#endif + + static void WriteEncryptionHeader(Stream stream, long crcValue) + { + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + Random rnd = new Random(); + rnd.NextBytes(cryptBuffer); + cryptBuffer[11] = (byte)(crcValue >> 24); + stream.Write(cryptBuffer, 0, cryptBuffer.Length); + } + + #endregion + + #region Instance Fields + bool isDisposed_; + string name_; + string comment_; + string rawPassword_; + Stream baseStream_; + bool isStreamOwner; + long offsetOfFirstEntry; + ZipEntry[] entries_; + byte[] key; + bool isNewArchive_; + + // Default is dynamic which is not backwards compatible and can cause problems + // with XP's built in compression which cant read Zip64 archives. + // However it does avoid the situation were a large file is added and cannot be completed correctly. + // Hint: Set always ZipEntry size before they are added to an archive and this setting isnt needed. + UseZip64 useZip64_ = UseZip64.Dynamic ; + + #region Zip Update Instance Fields + ArrayList updates_; + long updateCount_; // Count is managed manually as updates_ can contain nulls! + Hashtable updateIndex_; + IArchiveStorage archiveStorage_; + IDynamicDataSource updateDataSource_; + bool contentsEdited_; + int bufferSize_ = DefaultBufferSize; + byte[] copyBuffer_; + ZipString newComment_; + bool commentEdited_; + IEntryFactory updateEntryFactory_ = new ZipEntryFactory(); + #endregion + #endregion + + #region Support Classes + /// + /// Represents a string from a which is stored as an array of bytes. + /// + class ZipString + { + #region Constructors + /// + /// Initialise a with a string. + /// + /// The textual string form. + public ZipString(string comment) + { + comment_ = comment; + isSourceString_ = true; + } + + /// + /// Initialise a using a string in its binary 'raw' form. + /// + /// + public ZipString(byte[] rawString) + { + rawComment_ = rawString; + } + #endregion + + /// + /// Get a value indicating the original source of data for this instance. + /// True if the source was a string; false if the source was binary data. + /// + public bool IsSourceString + { + get { return isSourceString_; } + } + + /// + /// Get the length of the comment when represented as raw bytes. + /// + public int RawLength + { + get { + MakeBytesAvailable(); + return rawComment_.Length; + } + } + + /// + /// Get the comment in its 'raw' form as plain bytes. + /// + public byte[] RawComment + { + get { + MakeBytesAvailable(); + return (byte[])rawComment_.Clone(); + } + } + + /// + /// Reset the comment to its initial state. + /// + public void Reset() + { + if ( isSourceString_ ) { + rawComment_ = null; + } + else { + comment_ = null; + } + } + + void MakeTextAvailable() + { + if ( comment_ == null ) { + comment_ = ZipConstants.ConvertToString(rawComment_); + } + } + + void MakeBytesAvailable() + { + if ( rawComment_ == null ) { + rawComment_ = ZipConstants.ConvertToArray(comment_); + } + } + + /// + /// Implicit conversion of comment to a string. + /// + /// The to convert to a string. + /// The textual equivalent for the input value. + static public implicit operator string(ZipString zipString) + { + zipString.MakeTextAvailable(); + return zipString.comment_; + } + + #region Instance Fields + string comment_; + byte[] rawComment_; + bool isSourceString_; + #endregion + } + + /// + /// An enumerator for Zip entries + /// + class ZipEntryEnumerator : IEnumerator + { + #region Constructors + public ZipEntryEnumerator(ZipEntry[] entries) + { + array = entries; + } + + #endregion + #region IEnumerator Members + public object Current + { + get { + return array[index]; + } + } + + public void Reset() + { + index = -1; + } + + public bool MoveNext() + { + return (++index < array.Length); + } + #endregion + #region Instance Fields + ZipEntry[] array; + int index = -1; + #endregion + } + + /// + /// An is a stream that you can write uncompressed data + /// to and flush, but cannot read, seek or do anything else to. + /// + class UncompressedStream : Stream + { + #region Constructors + public UncompressedStream(Stream baseStream) + { + baseStream_ = baseStream; + } + + #endregion + + /// + /// Close this stream instance. + /// + public override void Close() + { + // Do nothing + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + public override bool CanRead + { + get { + return false; + } + } + + /// + /// Write any buffered data to underlying storage. + /// + public override void Flush() + { + baseStream_.Flush(); + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + public override bool CanWrite + { + get { + return baseStream_.CanWrite; + } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + public override bool CanSeek + { + get { + return false; + } + } + + /// + /// Get the length in bytes of the stream. + /// + public override long Length + { + get { + return 0; + } + } + + /// + /// Gets or sets the position within the current stream. + /// + public override long Position + { + get { + return baseStream_.Position; + } + + set + { + } + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + return 0; + } + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + return 0; + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + baseStream_.Write(buffer, offset, count); + } + + #region Instance Fields + Stream baseStream_; + #endregion + } + + /// + /// A is an + /// whose data is only a part or subsection of a file. + /// + class PartialInputStream : Stream + { + #region Constructors + /// + /// Initialise a new instance of the class. + /// + /// The containing the underlying stream to use for IO. + /// The start of the partial data. + /// The length of the partial data. + public PartialInputStream(ZipFile zipFile, long start, long length) + { + start_ = start; + length_ = length; + + // Although this is the only time the zipfile is used + // keeping a reference here prevents premature closure of + // this zip file and thus the baseStream_. + + // Code like this will cause apparently random failures depending + // on the size of the files and when garbage is collected. + // + // ZipFile z = new ZipFile (stream); + // Stream reader = z.GetInputStream(0); + // uses reader here.... + zipFile_ = zipFile; + baseStream_ = zipFile_.baseStream_; + readPos_ = start; + end_ = start + length; + } + #endregion + + /// + /// Read a byte from this stream. + /// + /// Returns the byte read or -1 on end of stream. + public override int ReadByte() + { + if (readPos_ >= end_) { + // -1 is the correct value at end of stream. + return -1; + } + + lock( baseStream_ ) { + baseStream_.Seek(readPos_++, SeekOrigin.Begin); + return baseStream_.ReadByte(); + } + } + + /// + /// Close this partial input stream. + /// + /// + /// The underlying stream is not closed. Close the parent ZipFile class to do that. + /// + public override void Close() + { + // Do nothing at all! + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + lock(baseStream_) { + if (count > end_ - readPos_) { + count = (int) (end_ - readPos_); + if (count == 0) { + return 0; + } + } + + baseStream_.Seek(readPos_, SeekOrigin.Begin); + int readCount = baseStream_.Read(buffer, offset, count); + if (readCount > 0) { + readPos_ += readCount; + } + return readCount; + } + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = readPos_; + + switch ( origin ) + { + case SeekOrigin.Begin: + newPos = start_ + offset; + break; + + case SeekOrigin.Current: + newPos = readPos_ + offset; + break; + + case SeekOrigin.End: + newPos = end_ + offset; + break; + } + + if ( newPos < start_ ) { + throw new ArgumentException("Negative position is invalid"); + } + + if ( newPos >= end_ ) { + throw new IOException("Cannot seek past end"); + } + readPos_ = newPos; + return readPos_; + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + /// An I/O error occurs. + public override void Flush() + { + // Nothing to do. + } + + /// + /// Gets or sets the position within the current stream. + /// + /// + /// The current position within the stream. + /// An I/O error occurs. + /// The stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Position { + get { return readPos_ - start_; } + set { + long newPos = start_ + value; + + if ( newPos < start_ ) { + throw new ArgumentException("Negative position is invalid"); + } + + if ( newPos >= end_ ) { + throw new InvalidOperationException("Cannot seek past end"); + } + readPos_ = newPos; + } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// + /// A long value representing the length of the stream in bytes. + /// A class derived from Stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Length { + get { return length_; } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// false + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite { + get { return false; } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// true + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek { + get { return true; } + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true. + /// true if the stream supports reading; otherwise, false. + public override bool CanRead { + get { return true; } + } + +#if !NET_1_0 && !NET_1_1 && !NETCF_1_0 + /// + /// Gets a value that determines whether the current stream can time out. + /// + /// + /// A value that determines whether the current stream can time out. + public override bool CanTimeout { + get { return baseStream_.CanTimeout; } + } +#endif + #region Instance Fields + ZipFile zipFile_; + Stream baseStream_; + long start_; + long length_; + long readPos_; + long end_; + #endregion + } + #endregion + } + + #endregion + + #region DataSources + /// + /// Provides a static way to obtain a source of data for an entry. + /// + public interface IStaticDataSource + { + /// + /// Get a source of data by creating a new stream. + /// + /// Returns a to use for compression input. + /// Ideally a new stream is created and opened to achieve this, to avoid locking problems. + Stream GetSource(); + } + + /// + /// Represents a source of data that can dynamically provide + /// multiple data sources based on the parameters passed. + /// + public interface IDynamicDataSource + { + /// + /// Get a data source. + /// + /// The to get a source for. + /// The name for data if known. + /// Returns a to use for compression input. + /// Ideally a new stream is created and opened to achieve this, to avoid locking problems. + Stream GetSource(ZipEntry entry, string name); + } + + /// + /// Default implementation of a for use with files stored on disk. + /// + public class StaticDiskDataSource : IStaticDataSource + { + /// + /// Initialise a new instnace of + /// + /// The name of the file to obtain data from. + public StaticDiskDataSource(string fileName) + { + fileName_ = fileName; + } + + #region IDataSource Members + + /// + /// Get a providing data. + /// + /// Returns a provising data. + public Stream GetSource() + { + return File.Open(fileName_, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + #endregion + #region Instance Fields + string fileName_; + #endregion + } + + + /// + /// Default implementation of for files stored on disk. + /// + public class DynamicDiskDataSource : IDynamicDataSource + { + /// + /// Initialise a default instance of . + /// + public DynamicDiskDataSource() + { + } + + #region IDataSource Members + /// + /// Get a providing data for an entry. + /// + /// The entry to provide data for. + /// The file name for data if known. + /// Returns a stream providing data; or null if not available + public Stream GetSource(ZipEntry entry, string name) + { + Stream result = null; + + if ( name != null ) { + result = File.Open(name, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + return result; + } + + #endregion + } + + #endregion + + #region Archive Storage + /// + /// Defines facilities for data storage when updating Zip Archives. + /// + public interface IArchiveStorage + { + /// + /// Get the to apply during updates. + /// + FileUpdateMode UpdateMode { get; } + + /// + /// Get an empty that can be used for temporary output. + /// + /// Returns a temporary output + /// + Stream GetTemporaryOutput(); + + /// + /// Convert a temporary output stream to a final stream. + /// + /// The resulting final + /// + Stream ConvertTemporaryToFinal(); + + /// + /// Make a temporary copy of the original stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + Stream MakeTemporaryCopy(Stream stream); + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The current stream. + /// Returns a stream suitable for direct updating. + /// This may be the current stream passed. + Stream OpenForDirectUpdate(Stream stream); + + /// + /// Dispose of this instance. + /// + void Dispose(); + } + + /// + /// An abstract suitable for extension by inheritance. + /// + abstract public class BaseArchiveStorage : IArchiveStorage + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The update mode. + protected BaseArchiveStorage(FileUpdateMode updateMode) + { + updateMode_ = updateMode; + } + #endregion + + #region IArchiveStorage Members + + /// + /// Gets a temporary output + /// + /// Returns the temporary output stream. + /// + public abstract Stream GetTemporaryOutput(); + + /// + /// Converts the temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + /// + public abstract Stream ConvertTemporaryToFinal(); + + /// + /// Make a temporary copy of a . + /// + /// The to make a copy of. + /// Returns a temporary output that is a copy of the input. + public abstract Stream MakeTemporaryCopy(Stream stream); + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The to open for direct update. + /// Returns a stream suitable for direct updating. + public abstract Stream OpenForDirectUpdate(Stream stream); + + /// + /// Disposes this instance. + /// + public abstract void Dispose(); + + /// + /// Gets the update mode applicable. + /// + /// The update mode. + public FileUpdateMode UpdateMode + { + get { + return updateMode_; + } + } + + #endregion + + #region Instance Fields + FileUpdateMode updateMode_; + #endregion + } + + /// + /// An implementation suitable for hard disks. + /// + public class DiskArchiveStorage : BaseArchiveStorage + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The file. + /// The update mode. + public DiskArchiveStorage(ZipFile file, FileUpdateMode updateMode) + : base(updateMode) + { + if ( file.Name == null ) { + throw new ZipException("Cant handle non file archives"); + } + + fileName_ = file.Name; + } + + /// + /// Initializes a new instance of the class. + /// + /// The file. + public DiskArchiveStorage(ZipFile file) + : this(file, FileUpdateMode.Safe) + { + } + #endregion + + #region IArchiveStorage Members + + /// + /// Gets a temporary output for performing updates on. + /// + /// Returns the temporary output stream. + public override Stream GetTemporaryOutput() + { + if ( temporaryName_ != null ) { + temporaryName_ = GetTempFileName(temporaryName_, true); + temporaryStream_ = File.Open(temporaryName_, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + } + else { + // Determine where to place files based on internal strategy. + // Currently this is always done in system temp directory. + temporaryName_ = Path.GetTempFileName(); + temporaryStream_ = File.Open(temporaryName_, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); + } + + return temporaryStream_; + } + + /// + /// Converts a temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + public override Stream ConvertTemporaryToFinal() + { + if ( temporaryStream_ == null ) { + throw new ZipException("No temporary stream has been created"); + } + + Stream result = null; + + string moveTempName = GetTempFileName(fileName_, false); + bool newFileCreated = false; + + try { + temporaryStream_.Close(); + File.Move(fileName_, moveTempName); + File.Move(temporaryName_, fileName_); + newFileCreated = true; + File.Delete(moveTempName); + + result = File.Open(fileName_, FileMode.Open, FileAccess.Read, FileShare.Read); + } + catch(Exception) { + result = null; + + // Try to roll back changes... + if ( !newFileCreated ) { + File.Move(moveTempName, fileName_); + File.Delete(temporaryName_); + } + + throw; + } + + return result; + } + + /// + /// Make a temporary copy of a stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + public override Stream MakeTemporaryCopy(Stream stream) + { + stream.Close(); + + temporaryName_ = GetTempFileName(fileName_, true); + File.Copy(fileName_, temporaryName_, true); + + temporaryStream_ = new FileStream(temporaryName_, + FileMode.Open, + FileAccess.ReadWrite); + return temporaryStream_; + } + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The current stream. + /// Returns a stream suitable for direct updating. + /// If the stream is not null this is used as is. + public override Stream OpenForDirectUpdate(Stream stream) + { + Stream result; + if ((stream == null) || !stream.CanWrite) + { + if (stream != null) { + stream.Close(); + } + + result = new FileStream(fileName_, + FileMode.Open, + FileAccess.ReadWrite); + } + else + { + result = stream; + } + + return result; + } + + /// + /// Disposes this instance. + /// + public override void Dispose() + { + if ( temporaryStream_ != null ) { + temporaryStream_.Close(); + } + } + + #endregion + + #region Internal routines + static string GetTempFileName(string original, bool makeTempFile) + { + string result = null; + + if ( original == null ) { + result = Path.GetTempFileName(); + } + else { + int counter = 0; + int suffixSeed = DateTime.Now.Second; + + while ( result == null ) { + counter += 1; + string newName = string.Format("{0}.{1}{2}.tmp", original, suffixSeed, counter); + if ( !File.Exists(newName) ) { + if ( makeTempFile) { + try { + // Try and create the file. + using ( FileStream stream = File.Create(newName) ) { + } + result = newName; + } + catch { + suffixSeed = DateTime.Now.Second; + } + } + else { + result = newName; + } + } + } + } + return result; + } + #endregion + + #region Instance Fields + Stream temporaryStream_; + string fileName_; + string temporaryName_; + #endregion + } + + /// + /// An implementation suitable for in memory streams. + /// + public class MemoryArchiveStorage : BaseArchiveStorage + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public MemoryArchiveStorage() + : base(FileUpdateMode.Direct) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to use + /// This constructor is for testing as memory streams dont really require safe mode. + public MemoryArchiveStorage(FileUpdateMode updateMode) + : base(updateMode) + { + } + + #endregion + + #region Properties + /// + /// Get the stream returned by if this was in fact called. + /// + public MemoryStream FinalStream + { + get { return finalStream_; } + } + + #endregion + + #region IArchiveStorage Members + + /// + /// Gets the temporary output + /// + /// Returns the temporary output stream. + public override Stream GetTemporaryOutput() + { + temporaryStream_ = new MemoryStream(); + return temporaryStream_; + } + + /// + /// Converts the temporary to its final form. + /// + /// Returns a that can be used to read + /// the final storage for the archive. + public override Stream ConvertTemporaryToFinal() + { + if ( temporaryStream_ == null ) { + throw new ZipException("No temporary stream has been created"); + } + + finalStream_ = new MemoryStream(temporaryStream_.ToArray()); + return finalStream_; + } + + /// + /// Make a temporary copy of the original stream. + /// + /// The to copy. + /// Returns a temporary output that is a copy of the input. + public override Stream MakeTemporaryCopy(Stream stream) + { + temporaryStream_ = new MemoryStream(); + stream.Position = 0; + StreamUtils.Copy(stream, temporaryStream_, new byte[4096]); + return temporaryStream_; + } + + /// + /// Return a stream suitable for performing direct updates on the original source. + /// + /// The original source stream + /// Returns a stream suitable for direct updating. + /// If the passed is not null this is used; + /// otherwise a new is returned. + public override Stream OpenForDirectUpdate(Stream stream) + { + Stream result; + if ((stream == null) || !stream.CanWrite) { + + result = new MemoryStream(); + + if (stream != null) { + stream.Position = 0; + StreamUtils.Copy(stream, result, new byte[4096]); + + stream.Close(); + } + } + else { + result = stream; + } + + return result; + } + + /// + /// Disposes this instance. + /// + public override void Dispose() + { + if ( temporaryStream_ != null ) { + temporaryStream_.Close(); + } + } + + #endregion + + #region Instance Fields + MemoryStream temporaryStream_; + MemoryStream finalStream_; + #endregion + } + + #endregion +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs new file mode 100644 index 000000000..ed9572f05 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipHelperStream.cs @@ -0,0 +1,623 @@ +// ZipHelperStream.cs +// +// Copyright 2006, 2007 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +using System; +using System.IO; +using System.Text; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + + /// + /// Holds data pertinent to a data descriptor. + /// + public class DescriptorData + { + /// + /// Get /set the compressed size of data. + /// + public long CompressedSize + { + get { return compressedSize; } + set { compressedSize = value; } + } + + /// + /// Get / set the uncompressed size of data + /// + public long Size + { + get { return size; } + set { size = value; } + } + + /// + /// Get /set the crc value. + /// + public long Crc + { + get { return crc; } + set { crc = (value & 0xffffffff); } + } + + #region Instance Fields + long size; + long compressedSize; + long crc; + #endregion + } + + class EntryPatchData + { + public long SizePatchOffset + { + get { return sizePatchOffset_; } + set { sizePatchOffset_ = value; } + } + + public long CrcPatchOffset + { + get { return crcPatchOffset_; } + set { crcPatchOffset_ = value; } + } + + #region Instance Fields + long sizePatchOffset_; + long crcPatchOffset_; + #endregion + } + + /// + /// This class assists with writing/reading from Zip files. + /// + internal class ZipHelperStream : Stream + { + #region Constructors + /// + /// Initialise an instance of this class. + /// + /// The name of the file to open. + public ZipHelperStream(string name) + { + stream_ = new FileStream(name, FileMode.Open, FileAccess.ReadWrite); + isOwner_ = true; + } + + /// + /// Initialise a new instance of . + /// + /// The stream to use. + public ZipHelperStream(Stream stream) + { + stream_ = stream; + } + #endregion + + /// + /// Get / set a value indicating wether the the underlying stream is owned or not. + /// + /// If the stream is owned it is closed when this instance is closed. + public bool IsStreamOwner + { + get { return isOwner_; } + set { isOwner_ = value; } + } + + #region Base Stream Methods + public override bool CanRead + { + get { return stream_.CanRead; } + } + + public override bool CanSeek + { + get { return stream_.CanSeek; } + } + +#if !NET_1_0 && !NET_1_1 && !NETCF_1_0 + public override bool CanTimeout + { + get { return stream_.CanTimeout; } + } +#endif + + public override long Length + { + get { return stream_.Length; } + } + + public override long Position + { + get { return stream_.Position; } + set { stream_.Position = value; } + } + + public override bool CanWrite + { + get { return stream_.CanWrite; } + } + + public override void Flush() + { + stream_.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return stream_.Seek(offset, origin); + } + + public override void SetLength(long value) + { + stream_.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return stream_.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + stream_.Write(buffer, offset, count); + } + + /// + /// Close the stream. + /// + /// + /// The underlying stream is closed only if is true. + /// + override public void Close() + { + Stream toClose = stream_; + stream_ = null; + if (isOwner_ && (toClose != null)) + { + isOwner_ = false; + toClose.Close(); + } + } + #endregion + + // Write the local file header + // TODO: ZipHelperStream.WriteLocalHeader is not yet used and needs checking for ZipFile and ZipOuptutStream usage + void WriteLocalHeader(ZipEntry entry, EntryPatchData patchData) + { + CompressionMethod method = entry.CompressionMethod; + bool headerInfoAvailable = true; // How to get this? + bool patchEntryHeader = false; + + WriteLEInt(ZipConstants.LocalHeaderSignature); + + WriteLEShort(entry.Version); + WriteLEShort(entry.Flags); + WriteLEShort((byte)method); + WriteLEInt((int)entry.DosTime); + + if (headerInfoAvailable == true) { + WriteLEInt((int)entry.Crc); + if ( entry.LocalHeaderRequiresZip64 ) { + WriteLEInt(-1); + WriteLEInt(-1); + } + else { + WriteLEInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); + WriteLEInt((int)entry.Size); + } + } else { + if (patchData != null) { + patchData.CrcPatchOffset = stream_.Position; + } + WriteLEInt(0); // Crc + + if ( patchData != null ) { + patchData.SizePatchOffset = stream_.Position; + } + + // For local header both sizes appear in Zip64 Extended Information + if ( entry.LocalHeaderRequiresZip64 && patchEntryHeader ) { + WriteLEInt(-1); + WriteLEInt(-1); + } + else { + WriteLEInt(0); // Compressed size + WriteLEInt(0); // Uncompressed size + } + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xFFFF) { + throw new ZipException("Entry name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader)) { + ed.StartNewEntry(); + if (headerInfoAvailable) { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + } + else { + ed.AddLeLong(-1); + ed.AddLeLong(-1); + } + ed.AddNewEntry(1); + + if ( !ed.Find(1) ) { + throw new ZipException("Internal error cant find extra data"); + } + + if ( patchData != null ) { + patchData.SizePatchOffset = ed.CurrentReadIndex; + } + } + else { + ed.Delete(1); + } + + byte[] extra = ed.GetEntryData(); + + WriteLEShort(name.Length); + WriteLEShort(extra.Length); + + if ( name.Length > 0 ) { + stream_.Write(name, 0, name.Length); + } + + if ( entry.LocalHeaderRequiresZip64 && patchEntryHeader ) { + patchData.SizePatchOffset += stream_.Position; + } + + if ( extra.Length > 0 ) { + stream_.Write(extra, 0, extra.Length); + } + } + + /// + /// Locates a block with the desired . + /// + /// The signature to find. + /// Location, marking the end of block. + /// Minimum size of the block. + /// The maximum variable data. + /// Eeturns the offset of the first byte after the signature; -1 if not found + public long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize, int maximumVariableData) + { + long pos = endLocation - minimumBlockSize; + if ( pos < 0 ) { + return -1; + } + + long giveUpMarker = Math.Max(pos - maximumVariableData, 0); + + // TODO: This loop could be optimised for speed. + do { + if ( pos < giveUpMarker ) { + return -1; + } + Seek(pos--, SeekOrigin.Begin); + } while ( ReadLEInt() != signature ); + + return Position; + } + + /// + /// Write Zip64 end of central directory records (File header and locator). + /// + /// The number of entries in the central directory. + /// The size of entries in the central directory. + /// The offset of the dentral directory. + public void WriteZip64EndOfCentralDirectory(long noOfEntries, long sizeEntries, long centralDirOffset) + { + long centralSignatureOffset = stream_.Position; + WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature); + WriteLELong(44); // Size of this record (total size of remaining fields in header or full size - 12) + WriteLEShort(ZipConstants.VersionMadeBy); // Version made by + WriteLEShort(ZipConstants.VersionZip64); // Version to extract + WriteLEInt(0); // Number of this disk + WriteLEInt(0); // number of the disk with the start of the central directory + WriteLELong(noOfEntries); // No of entries on this disk + WriteLELong(noOfEntries); // Total No of entries in central directory + WriteLELong(sizeEntries); // Size of the central directory + WriteLELong(centralDirOffset); // offset of start of central directory + // zip64 extensible data sector not catered for here (variable size) + + // Write the Zip64 end of central directory locator + WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature); + + // no of the disk with the start of the zip64 end of central directory + WriteLEInt(0); + + // relative offset of the zip64 end of central directory record + WriteLELong(centralSignatureOffset); + + // total number of disks + WriteLEInt(1); + } + + /// + /// Write the required records to end the central directory. + /// + /// The number of entries in the directory. + /// The size of the entries in the directory. + /// The start of the central directory. + /// The archive comment. (This can be null). + public void WriteEndOfCentralDirectory(long noOfEntries, long sizeEntries, + long startOfCentralDirectory, byte[] comment) + { + + if ( (noOfEntries >= 0xffff) || + (startOfCentralDirectory >= 0xffffffff) || + (sizeEntries >= 0xffffffff) ) { + WriteZip64EndOfCentralDirectory(noOfEntries, sizeEntries, startOfCentralDirectory); + } + + WriteLEInt(ZipConstants.EndOfCentralDirectorySignature); + + // TODO: ZipFile Multi disk handling not done + WriteLEShort(0); // number of this disk + WriteLEShort(0); // no of disk with start of central dir + + + // Number of entries + if ( noOfEntries >= 0xffff ) { + WriteLEUshort(0xffff); // Zip64 marker + WriteLEUshort(0xffff); + } + else { + WriteLEShort(( short )noOfEntries); // entries in central dir for this disk + WriteLEShort(( short )noOfEntries); // total entries in central directory + } + + // Size of the central directory + if ( sizeEntries >= 0xffffffff ) { + WriteLEUint(0xffffffff); // Zip64 marker + } + else { + WriteLEInt(( int )sizeEntries); + } + + + // offset of start of central directory + if ( startOfCentralDirectory >= 0xffffffff ) { + WriteLEUint(0xffffffff); // Zip64 marker + } + else { + WriteLEInt(( int )startOfCentralDirectory); + } + + int commentLength = (comment != null) ? comment.Length : 0; + + if ( commentLength > 0xffff ) { + throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength)); + } + + WriteLEShort(commentLength); + + if ( commentLength > 0 ) { + Write(comment, 0, comment.Length); + } + } + + #region LE value reading/writing + /// + /// Read an unsigned short in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + public int ReadLEShort() + { + int byteValue1 = stream_.ReadByte(); + + if (byteValue1 < 0) { + throw new EndOfStreamException(); + } + + int byteValue2 = stream_.ReadByte(); + if (byteValue2 < 0) { + throw new EndOfStreamException(); + } + + return byteValue1 | (byteValue2 << 8); + } + + /// + /// Read an int in little endian byte order. + /// + /// Returns the value read. + /// + /// An i/o error occurs. + /// + /// + /// The file ends prematurely + /// + public int ReadLEInt() + { + return ReadLEShort() | (ReadLEShort() << 16); + } + + /// + /// Read a long in little endian byte order. + /// + /// The value read. + public long ReadLELong() + { + return (uint)ReadLEInt() | ((long)ReadLEInt() << 32); + } + + /// + /// Write an unsigned short in little endian byte order. + /// + /// The value to write. + public void WriteLEShort(int value) + { + stream_.WriteByte(( byte )(value & 0xff)); + stream_.WriteByte(( byte )((value >> 8) & 0xff)); + } + + /// + /// Write a ushort in little endian byte order. + /// + /// The value to write. + public void WriteLEUshort(ushort value) + { + stream_.WriteByte(( byte )(value & 0xff)); + stream_.WriteByte(( byte )(value >> 8)); + } + + /// + /// Write an int in little endian byte order. + /// + /// The value to write. + public void WriteLEInt(int value) + { + WriteLEShort(value); + WriteLEShort(value >> 16); + } + + /// + /// Write a uint in little endian byte order. + /// + /// The value to write. + public void WriteLEUint(uint value) + { + WriteLEUshort(( ushort )(value & 0xffff)); + WriteLEUshort(( ushort )(value >> 16)); + } + + /// + /// Write a long in little endian byte order. + /// + /// The value to write. + public void WriteLELong(long value) + { + WriteLEInt(( int )value); + WriteLEInt(( int )(value >> 32)); + } + + /// + /// Write a ulong in little endian byte order. + /// + /// The value to write. + public void WriteLEUlong(ulong value) + { + WriteLEUint(( uint )(value & 0xffffffff)); + WriteLEUint(( uint )(value >> 32)); + } + + #endregion + + /// + /// Write a data descriptor. + /// + /// The entry to write a descriptor for. + /// Returns the number of descriptor bytes written. + public int WriteDataDescriptor(ZipEntry entry) + { + if (entry == null) { + throw new ArgumentNullException("entry"); + } + + int result=0; + + // Add data descriptor if flagged as required + if ((entry.Flags & (int)GeneralBitFlags.Descriptor) != 0) + { + // The signature is not PKZIP originally but is now described as optional + // in the PKZIP Appnote documenting trhe format. + WriteLEInt(ZipConstants.DataDescriptorSignature); + WriteLEInt(unchecked((int)(entry.Crc))); + + result+=8; + + if (entry.LocalHeaderRequiresZip64) + { + WriteLELong(entry.CompressedSize); + WriteLELong(entry.Size); + result+=16; + } + else + { + WriteLEInt((int)entry.CompressedSize); + WriteLEInt((int)entry.Size); + result+=8; + } + } + + return result; + } + + /// + /// Read data descriptor at the end of compressed data. + /// + /// if set to true [zip64]. + /// The data to fill in. + /// Returns the number of bytes read in the descriptor. + public void ReadDataDescriptor(bool zip64, DescriptorData data) + { + int intValue = ReadLEInt(); + + // In theory this may not be a descriptor according to PKZIP appnote. + // In practise its always there. + if (intValue != ZipConstants.DataDescriptorSignature) { + throw new ZipException("Data descriptor signature not found"); + } + + data.Crc = ReadLEInt(); + + if (zip64) { + data.CompressedSize = ReadLELong(); + data.Size = ReadLELong(); + } + else { + data.CompressedSize = ReadLEInt(); + data.Size = ReadLEInt(); + } + } + + #region Instance Fields + bool isOwner_; + Stream stream_; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs new file mode 100644 index 000000000..90848d018 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipInputStream.cs @@ -0,0 +1,675 @@ +// ZipInputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 2010-05-25 Z-1663 Fixed exception when testing local header compressed size of -1 + +using System; +using System.IO; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +#if !NETCF_1_0 +using GitHub.ICSharpCode.SharpZipLib.Encryption; +#endif + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// This is an InflaterInputStream that reads the files baseInputStream an zip archive + /// one after another. It has a special method to get the zip entry of + /// the next file. The zip entry contains information about the file name + /// size, compressed size, Crc, etc. + /// It includes support for Stored and Deflated entries. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// + /// This sample shows how to read a zip file + /// + /// using System; + /// using System.Text; + /// using System.IO; + /// + /// using GitHub.ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// public static void Main(string[] args) + /// { + /// using ( ZipInputStream s = new ZipInputStream(File.OpenRead(args[0]))) { + /// + /// ZipEntry theEntry; + /// const int size = 2048; + /// byte[] data = new byte[2048]; + /// + /// while ((theEntry = s.GetNextEntry()) != null) { + /// if ( entry.IsFile ) { + /// Console.Write("Show contents (y/n) ?"); + /// if (Console.ReadLine() == "y") { + /// while (true) { + /// size = s.Read(data, 0, data.Length); + /// if (size > 0) { + /// Console.Write(new ASCIIEncoding().GetString(data, 0, size)); + /// } else { + /// break; + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipInputStream : InflaterInputStream + { + #region Instance Fields + + /// + /// Delegate for reading bytes from a stream. + /// + delegate int ReadDataHandler(byte[] b, int offset, int length); + + /// + /// The current reader this instance. + /// + ReadDataHandler internalReader; + + Crc32 crc = new Crc32(); + ZipEntry entry; + + long size; + int method; + int flags; + string password; + #endregion + + #region Constructors + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + public ZipInputStream(Stream baseInputStream) + : base(baseInputStream, new Inflater(true)) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + } + + /// + /// Creates a new Zip input stream, for reading a zip archive. + /// + /// The underlying providing data. + /// Size of the buffer. + public ZipInputStream( Stream baseInputStream, int bufferSize ) + : base(baseInputStream, new Inflater(true), bufferSize) + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + } + #endregion + + /// + /// Optional password used for encryption when non-null + /// + /// A password for all encrypted entries in this + public string Password + { + get { + return password; + } + set { + password = value; + } + } + + + /// + /// Gets a value indicating if there is a current entry and it can be decompressed + /// + /// + /// The entry can only be decompressed if the library supports the zip features required to extract it. + /// See the ZipEntry Version property for more details. + /// + public bool CanDecompressEntry { + get { + return (entry != null) && entry.CanDecompress; + } + } + + /// + /// Advances to the next entry in the archive + /// + /// + /// The next entry in the archive or null if there are no more entries. + /// + /// + /// If the previous entry is still open CloseEntry is called. + /// + /// + /// Input stream is closed + /// + /// + /// Password is not set, password is invalid, compression method is invalid, + /// version required to extract is not supported + /// + public ZipEntry GetNextEntry() + { + if (crc == null) { + throw new InvalidOperationException("Closed."); + } + + if (entry != null) { + CloseEntry(); + } + + int header = inputBuffer.ReadLeInt(); + + if (header == ZipConstants.CentralHeaderSignature || + header == ZipConstants.EndOfCentralDirectorySignature || + header == ZipConstants.CentralHeaderDigitalSignature || + header == ZipConstants.ArchiveExtraDataSignature || + header == ZipConstants.Zip64CentralFileHeaderSignature) { + // No more individual entries exist + Close(); + return null; + } + + // -jr- 07-Dec-2003 Ignore spanning temporary signatures if found + // Spanning signature is same as descriptor signature and is untested as yet. + if ( (header == ZipConstants.SpanningTempSignature) || (header == ZipConstants.SpanningSignature) ) { + header = inputBuffer.ReadLeInt(); + } + + if (header != ZipConstants.LocalHeaderSignature) { + throw new ZipException("Wrong Local header signature: 0x" + String.Format("{0:X}", header)); + } + + short versionRequiredToExtract = (short)inputBuffer.ReadLeShort(); + + flags = inputBuffer.ReadLeShort(); + method = inputBuffer.ReadLeShort(); + uint dostime = (uint)inputBuffer.ReadLeInt(); + int crc2 = inputBuffer.ReadLeInt(); + csize = inputBuffer.ReadLeInt(); + size = inputBuffer.ReadLeInt(); + int nameLen = inputBuffer.ReadLeShort(); + int extraLen = inputBuffer.ReadLeShort(); + + bool isCrypted = (flags & 1) == 1; + + byte[] buffer = new byte[nameLen]; + inputBuffer.ReadRawBuffer(buffer); + + string name = ZipConstants.ConvertToStringExt(flags, buffer); + + entry = new ZipEntry(name, versionRequiredToExtract); + entry.Flags = flags; + + entry.CompressionMethod = (CompressionMethod)method; + + if ((flags & 8) == 0) { + entry.Crc = crc2 & 0xFFFFFFFFL; + entry.Size = size & 0xFFFFFFFFL; + entry.CompressedSize = csize & 0xFFFFFFFFL; + + entry.CryptoCheckValue = (byte)((crc2 >> 24) & 0xff); + + } else { + + // This allows for GNU, WinZip and possibly other archives, the PKZIP spec + // says these values are zero under these circumstances. + if (crc2 != 0) { + entry.Crc = crc2 & 0xFFFFFFFFL; + } + + if (size != 0) { + entry.Size = size & 0xFFFFFFFFL; + } + + if (csize != 0) { + entry.CompressedSize = csize & 0xFFFFFFFFL; + } + + entry.CryptoCheckValue = (byte)((dostime >> 8) & 0xff); + } + + entry.DosTime = dostime; + + // If local header requires Zip64 is true then the extended header should contain + // both values. + + // Handle extra data if present. This can set/alter some fields of the entry. + if (extraLen > 0) { + byte[] extra = new byte[extraLen]; + inputBuffer.ReadRawBuffer(extra); + entry.ExtraData = extra; + } + + entry.ProcessExtraData(true); + if ( entry.CompressedSize >= 0 ) { + csize = entry.CompressedSize; + } + + if ( entry.Size >= 0 ) { + size = entry.Size; + } + + if (method == (int)CompressionMethod.Stored && (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size))) { + throw new ZipException("Stored, but compressed != uncompressed"); + } + + // Determine how to handle reading of data if this is attempted. + if (entry.IsCompressionMethodSupported()) { + internalReader = new ReadDataHandler(InitialRead); + } else { + internalReader = new ReadDataHandler(ReadingNotSupported); + } + + return entry; + } + + /// + /// Read data descriptor at the end of compressed data. + /// + void ReadDataDescriptor() + { + if (inputBuffer.ReadLeInt() != ZipConstants.DataDescriptorSignature) { + throw new ZipException("Data descriptor signature not found"); + } + + entry.Crc = inputBuffer.ReadLeInt() & 0xFFFFFFFFL; + + if ( entry.LocalHeaderRequiresZip64 ) { + csize = inputBuffer.ReadLeLong(); + size = inputBuffer.ReadLeLong(); + } else { + csize = inputBuffer.ReadLeInt(); + size = inputBuffer.ReadLeInt(); + } + entry.CompressedSize = csize; + entry.Size = size; + } + + /// + /// Complete cleanup as the final part of closing. + /// + /// True if the crc value should be tested + void CompleteCloseEntry(bool testCrc) + { + StopDecrypting(); + + if ((flags & 8) != 0) { + ReadDataDescriptor(); + } + + size = 0; + + if ( testCrc && + ((crc.Value & 0xFFFFFFFFL) != entry.Crc) && (entry.Crc != -1)) { + throw new ZipException("CRC mismatch"); + } + + crc.Reset(); + + if (method == (int)CompressionMethod.Deflated) { + inf.Reset(); + } + entry = null; + } + + /// + /// Closes the current zip entry and moves to the next one. + /// + /// + /// The stream is closed + /// + /// + /// The Zip stream ends early + /// + public void CloseEntry() + { + if (crc == null) { + throw new InvalidOperationException("Closed"); + } + + if (entry == null) { + return; + } + + if (method == (int)CompressionMethod.Deflated) { + if ((flags & 8) != 0) { + // We don't know how much we must skip, read until end. + byte[] tmp = new byte[4096]; + + // Read will close this entry + while (Read(tmp, 0, tmp.Length) > 0) { + } + return; + } + + csize -= inf.TotalIn; + inputBuffer.Available += inf.RemainingInput; + } + + if ( (inputBuffer.Available > csize) && (csize >= 0) ) { + inputBuffer.Available = (int)((long)inputBuffer.Available - csize); + } else { + csize -= inputBuffer.Available; + inputBuffer.Available = 0; + while (csize != 0) { + long skipped = base.Skip(csize); + + if (skipped <= 0) { + throw new ZipException("Zip archive ends early."); + } + + csize -= skipped; + } + } + + CompleteCloseEntry(false); + } + + /// + /// Returns 1 if there is an entry available + /// Otherwise returns 0. + /// + public override int Available { + get { + return entry != null ? 1 : 0; + } + } + + /// + /// Returns the current size that can be read from the current entry if available + /// + /// Thrown if the entry size is not known. + /// Thrown if no entry is currently available. + public override long Length + { + get { + if ( entry != null ) { + if ( entry.Size >= 0 ) { + return entry.Size; + } else { + throw new ZipException("Length not available for the current entry"); + } + } + else { + throw new InvalidOperationException("No current entry"); + } + } + + } + + /// + /// Reads a byte from the current zip entry. + /// + /// + /// The byte or -1 if end of stream is reached. + /// + public override int ReadByte() + { + byte[] b = new byte[1]; + if (Read(b, 0, 1) <= 0) { + return -1; + } + return b[0] & 0xff; + } + + /// + /// Handle attempts to read by throwing an . + /// + /// The destination array to store data in. + /// The offset at which data read should be stored. + /// The maximum number of bytes to read. + /// Returns the number of bytes actually read. + int ReadingNotAvailable(byte[] destination, int offset, int count) + { + throw new InvalidOperationException("Unable to read from this stream"); + } + + /// + /// Handle attempts to read from this entry by throwing an exception + /// + int ReadingNotSupported(byte[] destination, int offset, int count) + { + throw new ZipException("The compression method for this entry is not supported"); + } + + /// + /// Perform the initial read on an entry which may include + /// reading encryption headers and setting up inflation. + /// + /// The destination to fill with data read. + /// The offset to start reading at. + /// The maximum number of bytes to read. + /// The actual number of bytes read. + int InitialRead(byte[] destination, int offset, int count) + { + if ( !CanDecompressEntry ) { + throw new ZipException("Library cannot extract this entry. Version required is (" + entry.Version.ToString() + ")"); + } + + // Handle encryption if required. + if (entry.IsCrypted) { +#if NETCF_1_0 + throw new ZipException("Encryption not supported for Compact Framework 1.0"); +#else + if (password == null) { + throw new ZipException("No password set."); + } + + // Generate and set crypto transform... + PkzipClassicManaged managed = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(ZipConstants.ConvertToArray(password)); + + inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null); + + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize); + + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) { + throw new ZipException("Invalid password"); + } + + if (csize >= ZipConstants.CryptoHeaderSize) { + csize -= ZipConstants.CryptoHeaderSize; + } + else if ( (entry.Flags & (int)GeneralBitFlags.Descriptor) == 0 ) { + throw new ZipException(string.Format("Entry compressed size {0} too small for encryption", csize)); + } +#endif + } else { +#if !NETCF_1_0 + inputBuffer.CryptoTransform = null; +#endif + } + + if ((csize > 0) || ((flags & (int)GeneralBitFlags.Descriptor) != 0)) { + if ((method == (int)CompressionMethod.Deflated) && (inputBuffer.Available > 0)) { + inputBuffer.SetInflaterInput(inf); + } + + internalReader = new ReadDataHandler(BodyRead); + return BodyRead(destination, offset, count); + } + else { + internalReader = new ReadDataHandler(ReadingNotAvailable); + return 0; + } + } + + /// + /// Read a block of bytes from the stream. + /// + /// The destination for the bytes. + /// The index to start storing data. + /// The number of bytes to attempt to read. + /// Returns the number of bytes read. + /// Zero bytes read means end of stream. + public override int Read(byte[] buffer, int offset, int count) + { + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "Cannot be negative"); +#endif + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Cannot be negative"); +#endif + } + + if ( (buffer.Length - offset) < count ) { + throw new ArgumentException("Invalid offset/count combination"); + } + + return internalReader(buffer, offset, count); + } + + /// + /// Reads a block of bytes from the current zip entry. + /// + /// + /// The number of bytes read (this may be less than the length requested, even before the end of stream), or 0 on end of stream. + /// + /// + /// An i/o error occured. + /// + /// + /// The deflated stream is corrupted. + /// + /// + /// The stream is not open. + /// + int BodyRead(byte[] buffer, int offset, int count) + { + if ( crc == null ) { + throw new InvalidOperationException("Closed"); + } + + if ( (entry == null) || (count <= 0) ) { + return 0; + } + + if ( offset + count > buffer.Length ) { + throw new ArgumentException("Offset + count exceeds buffer size"); + } + + bool finished = false; + + switch (method) { + case (int)CompressionMethod.Deflated: + count = base.Read(buffer, offset, count); + if (count <= 0) { + if (!inf.IsFinished) { + throw new ZipException("Inflater not finished!"); + } + inputBuffer.Available = inf.RemainingInput; + + // A csize of -1 is from an unpatched local header + if ((flags & 8) == 0 && + (inf.TotalIn != csize && csize != 0xFFFFFFFF && csize != -1 || inf.TotalOut != size)) { + throw new ZipException("Size mismatch: " + csize + ";" + size + " <-> " + inf.TotalIn + ";" + inf.TotalOut); + } + inf.Reset(); + finished = true; + } + break; + + case (int)CompressionMethod.Stored: + if ( (count > csize) && (csize >= 0) ) { + count = (int)csize; + } + + if ( count > 0 ) { + count = inputBuffer.ReadClearTextBuffer(buffer, offset, count); + if (count > 0) { + csize -= count; + size -= count; + } + } + + if (csize == 0) { + finished = true; + } else { + if (count < 0) { + throw new ZipException("EOF in stored block"); + } + } + break; + } + + if (count > 0) { + crc.Update(buffer, offset, count); + } + + if (finished) { + CompleteCloseEntry(true); + } + + return count; + } + + /// + /// Closes the zip input stream + /// + public override void Close() + { + internalReader = new ReadDataHandler(ReadingNotAvailable); + crc = null; + entry = null; + + base.Close(); + } + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs new file mode 100644 index 000000000..916f4b2e7 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipNameTransform.cs @@ -0,0 +1,269 @@ +// ZipNameTransform.cs +// +// Copyright 2005 John Reilly +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + + +using System; +using System.IO; +using System.Text; + +using GitHub.ICSharpCode.SharpZipLib.Core; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// ZipNameTransform transforms names as per the Zip file naming convention. + /// + /// The use of absolute names is supported although its use is not valid + /// according to Zip naming conventions, and should not be used if maximum compatability is desired. + public class ZipNameTransform : INameTransform + { + #region Constructors + /// + /// Initialize a new instance of + /// + public ZipNameTransform() + { + } + + /// + /// Initialize a new instance of + /// + /// The string to trim from the front of paths if found. + public ZipNameTransform(string trimPrefix) + { + TrimPrefix = trimPrefix; + } + #endregion + + /// + /// Static constructor. + /// + static ZipNameTransform() + { + char[] invalidPathChars; +#if NET_1_0 || NET_1_1 || NETCF_1_0 + invalidPathChars = Path.InvalidPathChars; +#else + invalidPathChars = Path.GetInvalidPathChars(); +#endif + int howMany = invalidPathChars.Length + 2; + + InvalidEntryCharsRelaxed = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryCharsRelaxed, 0, invalidPathChars.Length); + InvalidEntryCharsRelaxed[howMany - 1] = '*'; + InvalidEntryCharsRelaxed[howMany - 2] = '?'; + + howMany = invalidPathChars.Length + 4; + InvalidEntryChars = new char[howMany]; + Array.Copy(invalidPathChars, 0, InvalidEntryChars, 0, invalidPathChars.Length); + InvalidEntryChars[howMany - 1] = ':'; + InvalidEntryChars[howMany - 2] = '\\'; + InvalidEntryChars[howMany - 3] = '*'; + InvalidEntryChars[howMany - 4] = '?'; + } + + /// + /// Transform a windows directory name according to the Zip file naming conventions. + /// + /// The directory name to transform. + /// The transformed name. + public string TransformDirectory(string name) + { + name = TransformFile(name); + if (name.Length > 0) { + if ( !name.EndsWith("/") ) { + name += "/"; + } + } + else { + throw new ZipException("Cannot have an empty directory name"); + } + return name; + } + + /// + /// Transform a windows file name according to the Zip file naming conventions. + /// + /// The file name to transform. + /// The transformed name. + public string TransformFile(string name) + { + if (name != null) { + string lowerName = name.ToLower(); + if ( (trimPrefix_ != null) && (lowerName.IndexOf(trimPrefix_) == 0) ) { + name = name.Substring(trimPrefix_.Length); + } + + name = name.Replace(@"\", "/"); + name = WindowsPathUtils.DropPathRoot(name); + + // Drop any leading slashes. + while ((name.Length > 0) && (name[0] == '/')) + { + name = name.Remove(0, 1); + } + + // Drop any trailing slashes. + while ((name.Length > 0) && (name[name.Length - 1] == '/')) + { + name = name.Remove(name.Length - 1, 1); + } + + // Convert consecutive // characters to / + int index = name.IndexOf("//"); + while (index >= 0) + { + name = name.Remove(index, 1); + index = name.IndexOf("//"); + } + + name = MakeValidName(name, '_'); + } + else { + name = string.Empty; + } + return name; + } + + /// + /// Get/set the path prefix to be trimmed from paths if present. + /// + /// The prefix is trimmed before any conversion from + /// a windows path is done. + public string TrimPrefix + { + get { return trimPrefix_; } + set { + trimPrefix_ = value; + if (trimPrefix_ != null) { + trimPrefix_ = trimPrefix_.ToLower(); + } + } + } + + /// + /// Force a name to be valid by replacing invalid characters with a fixed value + /// + /// The name to force valid + /// The replacement character to use. + /// Returns a valid name + static string MakeValidName(string name, char replacement) + { + int index = name.IndexOfAny(InvalidEntryChars); + if (index >= 0) { + StringBuilder builder = new StringBuilder(name); + + while (index >= 0 ) { + builder[index] = replacement; + + if (index >= name.Length) { + index = -1; + } + else { + index = name.IndexOfAny(InvalidEntryChars, index + 1); + } + } + name = builder.ToString(); + } + + if (name.Length > 0xffff) { + throw new PathTooLongException(); + } + + return name; + } + + /// + /// Test a name to see if it is a valid name for a zip entry. + /// + /// The name to test. + /// If true checking is relaxed about windows file names and absolute paths. + /// Returns true if the name is a valid zip name; false otherwise. + /// Zip path names are actually in Unix format, and should only contain relative paths. + /// This means that any path stored should not contain a drive or + /// device letter, or a leading slash. All slashes should forward slashes '/'. + /// An empty name is valid for a file where the input comes from standard input. + /// A null name is not considered valid. + /// + public static bool IsValidName(string name, bool relaxed) + { + bool result = (name != null); + + if ( result ) { + if ( relaxed ) { + result = name.IndexOfAny(InvalidEntryCharsRelaxed) < 0; + } + else { + result = + (name.IndexOfAny(InvalidEntryChars) < 0) && + (name.IndexOf('/') != 0); + } + } + + return result; + } + + /// + /// Test a name to see if it is a valid name for a zip entry. + /// + /// The name to test. + /// Returns true if the name is a valid zip name; false otherwise. + /// Zip path names are actually in unix format, + /// and should only contain relative paths if a path is present. + /// This means that the path stored should not contain a drive or + /// device letter, or a leading slash. All slashes should forward slashes '/'. + /// An empty name is valid where the input comes from standard input. + /// A null name is not considered valid. + /// + public static bool IsValidName(string name) + { + bool result = + (name != null) && + (name.IndexOfAny(InvalidEntryChars) < 0) && + (name.IndexOf('/') != 0) + ; + return result; + } + + #region Instance Fields + string trimPrefix_; + #endregion + + #region Class Fields + static readonly char[] InvalidEntryChars; + static readonly char[] InvalidEntryCharsRelaxed; + #endregion + } +} diff --git a/src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs b/src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs new file mode 100644 index 000000000..a5cb5bd20 --- /dev/null +++ b/src/GitHub.Api/SharpZipLib/Zip/ZipOutputStream.cs @@ -0,0 +1,900 @@ +// ZipOutputStream.cs +// +// Copyright (C) 2001 Mike Krueger +// Copyright (C) 2004 John Reilly +// +// This file was translated from java, it was part of the GNU Classpath +// Copyright (C) 2001 Free Software Foundation, Inc. +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// +// Linking this library statically or dynamically with other modules is +// making a combined work based on this library. Thus, the terms and +// conditions of the GNU General Public License cover the whole +// combination. +// +// As a special exception, the copyright holders of this library give you +// permission to link this library with independent modules to produce an +// executable, regardless of the license terms of these independent +// modules, and to copy and distribute the resulting executable under +// terms of your choice, provided that you also meet, for each linked +// independent module, the terms and conditions of the license of that +// module. An independent module is a module which is not derived from +// or based on this library. If you modify this library, you may extend +// this exception to your version of the library, but you are not +// obligated to do so. If you do not wish to do so, delete this +// exception statement from your version. + +// HISTORY +// 22-12-2009 Z-1649 Added AES support +// 22-02-2010 Z-1648 Zero byte entries would create invalid zip files + +using System; +using System.IO; +using System.Collections; + +using GitHub.ICSharpCode.SharpZipLib.Checksums; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression; +using GitHub.ICSharpCode.SharpZipLib.Zip.Compression.Streams; + +namespace GitHub.ICSharpCode.SharpZipLib.Zip +{ + /// + /// This is a DeflaterOutputStream that writes the files into a zip + /// archive one after another. It has a special method to start a new + /// zip entry. The zip entries contains information about the file name + /// size, compressed size, CRC, etc. + /// + /// It includes support for Stored and Deflated entries. + /// This class is not thread safe. + ///
+ ///
Author of the original java version : Jochen Hoenicke + ///
+ /// This sample shows how to create a zip file + /// + /// using System; + /// using System.IO; + /// + /// using GitHub.ICSharpCode.SharpZipLib.Core; + /// using GitHub.ICSharpCode.SharpZipLib.Zip; + /// + /// class MainClass + /// { + /// public static void Main(string[] args) + /// { + /// string[] filenames = Directory.GetFiles(args[0]); + /// byte[] buffer = new byte[4096]; + /// + /// using ( ZipOutputStream s = new ZipOutputStream(File.Create(args[1])) ) { + /// + /// s.SetLevel(9); // 0 - store only to 9 - means best compression + /// + /// foreach (string file in filenames) { + /// ZipEntry entry = new ZipEntry(file); + /// s.PutNextEntry(entry); + /// + /// using (FileStream fs = File.OpenRead(file)) { + /// StreamUtils.Copy(fs, s, buffer); + /// } + /// } + /// } + /// } + /// } + /// + /// + public class ZipOutputStream : DeflaterOutputStream + { + #region Constructors + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// + /// The output stream to which the archive contents are written. + /// + public ZipOutputStream(Stream baseOutputStream) + : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true)) + { + } + + /// + /// Creates a new Zip output stream, writing a zip archive. + /// + /// The output stream to which the archive contents are written. + /// Size of the buffer to use. + public ZipOutputStream( Stream baseOutputStream, int bufferSize ) + : base(baseOutputStream, new Deflater(Deflater.DEFAULT_COMPRESSION, true), bufferSize) + { + } + #endregion + + /// + /// Gets a flag value of true if the central header has been added for this archive; false if it has not been added. + /// + /// No further entries can be added once this has been done. + public bool IsFinished + { + get { + return entries == null; + } + } + + /// + /// Set the zip file comment. + /// + /// + /// The comment text for the entire archive. + /// + /// + /// The converted comment is longer than 0xffff bytes. + /// + public void SetComment(string comment) + { + // TODO: Its not yet clear how to handle unicode comments here. + byte[] commentBytes = ZipConstants.ConvertToArray(comment); + if (commentBytes.Length > 0xffff) { + throw new ArgumentOutOfRangeException("comment"); + } + zipComment = commentBytes; + } + + /// + /// Sets the compression level. The new level will be activated + /// immediately. + /// + /// The new compression level (1 to 9). + /// + /// Level specified is not supported. + /// + /// + public void SetLevel(int level) + { + deflater_.SetLevel(level); + defaultCompressionLevel = level; + } + + /// + /// Get the current deflater compression level + /// + /// The current compression level + public int GetLevel() + { + return deflater_.GetLevel(); + } + + /// + /// Get / set a value indicating how Zip64 Extension usage is determined when adding entries. + /// + /// Older archivers may not understand Zip64 extensions. + /// If backwards compatability is an issue be careful when adding entries to an archive. + /// Setting this property to off is workable but less desirable as in those circumstances adding a file + /// larger then 4GB will fail. + public UseZip64 UseZip64 + { + get { return useZip64_; } + set { useZip64_ = value; } + } + + /// + /// Write an unsigned short in little endian byte order. + /// + private void WriteLeShort(int value) + { + unchecked { + baseOutputStream_.WriteByte((byte)(value & 0xff)); + baseOutputStream_.WriteByte((byte)((value >> 8) & 0xff)); + } + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLeInt(int value) + { + unchecked { + WriteLeShort(value); + WriteLeShort(value >> 16); + } + } + + /// + /// Write an int in little endian byte order. + /// + private void WriteLeLong(long value) + { + unchecked { + WriteLeInt((int)value); + WriteLeInt((int)(value >> 32)); + } + } + + /// + /// Starts a new Zip entry. It automatically closes the previous + /// entry if present. + /// All entry elements bar name are optional, but must be correct if present. + /// If the compression method is stored and the output is not patchable + /// the compression for that entry is automatically changed to deflate level 0 + /// + /// + /// the entry. + /// + /// + /// if entry passed is null. + /// + /// + /// if an I/O error occured. + /// + /// + /// if stream was finished + /// + /// + /// Too many entries in the Zip file
+ /// Entry name is too long
+ /// Finish has already been called
+ ///
+ public void PutNextEntry(ZipEntry entry) + { + if ( entry == null ) { + throw new ArgumentNullException("entry"); + } + + if (entries == null) { + throw new InvalidOperationException("ZipOutputStream was finished"); + } + + if (curEntry != null) { + CloseEntry(); + } + + if (entries.Count == int.MaxValue) { + throw new ZipException("Too many entries for Zip file"); + } + + CompressionMethod method = entry.CompressionMethod; + int compressionLevel = defaultCompressionLevel; + + // Clear flags that the library manages internally + entry.Flags &= (int)GeneralBitFlags.UnicodeText; + patchEntryHeader = false; + + bool headerInfoAvailable; + + // No need to compress - definitely no data. + if (entry.Size == 0) + { + entry.CompressedSize = entry.Size; + entry.Crc = 0; + method = CompressionMethod.Stored; + headerInfoAvailable = true; + } + else + { + headerInfoAvailable = (entry.Size >= 0) && entry.HasCrc; + + // Switch to deflation if storing isnt possible. + if (method == CompressionMethod.Stored) + { + if (!headerInfoAvailable) + { + if (!CanPatchEntries) + { + // Can't patch entries so storing is not possible. + method = CompressionMethod.Deflated; + compressionLevel = 0; + } + } + else // entry.size must be > 0 + { + entry.CompressedSize = entry.Size; + headerInfoAvailable = entry.HasCrc; + } + } + } + + if (headerInfoAvailable == false) { + if (CanPatchEntries == false) { + // Only way to record size and compressed size is to append a data descriptor + // after compressed data. + + // Stored entries of this form have already been converted to deflating. + entry.Flags |= 8; + } else { + patchEntryHeader = true; + } + } + + if (Password != null) { + entry.IsCrypted = true; + if (entry.Crc < 0) { + // Need to append a data descriptor as the crc isnt available for use + // with encryption, the date is used instead. Setting the flag + // indicates this to the decompressor. + entry.Flags |= 8; + } + } + + entry.Offset = offset; + entry.CompressionMethod = (CompressionMethod)method; + + curMethod = method; + sizePatchPos = -1; + + if ( (useZip64_ == UseZip64.On) || ((entry.Size < 0) && (useZip64_ == UseZip64.Dynamic)) ) { + entry.ForceZip64(); + } + + // Write the local file header + WriteLeInt(ZipConstants.LocalHeaderSignature); + + WriteLeShort(entry.Version); + WriteLeShort(entry.Flags); + WriteLeShort((byte)entry.CompressionMethodForHeader); + WriteLeInt((int)entry.DosTime); + + // TODO: Refactor header writing. Its done in several places. + if (headerInfoAvailable == true) { + WriteLeInt((int)entry.Crc); + if ( entry.LocalHeaderRequiresZip64 ) { + WriteLeInt(-1); + WriteLeInt(-1); + } + else { + WriteLeInt(entry.IsCrypted ? (int)entry.CompressedSize + ZipConstants.CryptoHeaderSize : (int)entry.CompressedSize); + WriteLeInt((int)entry.Size); + } + } else { + if (patchEntryHeader) { + crcPatchPos = baseOutputStream_.Position; + } + WriteLeInt(0); // Crc + + if ( patchEntryHeader ) { + sizePatchPos = baseOutputStream_.Position; + } + + // For local header both sizes appear in Zip64 Extended Information + if ( entry.LocalHeaderRequiresZip64 || patchEntryHeader ) { + WriteLeInt(-1); + WriteLeInt(-1); + } + else { + WriteLeInt(0); // Compressed size + WriteLeInt(0); // Uncompressed size + } + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xFFFF) { + throw new ZipException("Entry name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if (entry.LocalHeaderRequiresZip64) { + ed.StartNewEntry(); + if (headerInfoAvailable) { + ed.AddLeLong(entry.Size); + ed.AddLeLong(entry.CompressedSize); + } + else { + ed.AddLeLong(-1); + ed.AddLeLong(-1); + } + ed.AddNewEntry(1); + + if ( !ed.Find(1) ) { + throw new ZipException("Internal error cant find extra data"); + } + + if ( patchEntryHeader ) { + sizePatchPos = ed.CurrentReadIndex; + } + } + else { + ed.Delete(1); + } + +#if !NET_1_1 && !NETCF_2_0 + if (entry.AESKeySize > 0) { + AddExtraDataAES(entry, ed); + } +#endif + byte[] extra = ed.GetEntryData(); + + WriteLeShort(name.Length); + WriteLeShort(extra.Length); + + if ( name.Length > 0 ) { + baseOutputStream_.Write(name, 0, name.Length); + } + + if ( entry.LocalHeaderRequiresZip64 && patchEntryHeader ) { + sizePatchPos += baseOutputStream_.Position; + } + + if ( extra.Length > 0 ) { + baseOutputStream_.Write(extra, 0, extra.Length); + } + + offset += ZipConstants.LocalHeaderBaseSize + name.Length + extra.Length; + // Fix offsetOfCentraldir for AES + if (entry.AESKeySize > 0) + offset += entry.AESOverheadSize; + + // Activate the entry. + curEntry = entry; + crc.Reset(); + if (method == CompressionMethod.Deflated) { + deflater_.Reset(); + deflater_.SetLevel(compressionLevel); + } + size = 0; + + if (entry.IsCrypted) { +#if !NET_1_1 && !NETCF_2_0 + if (entry.AESKeySize > 0) { + WriteAESHeader(entry); + } else +#endif + { + if (entry.Crc < 0) { // so testing Zip will says its ok + WriteEncryptionHeader(entry.DosTime << 16); + } else { + WriteEncryptionHeader(entry.Crc); + } + } + } + } + + /// + /// Closes the current entry, updating header and footer information as required + /// + /// + /// An I/O error occurs. + /// + /// + /// No entry is active. + /// + public void CloseEntry() + { + if (curEntry == null) { + throw new InvalidOperationException("No open entry"); + } + + long csize = size; + + // First finish the deflater, if appropriate + if (curMethod == CompressionMethod.Deflated) { + if (size >= 0) { + base.Finish(); + csize = deflater_.TotalOut; + } + else { + deflater_.Reset(); + } + } + + // Write the AES Authentication Code (a hash of the compressed and encrypted data) + if (curEntry.AESKeySize > 0) { + baseOutputStream_.Write(AESAuthCode, 0, 10); + } + + if (curEntry.Size < 0) { + curEntry.Size = size; + } else if (curEntry.Size != size) { + throw new ZipException("size was " + size + ", but I expected " + curEntry.Size); + } + + if (curEntry.CompressedSize < 0) { + curEntry.CompressedSize = csize; + } else if (curEntry.CompressedSize != csize) { + throw new ZipException("compressed size was " + csize + ", but I expected " + curEntry.CompressedSize); + } + + if (curEntry.Crc < 0) { + curEntry.Crc = crc.Value; + } else if (curEntry.Crc != crc.Value) { + throw new ZipException("crc was " + crc.Value + ", but I expected " + curEntry.Crc); + } + + offset += csize; + + if (curEntry.IsCrypted) { + if (curEntry.AESKeySize > 0) { + curEntry.CompressedSize += curEntry.AESOverheadSize; + + } else { + curEntry.CompressedSize += ZipConstants.CryptoHeaderSize; + } + } + + // Patch the header if possible + if (patchEntryHeader) { + patchEntryHeader = false; + + long curPos = baseOutputStream_.Position; + baseOutputStream_.Seek(crcPatchPos, SeekOrigin.Begin); + WriteLeInt((int)curEntry.Crc); + + if ( curEntry.LocalHeaderRequiresZip64 ) { + + if ( sizePatchPos == -1 ) { + throw new ZipException("Entry requires zip64 but this has been turned off"); + } + + baseOutputStream_.Seek(sizePatchPos, SeekOrigin.Begin); + WriteLeLong(curEntry.Size); + WriteLeLong(curEntry.CompressedSize); + } + else { + WriteLeInt((int)curEntry.CompressedSize); + WriteLeInt((int)curEntry.Size); + } + baseOutputStream_.Seek(curPos, SeekOrigin.Begin); + } + + // Add data descriptor if flagged as required + if ((curEntry.Flags & 8) != 0) { + WriteLeInt(ZipConstants.DataDescriptorSignature); + WriteLeInt(unchecked((int)curEntry.Crc)); + + if ( curEntry.LocalHeaderRequiresZip64 ) { + WriteLeLong(curEntry.CompressedSize); + WriteLeLong(curEntry.Size); + offset += ZipConstants.Zip64DataDescriptorSize; + } + else { + WriteLeInt((int)curEntry.CompressedSize); + WriteLeInt((int)curEntry.Size); + offset += ZipConstants.DataDescriptorSize; + } + } + + entries.Add(curEntry); + curEntry = null; + } + + void WriteEncryptionHeader(long crcValue) + { + offset += ZipConstants.CryptoHeaderSize; + + InitializePassword(Password); + + byte[] cryptBuffer = new byte[ZipConstants.CryptoHeaderSize]; + Random rnd = new Random(); + rnd.NextBytes(cryptBuffer); + cryptBuffer[11] = (byte)(crcValue >> 24); + + EncryptBlock(cryptBuffer, 0, cryptBuffer.Length); + baseOutputStream_.Write(cryptBuffer, 0, cryptBuffer.Length); + } + +#if !NET_1_1 && !NETCF_2_0 + private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) { + + // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. + const int VENDOR_VERSION = 2; + // Vendor ID is the two ASCII characters "AE". + const int VENDOR_ID = 0x4541; //not 6965; + extraData.StartNewEntry(); + // Pack AES extra data field see http://www.winzip.com/aes_info.htm + //extraData.AddLeShort(7); // Data size (currently 7) + extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2 + extraData.AddLeShort(VENDOR_ID); // "AE" + extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256 + extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file + extraData.AddNewEntry(0x9901); + } + + // Replaces WriteEncryptionHeader for AES + // + private void WriteAESHeader(ZipEntry entry) { + byte[] salt; + byte[] pwdVerifier; + InitializeAESPassword(entry, Password, out salt, out pwdVerifier); + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. + baseOutputStream_.Write(salt, 0, salt.Length); + baseOutputStream_.Write(pwdVerifier, 0, pwdVerifier.Length); + } +#endif + + /// + /// Writes the given buffer to the current entry. + /// + /// The buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Archive size is invalid + /// No entry is active. + public override void Write(byte[] buffer, int offset, int count) + { + if (curEntry == null) { + throw new InvalidOperationException("No open entry."); + } + + if ( buffer == null ) { + throw new ArgumentNullException("buffer"); + } + + if ( offset < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("offset"); +#else + throw new ArgumentOutOfRangeException("offset", "Cannot be negative"); +#endif + } + + if ( count < 0 ) { +#if NETCF_1_0 + throw new ArgumentOutOfRangeException("count"); +#else + throw new ArgumentOutOfRangeException("count", "Cannot be negative"); +#endif + } + + if ( (buffer.Length - offset) < count ) { + throw new ArgumentException("Invalid offset/count combination"); + } + + crc.Update(buffer, offset, count); + size += count; + + switch (curMethod) { + case CompressionMethod.Deflated: + base.Write(buffer, offset, count); + break; + + case CompressionMethod.Stored: + if (Password != null) { + CopyAndEncrypt(buffer, offset, count); + } else { + baseOutputStream_.Write(buffer, offset, count); + } + break; + } + } + + void CopyAndEncrypt(byte[] buffer, int offset, int count) + { + const int CopyBufferSize = 4096; + byte[] localBuffer = new byte[CopyBufferSize]; + while ( count > 0 ) { + int bufferCount = (count < CopyBufferSize) ? count : CopyBufferSize; + + Array.Copy(buffer, offset, localBuffer, 0, bufferCount); + EncryptBlock(localBuffer, 0, bufferCount); + baseOutputStream_.Write(localBuffer, 0, bufferCount); + count -= bufferCount; + offset += bufferCount; + } + } + + /// + /// Finishes the stream. This will write the central directory at the + /// end of the zip file and flush the stream. + /// + /// + /// This is automatically called when the stream is closed. + /// + /// + /// An I/O error occurs. + /// + /// + /// Comment exceeds the maximum length
+ /// Entry name exceeds the maximum length + ///
+ public override void Finish() + { + if (entries == null) { + return; + } + + if (curEntry != null) { + CloseEntry(); + } + + long numEntries = entries.Count; + long sizeEntries = 0; + + foreach (ZipEntry entry in entries) { + WriteLeInt(ZipConstants.CentralHeaderSignature); + WriteLeShort(ZipConstants.VersionMadeBy); + WriteLeShort(entry.Version); + WriteLeShort(entry.Flags); + WriteLeShort((short)entry.CompressionMethodForHeader); + WriteLeInt((int)entry.DosTime); + WriteLeInt((int)entry.Crc); + + if ( entry.IsZip64Forced() || + (entry.CompressedSize >= uint.MaxValue) ) + { + WriteLeInt(-1); + } + else { + WriteLeInt((int)entry.CompressedSize); + } + + if ( entry.IsZip64Forced() || + (entry.Size >= uint.MaxValue) ) + { + WriteLeInt(-1); + } + else { + WriteLeInt((int)entry.Size); + } + + byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name); + + if (name.Length > 0xffff) { + throw new ZipException("Name too long."); + } + + ZipExtraData ed = new ZipExtraData(entry.ExtraData); + + if ( entry.CentralHeaderRequiresZip64 ) { + ed.StartNewEntry(); + if ( entry.IsZip64Forced() || + (entry.Size >= 0xffffffff) ) + { + ed.AddLeLong(entry.Size); + } + + if ( entry.IsZip64Forced() || + (entry.CompressedSize >= 0xffffffff) ) + { + ed.AddLeLong(entry.CompressedSize); + } + + if ( entry.Offset >= 0xffffffff ) + { + ed.AddLeLong(entry.Offset); + } + + ed.AddNewEntry(1); + } + else { + ed.Delete(1); + } + +#if !NET_1_1 && !NETCF_2_0 + if (entry.AESKeySize > 0) { + AddExtraDataAES(entry, ed); + } +#endif + byte[] extra = ed.GetEntryData(); + + byte[] entryComment = + (entry.Comment != null) ? + ZipConstants.ConvertToArray(entry.Flags, entry.Comment) : + new byte[0]; + + if (entryComment.Length > 0xffff) { + throw new ZipException("Comment too long."); + } + + WriteLeShort(name.Length); + WriteLeShort(extra.Length); + WriteLeShort(entryComment.Length); + WriteLeShort(0); // disk number + WriteLeShort(0); // internal file attributes + // external file attributes + + if (entry.ExternalFileAttributes != -1) { + WriteLeInt(entry.ExternalFileAttributes); + } else { + if (entry.IsDirectory) { // mark entry as directory (from nikolam.AT.perfectinfo.com) + WriteLeInt(16); + } else { + WriteLeInt(0); + } + } + + if ( entry.Offset >= uint.MaxValue ) { + WriteLeInt(-1); + } + else { + WriteLeInt((int)entry.Offset); + } + + if ( name.Length > 0 ) { + baseOutputStream_.Write(name, 0, name.Length); + } + + if ( extra.Length > 0 ) { + baseOutputStream_.Write(extra, 0, extra.Length); + } + + if ( entryComment.Length > 0 ) { + baseOutputStream_.Write(entryComment, 0, entryComment.Length); + } + + sizeEntries += ZipConstants.CentralHeaderBaseSize + name.Length + extra.Length + entryComment.Length; + } + + using ( ZipHelperStream zhs = new ZipHelperStream(baseOutputStream_) ) { + zhs.WriteEndOfCentralDirectory(numEntries, sizeEntries, offset, zipComment); + } + + entries = null; + } + + #region Instance Fields + /// + /// The entries for the archive. + /// + ArrayList entries = new ArrayList(); + + /// + /// Used to track the crc of data added to entries. + /// + Crc32 crc = new Crc32(); + + /// + /// The current entry being added. + /// + ZipEntry curEntry; + + int defaultCompressionLevel = Deflater.DEFAULT_COMPRESSION; + + CompressionMethod curMethod = CompressionMethod.Deflated; + + /// + /// Used to track the size of data for an entry during writing. + /// + long size; + + /// + /// Offset to be recorded for each entry in the central header. + /// + long offset; + + /// + /// Comment for the entire archive recorded in central header. + /// + byte[] zipComment = new byte[0]; + + /// + /// Flag indicating that header patching is required for the current entry. + /// + bool patchEntryHeader; + + /// + /// Position to patch crc + /// + long crcPatchPos = -1; + + /// + /// Position to patch size. + /// + long sizePatchPos = -1; + + // Default is dynamic which is not backwards compatible and can cause problems + // with XP's built in compression which cant read Zip64 archives. + // However it does avoid the situation were a large file is added and cannot be completed correctly. + // NOTE: Setting the size for entries before they are added is the best solution! + UseZip64 useZip64_ = UseZip64.Dynamic; + #endregion + } +} From 324a655fa1a1602073a978b09a6ff17091db1f13 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 9 Oct 2018 09:53:27 -0400 Subject: [PATCH 445/567] Fixing ncrunch --- src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject | 8 ++++++++ src/GitHub.Api/GitHub.Api.v3.ncrunchproject | 4 ++-- .../GitHub.Logging.v3.ncrunchproject | 6 ++++++ .../GitHub.Unity.45.v3.ncrunchproject | 5 +++++ .../UnityTests/UnityTests.v3.ncrunchproject | 5 +++++ src/tests/CommandLine/CommandLine.csproj | 4 ---- .../IntegrationTests/IntegrationTests.csproj | 4 ---- .../IntegrationTests.v3.ncrunchproject | 15 ++++++++++++--- .../TestWebServer/TestWebServer.v3.ncrunchproject | 8 ++++++++ src/tests/UnitTests/UnitTests.v3.ncrunchproject | 6 +++--- 10 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject create mode 100644 src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject create mode 100644 src/tests/TestWebServer/TestWebServer.v3.ncrunchproject diff --git a/src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject b/src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject new file mode 100644 index 000000000..ebf9681fd --- /dev/null +++ b/src/GitHub.Api/GitHub.Api.45.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + + ..\..\script\lib\Managed\UnityEditor.dll + ..\..\script\lib\Managed\UnityEngine.dll + + + \ No newline at end of file diff --git a/src/GitHub.Api/GitHub.Api.v3.ncrunchproject b/src/GitHub.Api/GitHub.Api.v3.ncrunchproject index 2f25da2c4..8fa6360df 100644 --- a/src/GitHub.Api/GitHub.Api.v3.ncrunchproject +++ b/src/GitHub.Api/GitHub.Api.v3.ncrunchproject @@ -1,8 +1,8 @@  - ..\..\script\lib\UnityEditor.dll - ..\..\script\lib\UnityEngine.dll + ..\..\script\lib\Managed\UnityEngine.dll + ..\..\script\lib\Managed\UnityEditor.dll True diff --git a/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject b/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject index 6d9cc8a63..9e3e80dd6 100644 --- a/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject +++ b/src/GitHub.Logging/GitHub.Logging.v3.ncrunchproject @@ -1,5 +1,11 @@  + + ..\..\script\lib\UnityExtensions\Unity\TestRunner\UnityEngine.TestRunner.dll + ..\..\script\lib\UnityExtensions\Unity\TestRunner\Editor\UnityEditor.TestRunner.dll + ..\..\script\lib\Managed\UnityEditor.dll + ..\..\script\lib\Managed\UnityEngine.dll + True diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject new file mode 100644 index 000000000..319cd523c --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject new file mode 100644 index 000000000..319cd523c --- /dev/null +++ b/src/UnityExtension/Assets/Editor/UnityTests/UnityTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/src/tests/CommandLine/CommandLine.csproj b/src/tests/CommandLine/CommandLine.csproj index a5e3b5a0b..2b00d6b80 100644 --- a/src/tests/CommandLine/CommandLine.csproj +++ b/src/tests/CommandLine/CommandLine.csproj @@ -55,10 +55,6 @@ {bb6a8eda-15d8-471b-a6ed-ee551e0b3ba0} GitHub.Logging - - {add7a18b-dd2a-4c22-a2c1-488964eff30a} - GitHub.Unity - {3dd3451c-30fa-4294-a3a9-1e080342f867} TestWebServer diff --git a/src/tests/IntegrationTests/IntegrationTests.csproj b/src/tests/IntegrationTests/IntegrationTests.csproj index aae88d537..96ab7bcad 100644 --- a/src/tests/IntegrationTests/IntegrationTests.csproj +++ b/src/tests/IntegrationTests/IntegrationTests.csproj @@ -40,10 +40,6 @@ $(SolutionDir)packages\FluentAssertions.2.2.0.0\lib\net35\FluentAssertions.dll True - - False - $(SolutionDir)lib\ICSharpCode.SharpZipLib.dll - $(SolutionDir)packages\NCrunch.Framework.3.3.0.6\lib\NCrunch.Framework.dll True diff --git a/src/tests/IntegrationTests/IntegrationTests.v3.ncrunchproject b/src/tests/IntegrationTests/IntegrationTests.v3.ncrunchproject index 85e05b85b..dd346e243 100644 --- a/src/tests/IntegrationTests/IntegrationTests.v3.ncrunchproject +++ b/src/tests/IntegrationTests/IntegrationTests.v3.ncrunchproject @@ -1,10 +1,10 @@  - ..\..\..\lib\sfw.net\win\x64\Debug\**.* ..\..\GitHub.Api\PlatformResources\**.* - ..\..\..\script\lib\UnityEditor.dll - ..\..\..\script\lib\UnityEngine.dll + ..\..\..\script\lib\Managed\UnityEditor.dll + ..\..\..\script\lib\Managed\UnityEngine.dll + ..\..\..\lib\sfw\win\x64\**.* AbnormalReferenceResolution @@ -16,6 +16,15 @@ IntegrationTests.GitSetupTests.VerifyGitLfsBundle + + IntegrationTests.GitInstallerTestsWithHttp + + + IntegrationTests.GitInstallerTests + + + IntegrationTests.A_GitClientTests + True True diff --git a/src/tests/TestWebServer/TestWebServer.v3.ncrunchproject b/src/tests/TestWebServer/TestWebServer.v3.ncrunchproject new file mode 100644 index 000000000..fc42a761c --- /dev/null +++ b/src/tests/TestWebServer/TestWebServer.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + + ..\..\..\script\lib\Managed\UnityEditor.dll + ..\..\..\script\lib\Managed\UnityEngine.dll + + + \ No newline at end of file diff --git a/src/tests/UnitTests/UnitTests.v3.ncrunchproject b/src/tests/UnitTests/UnitTests.v3.ncrunchproject index d63e89685..d30a60555 100644 --- a/src/tests/UnitTests/UnitTests.v3.ncrunchproject +++ b/src/tests/UnitTests/UnitTests.v3.ncrunchproject @@ -1,9 +1,9 @@  - ..\..\..\lib\sfw.net\win\x64\Debug\**.* - ..\..\..\script\lib\UnityEditor.dll - ..\..\..\script\lib\UnityEngine.dll + ..\..\..\script\lib\Managed\UnityEditor.dll + ..\..\..\script\lib\Managed\UnityEngine.dll + ..\..\..\lib\sfw\win\x64\**.* PostBuildEventDisabled From b00727d45d1fcb8003c57b28669422fb364fb508 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 9 Oct 2018 16:09:52 -0400 Subject: [PATCH 446/567] Creating an octorun package manually --- octorun/version | 2 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/octorun/version b/octorun/version index 94ac57c5f..25af47546 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -bd66a20a \ No newline at end of file +902910f4 \ No newline at end of file diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 9baaca033..354bf69f3 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -88,7 +88,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "bd66a20a"; + public const string PackageVersion = "902910f4"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 5284a6d79..54758440a 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95a82d30f9e3a3f1eaec7d4a382983748d35320b87a2ec91134beb5d75738af3 -size 1399526 +oid sha256:3550059c5aceda73bc5ad30889c3fa7ba4e0abafeb17022721a777a3fa16dcf4 +size 212152 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 3f4ad43f2..bb4309be6 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -f6865e64072e9b65fa31ac9087fe1363 \ No newline at end of file +e20177a919ff49abab5d00f83080c622 From 59f7b5e2a184d1eeb650f5b3b74ca9d773b82a4c Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 9 Oct 2018 17:31:05 -0400 Subject: [PATCH 447/567] Removing stray sharpziplib meta file --- .../Editor/ICSharpCode.SharpZipLib.dll.meta | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta deleted file mode 100644 index cb9cd75f4..000000000 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ICSharpCode.SharpZipLib.dll.meta +++ /dev/null @@ -1,34 +0,0 @@ -fileFormatVersion: 2 -guid: ecfb28d906a32914d956497c8d3b3395 -timeCreated: 1493304328 -licenseType: Free -PluginImporter: - serializedVersion: 2 - iconMap: {} - executionOrder: {} - isPreloaded: 0 - isOverridable: 0 - platformData: - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - data: - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: From dd6bc566d9139f2c1a7fd00e4d907e9f248712f0 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 9 Oct 2018 17:34:01 -0400 Subject: [PATCH 448/567] Adding a helper method to attempt both copy styles --- src/GitHub.Api/GitHub.Api.45.csproj | 1 + src/GitHub.Api/GitHub.Api.csproj | 1 + src/GitHub.Api/Installer/CopyHelper.cs | 53 ++++++++++++++++++++ src/GitHub.Api/Installer/GitInstaller.cs | 8 +-- src/GitHub.Api/Installer/OctorunInstaller.cs | 4 +- 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 src/GitHub.Api/Installer/CopyHelper.cs diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj index 05a9c6b23..480e35974 100644 --- a/src/GitHub.Api/GitHub.Api.45.csproj +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -93,6 +93,7 @@ + diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index f52c319da..bf916ed46 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -104,6 +104,7 @@ + diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs new file mode 100644 index 000000000..c7f673850 --- /dev/null +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using GitHub.Logging; + +namespace GitHub.Unity +{ + public static class CopyHelper + { + private static readonly ILogging Logger = LogHelper.GetLogger(typeof(CopyHelper)); + + public static void Copy(NPath fromPath, NPath toPath) + { + try + { + + CopyFolder(fromPath, toPath); + } + catch (Exception ex1) + { + Logger.Warning(ex1, "Error copying from " + fromPath + " to " + toPath + ". Attempting to copy contents."); + + try + { + CopyFolderContents(fromPath, toPath); + } + catch (Exception ex2) + { + Logger.Error(ex2, "Error copying from " + fromPath + " to " + toPath + "."); + throw; + } + } + } + public static void CopyFolder(NPath fromPath, NPath toPath) + { + Logger.Trace("CopyFolder fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + + toPath.EnsureParentDirectoryExists(); + fromPath.Move(toPath); + fromPath.Delete(); + } + + public static void CopyFolderContents(NPath fromPath, NPath toPath) + { + Logger.Trace("CopyFolderContents fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + + toPath.DeleteContents(); + fromPath.MoveFiles(toPath, true); + fromPath.Delete(); + } + } +} diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 71dd77361..67b502a94 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -307,9 +307,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) { Logger.Trace("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); - target.DeleteContents(); - source.MoveFiles(target, true); - source.Parent.Delete(); + CopyHelper.Copy(source, target); state.GitIsValid = true; @@ -335,9 +333,7 @@ private GitInstallationState ExtractGit(GitInstallationState state) { Logger.Trace("Moving GitLFS source:{0} target:{1}", source.ToString(), target.ToString()); - target.DeleteContents(); - source.MoveFiles(target, true); - source.Parent.Delete(); + CopyHelper.Copy(source, target); state.GitLfsIsValid = true; } diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 354bf69f3..0aa655d93 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -55,9 +55,7 @@ private NPath MoveOctorun(NPath fromPath) Logger.Trace("MoveOctorun fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); - toPath.DeleteContents(); - fromPath.MoveFiles(toPath, true); - fromPath.Parent.Delete(); + CopyHelper.Copy(fromPath, toPath); return installDetails.ExecutablePath; } From 7562f7d77891d7994d006c87dc7b7d36b1168c98 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 9 Oct 2018 17:40:06 -0400 Subject: [PATCH 449/567] Fixing it up a bit --- src/GitHub.Api/Installer/CopyHelper.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs index c7f673850..22ec43579 100644 --- a/src/GitHub.Api/Installer/CopyHelper.cs +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -31,6 +31,10 @@ public static void Copy(NPath fromPath, NPath toPath) throw; } } + finally + { + fromPath.DeleteIfExists(); + } } public static void CopyFolder(NPath fromPath, NPath toPath) { @@ -38,7 +42,6 @@ public static void CopyFolder(NPath fromPath, NPath toPath) toPath.EnsureParentDirectoryExists(); fromPath.Move(toPath); - fromPath.Delete(); } public static void CopyFolderContents(NPath fromPath, NPath toPath) @@ -47,7 +50,6 @@ public static void CopyFolderContents(NPath fromPath, NPath toPath) toPath.DeleteContents(); fromPath.MoveFiles(toPath, true); - fromPath.Delete(); } } } From 7e44c95f896b6b92640c0c62c26df4563968cdc7 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 10 Oct 2018 10:29:01 -0400 Subject: [PATCH 450/567] Refreshing the asset database differently --- .../Assets/Editor/GitHub.Unity/UI/BranchesView.cs | 1 + .../Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs | 1 - src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs index aad002371..70d1d0c56 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BranchesView.cs @@ -514,6 +514,7 @@ private void SwitchBranch(string branch) { UsageTracker.IncrementBranchesViewButtonCheckoutLocalBranch(); Redraw(); + AssetDatabase.Refresh(); } else { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs index 0a0106267..3a9bb8dd9 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ProjectWindowInterface.cs @@ -67,7 +67,6 @@ private static void RepositoryOnStatusEntriesChanged(CacheUpdateEvent cacheUpdat if (!lastRepositoryStatusChangedEvent.Equals(cacheUpdateEvent)) { lastRepositoryStatusChangedEvent = cacheUpdateEvent; - AssetDatabase.Refresh(); entries.Clear(); entries.AddRange(Repository.CurrentChanges); OnStatusUpdate(); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 90557f47f..026bdd822 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -757,6 +757,8 @@ private void Pull() EditorUtility.DisplayDialog(Localization.PullActionTitle, String.Format(Localization.PullSuccessDescription, currentRemoteName), Localization.Ok); + + AssetDatabase.Refresh(); } else { From b30fc3809479f127ac5ab603acfeaa4b5cf0ec87 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 12 Oct 2018 14:52:51 -0400 Subject: [PATCH 451/567] Initial support for GitHub Enterprise --- octorun/bin/octorun-meta | 3 + octorun/src/api.js | 4 +- octorun/src/authentication.js | 4 +- octorun/src/bin/app-login.js | 3 +- octorun/src/bin/app-meta.js | 52 +++ octorun/src/bin/app-organizations.js | 4 +- octorun/src/bin/app-publish.js | 3 +- octorun/src/bin/app-validate.js | 3 +- octorun/src/bin/app.js | 1 + octorun/src/octokit.js | 12 +- octorun/version | 2 +- script | 2 +- src/GitHub.Api/Application/ApiClient.cs | 114 ++++- src/GitHub.Api/Application/IApiClient.cs | 1 + .../Authentication/ILoginManager.cs | 3 +- src/GitHub.Api/Authentication/LoginManager.cs | 14 +- src/GitHub.Api/Installer/CopyHelper.cs | 3 +- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 +- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- src/GitHub.Api/Tasks/OctorunTask.cs | 41 +- .../GitHub.Unity/GitHub.Unity.45.csproj | 2 + .../Editor/GitHub.Unity/GitHub.Unity.csproj | 2 + .../Services/AuthenticationService.cs | 20 +- .../GitHub.Unity/UI/AuthenticationView.cs | 254 +++--------- .../UI/GitHubAuthenticationView.cs | 262 ++++++++++++ .../UI/GitHubEnterpriseAuthenticationView.cs | 388 ++++++++++++++++++ .../GitHub/Editor/AsyncBridge.Net35.dll.meta | 46 +-- .../Plugins/GitHub/Editor/GitHub.Api.dll.meta | 46 +-- .../GitHub/Editor/GitHub.Unity.dll.meta | 46 +-- .../ReadOnlyCollectionsInterfaces.dll.meta | 46 +-- .../GitHub/Editor/System.Threading.dll.meta | 46 +-- .../ProjectSettings/DynamicsManager.asset | 2 + .../ProjectSettings/Physics2DSettings.asset | 7 +- .../ProjectSettings/DynamicsManager.asset | 2 + .../ProjectSettings/Physics2DSettings.asset | 7 +- .../ProjectSettings/ProjectSettings.asset | 212 ++++++++-- .../ProjectSettings/ProjectVersion.txt | 2 +- 38 files changed, 1264 insertions(+), 403 deletions(-) create mode 100644 octorun/bin/octorun-meta create mode 100644 octorun/src/bin/app-meta.js create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs diff --git a/octorun/bin/octorun-meta b/octorun/bin/octorun-meta new file mode 100644 index 000000000..1d2d3a7c9 --- /dev/null +++ b/octorun/bin/octorun-meta @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../src/bin/app-meta.js'); diff --git a/octorun/src/api.js b/octorun/src/api.js index 5b1bc833e..f7a672e39 100644 --- a/octorun/src/api.js +++ b/octorun/src/api.js @@ -1,7 +1,7 @@ var config = require("./configuration"); var octokitWrapper = require("./octokit"); -function ApiWrapper() { +function ApiWrapper(host) { if (!config.appName) { throw "appName missing"; } @@ -10,7 +10,7 @@ function ApiWrapper() { throw "token missing"; } - this.octokit = octokitWrapper.createOctokit(config.appName); + this.octokit = octokitWrapper.createOctokit(config.appName, host); this.octokit.authenticate({ type: "oauth", diff --git a/octorun/src/authentication.js b/octorun/src/authentication.js index e2d70d952..4147522cd 100644 --- a/octorun/src/authentication.js +++ b/octorun/src/authentication.js @@ -5,7 +5,7 @@ var twoFactorRegex = new RegExp("must specify two-factor authentication otp code var scopes = ["user", "repo"]; -var handleAuthentication = function (username, password, onSuccess, onFailure, twoFactor) { +var handleAuthentication = function (username, password, onSuccess, onFailure, twoFactor, host) { if (!config.clientId || !config.clientSecret) { throw "clientId and/or clientSecret missing"; } @@ -14,7 +14,7 @@ var handleAuthentication = function (username, password, onSuccess, onFailure, t throw "appName missing"; } - var octokit = octokitWrapper.createOctokit(config.appName); + var octokit = octokitWrapper.createOctokit(config.appName, host); octokit.authenticate({ type: "basic", diff --git a/octorun/src/bin/app-login.js b/octorun/src/bin/app-login.js index 6c2ab582a..4577bac81 100644 --- a/octorun/src/bin/app-login.js +++ b/octorun/src/bin/app-login.js @@ -6,6 +6,7 @@ var output = require('../output'); commander .version(package.version) .option('-t, --twoFactor') + .option('-h, --host ') .parse(process.argv); var handleAuthentication = function (username, password, twoFactor) { @@ -18,7 +19,7 @@ var handleAuthentication = function (username, password, twoFactor) { } }, function (error) { output.error(error); - }, twoFactor); + }, twoFactor, commander.host); } var encoding = 'utf-8'; diff --git a/octorun/src/bin/app-meta.js b/octorun/src/bin/app-meta.js new file mode 100644 index 000000000..ecbc36e66 --- /dev/null +++ b/octorun/src/bin/app-meta.js @@ -0,0 +1,52 @@ +var commander = require('commander'); +var package = require('../../package.json'); +var output = require('../output'); + +commander + .version(package.version) + .option('-h, --host ') + .parse(process.argv); + +var host = commander.host; +var port = 443; +var scheme = 'https'; + +if (host) { + var https = require(scheme); + var options = { + protocol: scheme + ':', + hostname: host, + port: port, + path: '/api/v3/meta', + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }; + + var req = https.request(options, function (res) { + var success = res.statusCode == 200; + + if(!success) { + output.error(res.statusCode); + } else { + res.on('data', function (d) { + output.custom("success", d, true); + }); + + res.on('end', function (d) { + process.exit(); + }); + } + }); + + req.on('error', function (error) { + output.error(error); + }); + + req.end(); +} +else { + commander.help(); + process.exit(-1); +} \ No newline at end of file diff --git a/octorun/src/bin/app-organizations.js b/octorun/src/bin/app-organizations.js index c9d70a760..1a67e181f 100644 --- a/octorun/src/bin/app-organizations.js +++ b/octorun/src/bin/app-organizations.js @@ -5,11 +5,11 @@ var output = require('../output'); commander .version(package.version) + .option('-h, --host ') .parse(process.argv); try { - - var apiWrapper = new ApiWrapper(); + var apiWrapper = new ApiWrapper(commander.host); apiWrapper.getOrgs(function (error, result) { if (error) { output.error(error); diff --git a/octorun/src/bin/app-publish.js b/octorun/src/bin/app-publish.js index 61d132326..62305763e 100644 --- a/octorun/src/bin/app-publish.js +++ b/octorun/src/bin/app-publish.js @@ -9,6 +9,7 @@ commander .option('-d, --description ') .option('-o, --organization ') .option('-p, --private') + .option('-h, --host ') .parse(process.argv); if(!commander.repository) @@ -23,7 +24,7 @@ if (commander.private) { } try { - var apiWrapper = new ApiWrapper(); + var apiWrapper = new ApiWrapper(commander.host); apiWrapper.publish(commander.repository, commander.description, private, commander.organization, function (error, result) { diff --git a/octorun/src/bin/app-validate.js b/octorun/src/bin/app-validate.js index 7fd53bbb9..294fbcbdc 100644 --- a/octorun/src/bin/app-validate.js +++ b/octorun/src/bin/app-validate.js @@ -5,10 +5,11 @@ var output = require('../output'); commander .version(package.version) + .option('-h, --host ') .parse(process.argv); try { - var apiWrapper = new ApiWrapper(); + var apiWrapper = new ApiWrapper(commander.host); apiWrapper.verifyUser(function (error, result) { if (error) { diff --git a/octorun/src/bin/app.js b/octorun/src/bin/app.js index e40d738b2..0eacfdc5e 100644 --- a/octorun/src/bin/app.js +++ b/octorun/src/bin/app.js @@ -9,4 +9,5 @@ commander .command('organizations', 'Get Organizations') .command('publish', 'Publish') .command('usage', 'Usage') + .command('meta', 'Get Server Meta Data') .parse(process.argv); \ No newline at end of file diff --git a/octorun/src/octokit.js b/octorun/src/octokit.js index f304d297a..fefde29e7 100644 --- a/octorun/src/octokit.js +++ b/octorun/src/octokit.js @@ -1,13 +1,19 @@ var Octokit = require('octokit-rest-for-node-v0.12'); -var createOctokit = function (appName) { - return Octokit({ +var createOctokit = function (appName, host) { + var octokitConfiguration = { timeout: 0, requestMedia: 'application/vnd.github.v3+json', headers: { 'user-agent': appName } - }); + }; + + if(host) { + octokitConfiguration.baseUrl = "https://" + host; + } + + return Octokit(octokitConfiguration); }; module.exports = { createOctokit: createOctokit }; \ No newline at end of file diff --git a/octorun/version b/octorun/version index 25af47546..3bf708bc6 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -902910f4 \ No newline at end of file +902910f45 \ No newline at end of file diff --git a/script b/script index 38269e987..d373977da 160000 --- a/script +++ b/script @@ -1 +1 @@ -Subproject commit 38269e987adabd0f42dda353872a46a5e206caea +Subproject commit d373977da73bdf7f9170e778638c80e5b49ca3b3 diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 01d189def..22cdcc6fb 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -1,15 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using GitHub.Logging; using System.Runtime.Serialization; using System.Text; +using System.Text.RegularExpressions; +using GitHub.Unity.Json; namespace GitHub.Unity { class ApiClient : IApiClient { private static readonly ILogging logger = LogHelper.GetLogger(); + private static readonly Regex httpStatusErrorRegex = new Regex("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + public HostAddress HostAddress { get; } public UriString OriginalUrl { get; } @@ -30,6 +35,9 @@ public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processM HostAddress = HostAddress.Create(host); OriginalUrl = host; + + logger.Trace("OriginalUrl: {1}", HostAddress.ToString(), OriginalUrl.ToString()); + this.keychain = keychain; this.processManager = processManager; this.taskManager = taskManager; @@ -52,7 +60,10 @@ public void CreateRepository(string name, string description, bool isPrivate, // this validates the user, again GetCurrentUser(); - var command = new StringBuilder("publish -r \""); + var command = new StringBuilder("publish -h "); + command.Append(OriginalUrl.Host); + + command.Append(" -r \""); command.Append(name); command.Append("\""); @@ -75,7 +86,13 @@ public void CreateRepository(string name, string description, bool isPrivate, command.Append(" -p"); } - var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, command.ToString()) + var adapter = keychain.Connect(OriginalUrl); + if (adapter.Credential == null) + { + throw new ApiClientException("No Credentials found"); + } + + var octorunTask = new OctorunTask(taskManager.Token, environment, command.ToString(), adapter.Credential.Token) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -103,13 +120,88 @@ public void CreateRepository(string name, string description, bool isPrivate, .Start(); } + public void GetServerMeta(Action onSuccess, Action onError = null) + { + Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); + new FuncTask(taskManager.Token, () => + { + var octorunTask = new OctorunTask(taskManager.Token, environment, "meta -h " + OriginalUrl.Host) + .Configure(processManager); + + var ret = octorunTask.RunSynchronously(); + if (ret.IsSuccess) + { + var deserializeObject = SimpleJson.DeserializeObject>(ret.Output[0]); + + return new GitHubHostMeta() + { + InstalledVersion = (string)deserializeObject["installed_version"], + GithubServicesSha = (string)deserializeObject["github_services_sha"], + VerifiablePasswordAuthentication = (bool)deserializeObject["verifiable_password_authentication"] + }; + } + + var message = ret.GetApiErrorMessage(); + + logger.Trace("Message: {0}", message); + + if (message != null) + { + if (message.Contains("ETIMEDOUT", StringComparison.InvariantCulture)) + { + message = "Connection timed out."; + } + else if (message.Contains("ECONNREFUSED", StringComparison.InvariantCulture)) + { + message = "Connection refused."; + } + else if (message.Contains("ENOTFOUND", StringComparison.InvariantCulture)) + { + message = "Address not found."; + } + else + { + int httpStatusCode; + if (int.TryParse(message, out httpStatusCode)) + { + var httpStatus = ((HttpStatusCode)httpStatusCode).ToString(); + message = httpStatusErrorRegex.Replace(httpStatus, " $1"); + } + } + } + else + { + message = "Error getting server meta"; + } + + throw new ApiClientException(message); + }) + .FinallyInUI((success, ex, meta) => + { + if (success) + onSuccess(meta); + else + { + logger.Error(ex, "Error getting server meta"); + onError?.Invoke(ex); + } + }) + .Start(); + } + public void GetOrganizations(Action onSuccess, Action onError = null) { Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); new FuncTask(taskManager.Token, () => { - var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, - "organizations") + var adapter = keychain.Connect(OriginalUrl); + if (adapter.Credential == null) + { + throw new ApiClientException("No Credentials found"); + } + + var octorunTask = new OctorunTask(taskManager.Token, environment, + "organizations -h " + OriginalUrl.Host, adapter.Credential.Token) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -247,7 +339,12 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha { try { - var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, "validate") + if (keychainAdapter.Credential == null) + { + throw new ApiClientException("No Credentials found"); + } + + var octorunTask = new OctorunTask(taskManager.Token, environment, "validate -h " + OriginalUrl.Host, keychainAdapter.Credential.Token) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -283,6 +380,13 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha } } + class GitHubHostMeta + { + public bool VerifiablePasswordAuthentication { get; set; } + public string GithubServicesSha { get; set; } + public string InstalledVersion { get; set; } + } + class GitHubUser { public string Name { get; set; } diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index 650595ce2..0f4c3270f 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -13,5 +13,6 @@ void CreateRepository(string name, string description, bool isPrivate, void ContinueLogin(LoginResult loginResult, string code); ITask Logout(UriString host); void GetCurrentUser(Action onSuccess, Action onError = null); + void GetServerMeta(Action onSuccess, Action onError = null); } } diff --git a/src/GitHub.Api/Authentication/ILoginManager.cs b/src/GitHub.Api/Authentication/ILoginManager.cs index 66d982ae0..41a9704e4 100644 --- a/src/GitHub.Api/Authentication/ILoginManager.cs +++ b/src/GitHub.Api/Authentication/ILoginManager.cs @@ -8,7 +8,7 @@ namespace GitHub.Unity interface ILoginManager { /// - /// Attempts to log into a GitHub server. + /// Attempts to log into a GitHub server with a username and password. /// /// /// The username. @@ -18,6 +18,7 @@ interface ILoginManager /// The login authorization failed. /// LoginResultData Login(UriString host, string username, string password); + LoginResultData ContinueLogin(LoginResultData loginResultData, string twofacode); /// diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 9a67986af..2f6cf5977 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -73,7 +73,7 @@ public LoginResultData Login( if (loginResultData.Code == LoginResultCodes.Success) { - username = RetrieveUsername(loginResultData, username); + username = RetrieveUsername(loginResultData.Token, username, host); keychainAdapter.Update(loginResultData.Token, username); keychain.SaveToSystem(host); } @@ -113,7 +113,7 @@ public LoginResultData ContinueLogin(LoginResultData loginResultData, string two } keychainAdapter.Update(loginResultData.Token, username); - username = RetrieveUsername(loginResultData, username); + username = RetrieveUsername(loginResultData.Token, username, host); keychainAdapter.Update(loginResultData.Token, username); keychain.SaveToSystem(host); @@ -147,9 +147,9 @@ private LoginResultData TryLogin( { var hasTwoFactorCode = code != null; - var arguments = hasTwoFactorCode ? "login --twoFactor" : "login"; - var loginTask = new OctorunTask(taskManager.Token, keychain, environment, - arguments); + var arguments = (hasTwoFactorCode ? "login --twoFactor -h " : "login -h ") + host.Host; + + var loginTask = new OctorunTask(taskManager.Token, environment, arguments); loginTask.Configure(processManager, withInput: true); loginTask.OnStartProcess += proc => { @@ -180,14 +180,14 @@ private LoginResultData TryLogin( return new LoginResultData(LoginResultCodes.Failed, ret.GetApiErrorMessage() ?? "Failed.", host); } - private string RetrieveUsername(LoginResultData loginResultData, string username) + private string RetrieveUsername(string token, string username, UriString host) { if (!username.Contains("@")) { return username; } - var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, "validate") + var octorunTask = new OctorunTask(taskManager.Token, environment, "validate -h " + host.Host, token) .Configure(processManager); var validateResult = octorunTask.RunSynchronously(); diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs index 22ec43579..e82aede38 100644 --- a/src/GitHub.Api/Installer/CopyHelper.cs +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -14,7 +14,6 @@ public static void Copy(NPath fromPath, NPath toPath) { try { - CopyFolder(fromPath, toPath); } catch (Exception ex1) @@ -39,7 +38,7 @@ public static void Copy(NPath fromPath, NPath toPath) public static void CopyFolder(NPath fromPath, NPath toPath) { Logger.Trace("CopyFolder fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); - + toPath.DeleteIfExists(); toPath.EnsureParentDirectoryExists(); fromPath.Move(toPath); } diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 0aa655d93..fce67ac2d 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -86,7 +86,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "902910f4"; + public const string PackageVersion = "902910f45"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 54758440a..0511ffd5f 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3550059c5aceda73bc5ad30889c3fa7ba4e0abafeb17022721a777a3fa16dcf4 -size 212152 +oid sha256:ffc11593937b7a03f3ef5450ec07699a0ae2f23d0785eb11fb827c1eccc82b20 +size 220342 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index bb4309be6..849770bbb 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -e20177a919ff49abab5d00f83080c622 +0e2d411bfe82bc5b579703f5604ceed0 diff --git a/src/GitHub.Api/Tasks/OctorunTask.cs b/src/GitHub.Api/Tasks/OctorunTask.cs index b094bd6e7..be90c835a 100644 --- a/src/GitHub.Api/Tasks/OctorunTask.cs +++ b/src/GitHub.Api/Tasks/OctorunTask.cs @@ -60,8 +60,9 @@ class OctorunTask : ProcessTask private readonly NPath pathToOctorunJs; private readonly string arguments; - public OctorunTask(CancellationToken token, IKeychain keychain, IEnvironment environment, + public OctorunTask(CancellationToken token, IEnvironment environment, string arguments, + string userToken = null, IOutputProcessor processor = null) : base(token, processor ?? new OctorunResultOutputProcessor()) { @@ -71,24 +72,26 @@ public OctorunTask(CancellationToken token, IKeychain keychain, IEnvironment env this.pathToOctorunJs = environment.OctorunScriptPath; this.arguments = $"\"{pathToOctorunJs}\" {arguments}"; - var cloneUrl = environment.Repository?.CloneUrl; - var host = String.IsNullOrEmpty(cloneUrl) - ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) - : new UriString(cloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); - - var adapter = keychain.Connect(host); - if (adapter.Credential?.Token != null) - { - userToken = adapter.Credential.Token; - } - else - { - // use a cached adapter if there is one filled out - adapter = keychain.LoadFromSystem(host); - if (adapter != null) - userToken = adapter.Credential.Token; - } + this.userToken = userToken; + +// var cloneUrl = environment.Repository?.CloneUrl; +// var host = String.IsNullOrEmpty(cloneUrl) +// ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) +// : new UriString(cloneUrl.ToRepositoryUri() +// .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); +// +// var adapter = keychain.Connect(host); +// if (adapter.Credential?.Token != null) +// { +// userToken = adapter.Credential.Token; +// } +// else +// { +// // use a cached adapter if there is one filled out +// adapter = keychain.LoadFromSystem(host); +// if (adapter != null) +// userToken = adapter.Credential.Token; +// } } public override void Configure(ProcessStartInfo psi) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj index 0f2f0f5ad..f3371540b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.45.csproj @@ -86,6 +86,8 @@ + + diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj index 8588ccebf..545846943 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/GitHub.Unity.csproj @@ -77,6 +77,8 @@ + + diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index 5c6afae20..053f5badf 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -16,9 +16,10 @@ IEnvironment environment client = new ApiClient(host, keychain, processManager, taskManager, environment); } + public HostAddress HostAddress { get { return client.HostAddress; } } + public void Login(string username, string password, Action twofaRequired, Action authResult) { - loginResultData = null; client.Login(username, password, r => { @@ -27,11 +28,28 @@ public void Login(string username, string password, Action twofaRequired }, authResult); } + public void LoginWithToken(string token, Action twofaRequired, Action authResult) + { + Login("[token]", token, twofaRequired, authResult); + } + + public void LoginWith2fa(string code) { if (loginResultData == null) throw new InvalidOperationException("Call Login() first"); client.ContinueLogin(loginResultData, code); } + + public void GetServerMeta(Action serverMeta, Action error) + { + loginResultData = null; + client.GetServerMeta(data => + { + serverMeta(data); + }, exception => { + error(exception.Message); + }); + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 6082238cb..07d24dc8a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -10,253 +10,113 @@ class AuthenticationView : Subview { private static readonly Vector2 viewSize = new Vector2(290, 290); - private const string CredentialsNeedRefreshMessage = "We've detected that your stored credentials are out of sync with your current user. This can happen if you have signed in to git outside of Unity. Sign in again to refresh your credentials."; - private const string NeedAuthenticationMessage = "We need you to authenticate first"; private const string WindowTitle = "Authenticate"; - private const string UsernameLabel = "Username"; - private const string PasswordLabel = "Password"; - private const string TwofaLabel = "2FA Code"; - private const string LoginButton = "Sign in"; - private const string BackButton = "Back"; - private const string AuthTitle = "Sign in to GitHub"; - private const string TwofaTitle = "Two-Factor Authentication"; - private const string TwofaDescription = "Open the two-factor authentication app on your device to view your 2FA code and verify your identity."; - private const string TwofaButton = "Verify"; - - [SerializeField] private Vector2 scroll; - [SerializeField] private string username = string.Empty; - [SerializeField] private string two2fa = string.Empty; - [SerializeField] private string message; - [SerializeField] private string errorMessage; - [SerializeField] private bool need2fa; [NonSerialized] private bool isBusy; - [NonSerialized] private bool enterPressed; - [NonSerialized] private string password = string.Empty; - [NonSerialized] private AuthenticationService authenticationService; + [SerializeField] private SubTab changeTab = SubTab.GitHub; + [SerializeField] private SubTab activeTab = SubTab.GitHub; + + [SerializeField] private GitHubAuthenticationView gitHubAuthenticationView; + [SerializeField] private GitHubEnterpriseAuthenticationView gitHubEnterpriseAuthenticationView; public override void InitializeView(IView parent) { base.InitializeView(parent); - need2fa = isBusy = false; - message = errorMessage = null; Title = WindowTitle; Size = viewSize; + + gitHubAuthenticationView = gitHubAuthenticationView ?? new GitHubAuthenticationView(); + gitHubEnterpriseAuthenticationView = gitHubEnterpriseAuthenticationView ?? new GitHubEnterpriseAuthenticationView(); + + gitHubAuthenticationView.InitializeView(parent); + gitHubEnterpriseAuthenticationView.InitializeView(parent); } public void Initialize(Exception exception) { - var usernameMismatchException = exception as TokenUsernameMismatchException; - if (usernameMismatchException != null) - { - message = CredentialsNeedRefreshMessage; - username = usernameMismatchException.CachedUsername; - } - - var keychainEmptyException = exception as KeychainEmptyException; - if (keychainEmptyException != null) - { - message = NeedAuthenticationMessage; - } - - if (usernameMismatchException == null && keychainEmptyException == null) - { - message = exception.Message; - } + } public override void OnGUI() { - HandleEnterPressed(); - - EditorGUIUtility.labelWidth = 90f; - - scroll = GUILayout.BeginScrollView(scroll); - { - GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); - { - GUILayout.Label(AuthTitle, Styles.HeaderRepoLabelStyle); - } - GUILayout.EndHorizontal(); - - GUILayout.BeginVertical(); - { - if (!need2fa) - { - OnGUILogin(); - } - else - { - OnGUI2FA(); - } - } - - GUILayout.EndVertical(); - } - GUILayout.EndScrollView(); + DoToolbarGUI(); + ActiveView.OnGUI(); } - private void HandleEnterPressed() + public override bool IsBusy { - if (Event.current.type != EventType.KeyDown) - return; - - enterPressed = Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter; - if (enterPressed) - Event.current.Use(); + get { return isBusy; } } - private void OnGUILogin() + private static SubTab TabButton(SubTab tab, string title, SubTab currentTab) { - EditorGUI.BeginDisabledGroup(isBusy); - { - ShowMessage(); + return GUILayout.Toggle(currentTab == tab, title, EditorStyles.toolbarButton) ? tab : currentTab; + } - EditorGUILayout.Space(); + private enum SubTab + { + None, + GitHub, + GitHubEnterprise + } - GUILayout.BeginHorizontal(); + private void DoToolbarGUI() + { + GUILayout.BeginHorizontal(EditorStyles.toolbar); + { + EditorGUI.BeginChangeCheck(); { - username = EditorGUILayout.TextField(UsernameLabel ,username, Styles.TextFieldStyle); + changeTab = TabButton(SubTab.GitHub, "GitHub", changeTab); + changeTab = TabButton(SubTab.GitHubEnterprise, "GitHub Enterprise", changeTab); } - GUILayout.EndHorizontal(); - EditorGUILayout.Space(); - - GUILayout.BeginHorizontal(); + if (EditorGUI.EndChangeCheck()) { - password = EditorGUILayout.PasswordField(PasswordLabel, password, Styles.TextFieldStyle); + UpdateActiveTab(); } - GUILayout.EndHorizontal(); - - EditorGUILayout.Space(); - ShowErrorMessage(); - - GUILayout.Space(Styles.BaseSpacing + 3); - GUILayout.BeginHorizontal(); - { - GUILayout.FlexibleSpace(); - if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) - { - GUI.FocusControl(null); - isBusy = true; - AuthenticationService.Login(username, password, DoRequire2fa, DoResult); - } - } - GUILayout.EndHorizontal(); + GUILayout.FlexibleSpace(); } - EditorGUI.EndDisabledGroup(); + EditorGUILayout.EndHorizontal(); } - private void OnGUI2FA() + private void UpdateActiveTab() { - GUILayout.BeginVertical(); + if (changeTab != activeTab) { - GUILayout.Label(TwofaTitle, EditorStyles.boldLabel); - GUILayout.Label(TwofaDescription, EditorStyles.wordWrappedLabel); - - EditorGUI.BeginDisabledGroup(isBusy); - { - EditorGUILayout.Space(); - two2fa = EditorGUILayout.TextField(TwofaLabel, two2fa, Styles.TextFieldStyle); - EditorGUILayout.Space(); - ShowErrorMessage(); - - GUILayout.BeginHorizontal(); - { - GUILayout.FlexibleSpace(); - if (GUILayout.Button(BackButton)) - { - GUI.FocusControl(null); - Clear(); - } - - if (GUILayout.Button(TwofaButton) || (!isBusy && enterPressed)) - { - GUI.FocusControl(null); - isBusy = true; - AuthenticationService.LoginWith2fa(two2fa); - } - } - GUILayout.EndHorizontal(); - - EditorGUILayout.Space(); - } - EditorGUI.EndDisabledGroup(); + var fromView = ActiveView; + activeTab = changeTab; + var toView = ActiveView; + SwitchView(fromView, toView); } - GUILayout.EndVertical(); } - - private void DoRequire2fa(string msg) + private void SwitchView(Subview fromView, Subview toView) { - need2fa = true; - errorMessage = msg; - isBusy = false; - Redraw(); - } - - private void Clear() - { - need2fa = false; - errorMessage = null; - isBusy = false; - Redraw(); - } - - private void DoResult(bool success, string msg) - { - isBusy = false; - if (success) - { - UsageTracker.IncrementAuthenticationViewButtonAuthentication(); - - Clear(); - Finish(true); - } - else - { - errorMessage = msg; - Redraw(); - } - } + GUI.FocusControl(null); - private void ShowMessage() - { - if (message != null) - { - EditorGUILayout.HelpBox(message, MessageType.Warning); - } - } + if (fromView != null) + fromView.OnDisable(); + toView.OnEnable(); - private void ShowErrorMessage() - { - if (errorMessage != null) - { - EditorGUILayout.HelpBox(errorMessage, MessageType.Error); - } + // this triggers a repaint + Parent.Redraw(); } - private AuthenticationService AuthenticationService + private Subview ActiveView { get { - if (authenticationService == null) + switch (activeTab) { - UriString host = Repository != null ? Repository.CloneUrl : null; - AuthenticationService = new AuthenticationService(host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + case SubTab.GitHub: + return gitHubAuthenticationView; + case SubTab.GitHubEnterprise: + return gitHubEnterpriseAuthenticationView; + default: + throw new NotImplementedException(); } - return authenticationService; - } - set - { - authenticationService = value; } } - - public override bool IsBusy - { - get { return isBusy; } - } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs new file mode 100644 index 000000000..84264f67a --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -0,0 +1,262 @@ +using System; +using System.Threading; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + [Serializable] + class GitHubAuthenticationView : Subview + { + private static readonly Vector2 viewSize = new Vector2(290, 290); + + private const string CredentialsNeedRefreshMessage = "We've detected that your stored credentials are out of sync with your current user. This can happen if you have signed in to git outside of Unity. Sign in again to refresh your credentials."; + private const string NeedAuthenticationMessage = "We need you to authenticate first"; + private const string WindowTitle = "Authenticate"; + private const string UsernameLabel = "Username"; + private const string PasswordLabel = "Password"; + private const string TwofaLabel = "2FA Code"; + private const string LoginButton = "Sign in"; + private const string BackButton = "Back"; + private const string AuthTitle = "Sign in to GitHub"; + private const string TwofaTitle = "Two-Factor Authentication"; + private const string TwofaDescription = "Open the two-factor authentication app on your device to view your 2FA code and verify your identity."; + private const string TwofaButton = "Verify"; + + [SerializeField] private Vector2 scroll; + [SerializeField] private string username = string.Empty; + [SerializeField] private string two2fa = string.Empty; + [SerializeField] private string message; + [SerializeField] private string errorMessage; + [SerializeField] private bool need2fa; + + [NonSerialized] private bool isBusy; + [NonSerialized] private bool enterPressed; + [NonSerialized] private string password = string.Empty; + [NonSerialized] private AuthenticationService authenticationService; + + + public override void InitializeView(IView parent) + { + base.InitializeView(parent); + need2fa = isBusy = false; + message = errorMessage = null; + Title = WindowTitle; + Size = viewSize; + } + + public void Initialize(Exception exception) + { + var usernameMismatchException = exception as TokenUsernameMismatchException; + if (usernameMismatchException != null) + { + message = CredentialsNeedRefreshMessage; + username = usernameMismatchException.CachedUsername; + } + + var keychainEmptyException = exception as KeychainEmptyException; + if (keychainEmptyException != null) + { + message = NeedAuthenticationMessage; + } + + if (usernameMismatchException == null && keychainEmptyException == null) + { + message = exception.Message; + } + } + + public override void OnGUI() + { + HandleEnterPressed(); + + EditorGUIUtility.labelWidth = 90f; + + scroll = GUILayout.BeginScrollView(scroll); + { + GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); + { + GUILayout.Label(AuthTitle, Styles.HeaderRepoLabelStyle); + } + GUILayout.EndHorizontal(); + + GUILayout.BeginVertical(); + { + if (!need2fa) + { + OnGUILogin(); + } + else + { + OnGUI2FA(); + } + } + + GUILayout.EndVertical(); + } + GUILayout.EndScrollView(); + } + + private void HandleEnterPressed() + { + if (Event.current.type != EventType.KeyDown) + return; + + enterPressed = Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter; + if (enterPressed) + Event.current.Use(); + } + + private void OnGUILogin() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + username = EditorGUILayout.TextField(UsernameLabel ,username, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + password = EditorGUILayout.PasswordField(PasswordLabel, password, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + AuthenticationService.Login(username, password, DoRequire2fa, DoResult); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUI2FA() + { + GUILayout.BeginVertical(); + { + GUILayout.Label(TwofaTitle, EditorStyles.boldLabel); + GUILayout.Label(TwofaDescription, EditorStyles.wordWrappedLabel); + + EditorGUI.BeginDisabledGroup(isBusy); + { + EditorGUILayout.Space(); + two2fa = EditorGUILayout.TextField(TwofaLabel, two2fa, Styles.TextFieldStyle); + EditorGUILayout.Space(); + ShowErrorMessage(); + + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(BackButton)) + { + GUI.FocusControl(null); + Clear(); + } + + if (GUILayout.Button(TwofaButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + AuthenticationService.LoginWith2fa(two2fa); + } + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + } + EditorGUI.EndDisabledGroup(); + } + GUILayout.EndVertical(); + } + + private void DoRequire2fa(string msg) + { + need2fa = true; + errorMessage = msg; + isBusy = false; + Redraw(); + } + + private void Clear() + { + need2fa = false; + errorMessage = null; + isBusy = false; + Redraw(); + } + + private void DoResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + + private void ShowMessage() + { + if (message != null) + { + EditorGUILayout.HelpBox(message, MessageType.Warning); + } + } + + private void ShowErrorMessage() + { + if (errorMessage != null) + { + EditorGUILayout.HelpBox(errorMessage, MessageType.Error); + } + } + + private AuthenticationService AuthenticationService + { + get + { + if (authenticationService == null) + { + UriString host = Repository != null ? Repository.CloneUrl : null; + AuthenticationService = new AuthenticationService(host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + } + return authenticationService; + } + set + { + authenticationService = value; + } + } + + public override bool IsBusy + { + get { return isBusy; } + } + } +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs new file mode 100644 index 000000000..84c03c8bd --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -0,0 +1,388 @@ +using System; +using System.Threading; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + [Serializable] + class GitHubEnterpriseAuthenticationView : Subview + { + private static readonly Vector2 viewSize = new Vector2(290, 290); + + private const string CredentialsNeedRefreshMessage = "We've detected that your stored credentials are out of sync with your current user. This can happen if you have signed in to git outside of Unity. Sign in again to refresh your credentials."; + private const string NeedAuthenticationMessage = "We need you to authenticate first"; + private const string WindowTitle = "Authenticate"; + private const string ServerAddressLabel = "Server Address"; + private const string TokenLabel = "Token"; + private const string UsernameLabel = "Username"; + private const string PasswordLabel = "Password"; + private const string TwofaLabel = "2FA Code"; + private const string LoginButton = "Sign in"; + private const string BackButton = "Back"; + private const string AuthTitle = "Sign in to GitHub Enterprise"; + private const string TwofaTitle = "Two-Factor Authentication"; + private const string TwofaDescription = "Open the two-factor authentication app on your device to view your 2FA code and verify your identity."; + private const string TwofaButton = "Verify"; + + [SerializeField] private Vector2 scroll; + [SerializeField] private string serverAddress = string.Empty; + [SerializeField] private string username = string.Empty; + [SerializeField] private string two2fa = string.Empty; + [SerializeField] private string message; + [SerializeField] private string errorMessage; + [SerializeField] private bool need2fa; + [SerializeField] private bool hasServerMeta; + [SerializeField] private bool verifiablePasswordAuthentication; + + [NonSerialized] private bool isBusy; + [NonSerialized] private bool enterPressed; + [NonSerialized] private string password = string.Empty; + [NonSerialized] private string token = string.Empty; + [NonSerialized] private AuthenticationService authenticationService; + + public override void InitializeView(IView parent) + { + base.InitializeView(parent); + need2fa = isBusy = false; + message = errorMessage = null; + Title = WindowTitle; + Size = viewSize; + } + + public void Initialize(Exception exception) + { + var usernameMismatchException = exception as TokenUsernameMismatchException; + if (usernameMismatchException != null) + { + message = CredentialsNeedRefreshMessage; + username = usernameMismatchException.CachedUsername; + } + + var keychainEmptyException = exception as KeychainEmptyException; + if (keychainEmptyException != null) + { + message = NeedAuthenticationMessage; + } + + if (usernameMismatchException == null && keychainEmptyException == null) + { + message = exception.Message; + } + } + + public override void OnGUI() + { + HandleEnterPressed(); + + EditorGUIUtility.labelWidth = 90f; + + scroll = GUILayout.BeginScrollView(scroll); + { + GUILayout.BeginHorizontal(Styles.AuthHeaderBoxStyle); + { + GUILayout.Label(AuthTitle, Styles.HeaderRepoLabelStyle); + } + GUILayout.EndHorizontal(); + + GUILayout.BeginVertical(); + { + if (!hasServerMeta) + { + OnGUIHost(); + } + else + { + EditorGUILayout.Space(); + + EditorGUI.BeginDisabledGroup(true); + { + GUILayout.BeginHorizontal(); + { + serverAddress = EditorGUILayout.TextField(ServerAddressLabel, serverAddress, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + + if (!need2fa) + { + if (verifiablePasswordAuthentication) + { + OnGUIUserPasswordLogin(); + } + else + { + OnGUITokenLogin(); + } + } + else + { + OnGUI2FA(); + } + } + } + + GUILayout.EndVertical(); + } + GUILayout.EndScrollView(); + } + + private void HandleEnterPressed() + { + if (Event.current.type != EventType.KeyDown) + return; + + enterPressed = Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter; + if (enterPressed) + Event.current.Use(); + } + + private void OnGUIHost() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + serverAddress = EditorGUILayout.TextField(ServerAddressLabel, serverAddress, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + errorMessage = null; + isBusy = true; + + GetAuthenticationService(serverAddress) + .GetServerMeta(DoServerMetaResult, DoServerMetaError); + + Redraw(); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUIUserPasswordLogin() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + username = EditorGUILayout.TextField(UsernameLabel ,username, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + password = EditorGUILayout.PasswordField(PasswordLabel, password, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + GetAuthenticationService(serverAddress) + .Login(username, password, DoRequire2fa, DoResult); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUITokenLogin() + { + EditorGUI.BeginDisabledGroup(isBusy); + { + ShowMessage(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + { + token = EditorGUILayout.TextField(TokenLabel, token, Styles.TextFieldStyle); + } + GUILayout.EndHorizontal(); + + ShowErrorMessage(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Back")) + { + GUI.FocusControl(null); + + hasServerMeta = false; + Redraw(); + } + + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + GetAuthenticationService(serverAddress) + .LoginWithToken(token, DoRequire2fa, DoResult); + } + } + GUILayout.EndHorizontal(); + } + EditorGUI.EndDisabledGroup(); + } + + private void OnGUI2FA() + { + GUILayout.BeginVertical(); + { + GUILayout.Label(TwofaTitle, EditorStyles.boldLabel); + GUILayout.Label(TwofaDescription, EditorStyles.wordWrappedLabel); + + EditorGUI.BeginDisabledGroup(isBusy); + { + EditorGUILayout.Space(); + two2fa = EditorGUILayout.TextField(TwofaLabel, two2fa, Styles.TextFieldStyle); + + ShowErrorMessage(); + + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button(BackButton)) + { + GUI.FocusControl(null); + Clear(); + } + + if (GUILayout.Button(TwofaButton) || (!isBusy && enterPressed)) + { + GUI.FocusControl(null); + isBusy = true; + GetAuthenticationService(serverAddress) + .LoginWith2fa(two2fa); + } + } + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + } + EditorGUI.EndDisabledGroup(); + } + GUILayout.EndVertical(); + } + + private void DoServerMetaResult(GitHubHostMeta gitHubHostMeta) + { + hasServerMeta = true; + verifiablePasswordAuthentication = gitHubHostMeta.VerifiablePasswordAuthentication; + isBusy = false; + Redraw(); + } + + private void DoServerMetaError(string message) + { + errorMessage = message; + hasServerMeta = false; + isBusy = false; + Redraw(); + } + + private void DoRequire2fa(string msg) + { + need2fa = true; + errorMessage = msg; + isBusy = false; + Redraw(); + } + + private void Clear() + { + need2fa = false; + errorMessage = null; + isBusy = false; + Redraw(); + } + + private void DoResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + + private void ShowMessage() + { + if (message != null) + { + EditorGUILayout.HelpBox(message, MessageType.Warning); + } + } + + private void ShowErrorMessage() + { + if (errorMessage != null) + { + EditorGUILayout.Space(); + + EditorGUILayout.HelpBox(errorMessage, MessageType.Error); + } + } + + private AuthenticationService GetAuthenticationService(string host) + { + if (authenticationService == null || authenticationService.HostAddress.WebUri.Host != host) + { + authenticationService + = new AuthenticationService( + host, + Platform.Keychain, + Manager.ProcessManager, + Manager.TaskManager, + Environment); + } + + return authenticationService; + } + + public override bool IsBusy + { + get { return isBusy; } + } + } +} diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta index 1c1d85763..c0727ba52 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta @@ -1,34 +1,32 @@ fileFormatVersion: 2 guid: d516f2a1bec6a9645a084ef8c9237132 -timeCreated: 1491391262 +timeCreated: 1539278074 licenseType: Free PluginImporter: + externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - data: - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta index d12a12326..6cddd7b96 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta @@ -1,34 +1,32 @@ fileFormatVersion: 2 guid: c743ae24ee231884887054d20ccdd0ae -timeCreated: 1491391261 +timeCreated: 1539278075 licenseType: Free PluginImporter: + externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - data: - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta index a70aca527..4520eb5a5 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta @@ -1,34 +1,32 @@ fileFormatVersion: 2 guid: 68c7e4565cde54155bb78d8e935f1dd4 -timeCreated: 1527097377 +timeCreated: 1539278078 licenseType: Free PluginImporter: + externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - data: - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta index 98b231bec..ae75f164a 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta @@ -1,34 +1,32 @@ fileFormatVersion: 2 guid: 48c22d5d7479fcb49ab3be0cdd2ccec0 -timeCreated: 1491391260 +timeCreated: 1539278074 licenseType: Free PluginImporter: + externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - data: - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta index ea6a32d4c..01e7881ac 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta @@ -1,34 +1,32 @@ fileFormatVersion: 2 guid: 790749ba7e4b18141953e39cb13f1b79 -timeCreated: 1491392717 +timeCreated: 1539278074 licenseType: Free PluginImporter: + externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - data: - first: - Any: - second: - enabled: 0 - settings: {} - data: - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - data: - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/ProjectSettings/DynamicsManager.asset b/unity/PackageProject/ProjectSettings/DynamicsManager.asset index 6be69106a..0be3d787c 100644 --- a/unity/PackageProject/ProjectSettings/DynamicsManager.asset +++ b/unity/PackageProject/ProjectSettings/DynamicsManager.asset @@ -16,3 +16,5 @@ PhysicsManager: m_EnableAdaptiveForce: 0 m_EnablePCM: 1 m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 1 diff --git a/unity/PackageProject/ProjectSettings/Physics2DSettings.asset b/unity/PackageProject/ProjectSettings/Physics2DSettings.asset index dd4738c2f..132ee6bc8 100644 --- a/unity/PackageProject/ProjectSettings/Physics2DSettings.asset +++ b/unity/PackageProject/ProjectSettings/Physics2DSettings.asset @@ -3,7 +3,7 @@ --- !u!19 &1 Physics2DSettings: m_ObjectHideFlags: 0 - serializedVersion: 2 + serializedVersion: 3 m_Gravity: {x: 0, y: -9.81} m_DefaultMaterial: {fileID: 0} m_VelocityIterations: 8 @@ -13,15 +13,18 @@ Physics2DSettings: m_MaxAngularCorrection: 8 m_MaxTranslationSpeed: 100 m_MaxRotationSpeed: 360 - m_MinPenetrationForPenalty: 0.01 m_BaumgarteScale: 0.2 m_BaumgarteTimeOfImpactScale: 0.75 m_TimeToSleep: 0.5 m_LinearSleepTolerance: 0.01 m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_AutoSimulation: 1 m_QueriesHitTriggers: 1 m_QueriesStartInColliders: 1 m_ChangeStopsCallbacks: 0 + m_CallbacksOnDisable: 1 + m_AutoSyncTransforms: 1 m_AlwaysShowColliders: 0 m_ShowColliderSleep: 1 m_ShowColliderContacts: 0 diff --git a/unity/TestProject/ProjectSettings/DynamicsManager.asset b/unity/TestProject/ProjectSettings/DynamicsManager.asset index 6be69106a..0be3d787c 100644 --- a/unity/TestProject/ProjectSettings/DynamicsManager.asset +++ b/unity/TestProject/ProjectSettings/DynamicsManager.asset @@ -16,3 +16,5 @@ PhysicsManager: m_EnableAdaptiveForce: 0 m_EnablePCM: 1 m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_AutoSimulation: 1 + m_AutoSyncTransforms: 1 diff --git a/unity/TestProject/ProjectSettings/Physics2DSettings.asset b/unity/TestProject/ProjectSettings/Physics2DSettings.asset index dd4738c2f..132ee6bc8 100644 --- a/unity/TestProject/ProjectSettings/Physics2DSettings.asset +++ b/unity/TestProject/ProjectSettings/Physics2DSettings.asset @@ -3,7 +3,7 @@ --- !u!19 &1 Physics2DSettings: m_ObjectHideFlags: 0 - serializedVersion: 2 + serializedVersion: 3 m_Gravity: {x: 0, y: -9.81} m_DefaultMaterial: {fileID: 0} m_VelocityIterations: 8 @@ -13,15 +13,18 @@ Physics2DSettings: m_MaxAngularCorrection: 8 m_MaxTranslationSpeed: 100 m_MaxRotationSpeed: 360 - m_MinPenetrationForPenalty: 0.01 m_BaumgarteScale: 0.2 m_BaumgarteTimeOfImpactScale: 0.75 m_TimeToSleep: 0.5 m_LinearSleepTolerance: 0.01 m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_AutoSimulation: 1 m_QueriesHitTriggers: 1 m_QueriesStartInColliders: 1 m_ChangeStopsCallbacks: 0 + m_CallbacksOnDisable: 1 + m_AutoSyncTransforms: 1 m_AlwaysShowColliders: 0 m_ShowColliderSleep: 1 m_ShowColliderContacts: 0 diff --git a/unity/TestProject/ProjectSettings/ProjectSettings.asset b/unity/TestProject/ProjectSettings/ProjectSettings.asset index 4a6b72fcf..019193686 100644 --- a/unity/TestProject/ProjectSettings/ProjectSettings.asset +++ b/unity/TestProject/ProjectSettings/ProjectSettings.asset @@ -3,9 +3,10 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 10 + serializedVersion: 13 productGUID: 0190cf875796f4b46a0ef8d5e39cdfd9 AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 defaultScreenOrientation: 4 targetDevice: 2 useOnDemandResources: 0 @@ -14,7 +15,7 @@ PlayerSettings: productName: UnityProject defaultCursor: {fileID: 0} cursorHotspot: {x: 0, y: 0} - m_SplashScreenBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21176471, a: 1} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} m_ShowUnitySplashScreen: 1 m_ShowUnitySplashLogo: 1 m_SplashScreenOverlayOpacity: 1 @@ -38,8 +39,6 @@ PlayerSettings: width: 1 height: 1 m_SplashScreenLogos: [] - m_SplashScreenBackgroundLandscape: {fileID: 0} - m_SplashScreenBackgroundPortrait: {fileID: 0} m_VirtualRealitySplashScreen: {fileID: 0} m_HolographicTrackingLossScreen: {fileID: 0} defaultScreenWidth: 1024 @@ -49,7 +48,6 @@ PlayerSettings: m_StereoRenderingPath: 0 m_ActiveColorSpace: 0 m_MTRendering: 1 - m_MobileMTRendering: 0 m_StackTraceTypes: 010000000100000001000000010000000100000001000000 iosShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1 @@ -64,18 +62,22 @@ PlayerSettings: useOSAutorotation: 1 use32BitDisplayBuffer: 1 disableDepthAndStencilBuffers: 0 + androidBlitType: 0 defaultIsFullScreen: 1 defaultIsNativeResolution: 1 + macRetinaSupport: 1 runInBackground: 0 captureSingleScreen: 0 muteOtherAudioSources: 0 Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 submitAnalytics: 1 usePlayerLog: 1 bakeCollisionMeshes: 0 forceSingleInstance: 0 resizableWindow: 0 useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games gpuSkinning: 0 graphicsJobs: 0 xboxPIXTextureCapture: 0 @@ -85,6 +87,7 @@ PlayerSettings: xboxEnableFitness: 0 visibleInBackground: 0 allowFullscreenSwitch: 1 + graphicsJobMode: 0 macFullscreenMode: 2 d3d9FullscreenMode: 1 d3d11FullscreenMode: 1 @@ -92,14 +95,16 @@ PlayerSettings: xboxEnableHeadOrientation: 0 xboxEnableGuest: 0 xboxEnablePIXSampling: 0 + metalFramebufferOnly: 0 n3dsDisableStereoscopicView: 0 n3dsEnableSharedListOpt: 1 n3dsEnableVSync: 0 - uiUse16BitDepthBuffer: 0 ignoreAlphaClear: 0 xboxOneResolution: 0 xboxOneMonoLoggingLevel: 0 xboxOneLoggingLevel: 1 + xboxOneDisableEsram: 0 + xboxOnePresentImmediateThreshold: 0 videoMemoryForVertexBuffers: 0 psp2PowerMode: 0 psp2AcquireBGM: 1 @@ -118,36 +123,60 @@ PlayerSettings: 16:10: 1 16:9: 1 Others: 1 - bundleIdentifier: com.Company.ProductName bundleVersion: 1.0 preloadedAssets: [] metroInputSource: 0 m_HolographicPauseOnTrackingLoss: 1 xboxOneDisableKinectGpuReservation: 0 + xboxOneEnable7thCore: 0 + vrSettings: + cardboard: + depthFormat: 0 + enableTransitionView: 0 + daydream: + depthFormat: 0 + useSustainedPerformanceMode: 0 + enableVideoLayer: 0 + useProtectedVideoMemory: 0 + hololens: + depthFormat: 1 protectGraphicsMemory: 0 + useHDRDisplay: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 0 + resolutionScalingMode: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: + Android: com.Company.ProductName + Standalone: unity.DefaultCompany.UnityProject + Tizen: com.Company.ProductName + iOS: com.Company.ProductName + tvOS: com.Company.ProductName + buildNumber: + iOS: 0 AndroidBundleVersionCode: 1 - AndroidMinSdkVersion: 9 + AndroidMinSdkVersion: 16 + AndroidTargetSdkVersion: 0 AndroidPreferredInstallLocation: 1 aotOptions: - apiCompatibilityLevel: 2 stripEngineCode: 1 iPhoneStrippingLevel: 0 iPhoneScriptCallOptimization: 0 - iPhoneBuildNumber: 0 ForceInternetPermission: 0 ForceSDCardPermission: 0 CreateWallpaper: 0 APKExpansionFiles: 0 - preloadShaders: 0 + keepLoadedShadersAlive: 0 StripUnusedMeshComponents: 0 VertexChannelCompressionMask: serializedVersion: 2 m_Bits: 238 iPhoneSdkVersion: 988 - iOSTargetOSVersionString: + iOSTargetOSVersionString: 7.0 tvOSSdkVersion: 0 tvOSRequireExtendedGameController: 0 - tvOSTargetOSVersionString: + tvOSTargetOSVersionString: 9.0 uIPrerenderedIcon: 0 uIRequiresPersistentWiFi: 0 uIRequiresFullScreen: 1 @@ -190,7 +219,13 @@ PlayerSettings: iOSURLSchemes: [] iOSBackgroundModes: 0 iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + iOSRenderExtraFrameOnPause: 1 appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + appleEnableAutomaticSigning: 0 AndroidTargetDevice: 0 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} @@ -198,7 +233,9 @@ PlayerSettings: AndroidKeyaliasName: AndroidTVCompatibility: 1 AndroidIsGame: 1 + AndroidEnableTango: 0 androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 m_AndroidBanners: - width: 320 height: 180 @@ -209,10 +246,13 @@ PlayerSettings: m_BuildTargetBatching: [] m_BuildTargetGraphicsAPIs: [] m_BuildTargetVRSettings: [] + m_BuildTargetEnableVuforiaSettings: [] openGLRequireES31: 0 openGLRequireES31AEP: 0 - webPlayerTemplate: APPLICATION:Default m_TemplateCustomTags: {} + mobileMTRendering: + iPhone: 1 + tvOS: 1 wiiUTitleID: 0005000011000000 wiiUGroupID: 00010000 wiiUCommonSaveSize: 4096 @@ -231,6 +271,7 @@ PlayerSettings: wiiUGamePadStartupScreen: {fileID: 0} wiiUDrcBufferDisabled: 0 wiiUProfilerLibPath: + playModeTestRunnerEnabled: 0 actionOnDotNetUnhandledException: 1 enableInternalProfiler: 0 logObjCUncaughtExceptions: 1 @@ -238,16 +279,116 @@ PlayerSettings: cameraUsageDescription: locationUsageDescription: microphoneUsageDescription: - XboxTitleId: - XboxImageXexPath: - XboxSpaPath: - XboxGenerateSpa: 0 - XboxDeployKinectResources: 0 - XboxSplashScreen: {fileID: 0} - xboxEnableSpeech: 0 - xboxAdditionalTitleMemorySize: 0 - xboxDeployKinectHeadOrientation: 0 - xboxDeployKinectHeadPosition: 0 + switchNetLibKey: + switchSocketMemoryPoolSize: 6144 + switchSocketAllocatorPoolSize: 128 + switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 + switchUseCPUProfiler: 0 + switchApplicationID: 0x01004b9000490000 + switchNSODependencies: + switchTitleNames_0: + switchTitleNames_1: + switchTitleNames_2: + switchTitleNames_3: + switchTitleNames_4: + switchTitleNames_5: + switchTitleNames_6: + switchTitleNames_7: + switchTitleNames_8: + switchTitleNames_9: + switchTitleNames_10: + switchTitleNames_11: + switchPublisherNames_0: + switchPublisherNames_1: + switchPublisherNames_2: + switchPublisherNames_3: + switchPublisherNames_4: + switchPublisherNames_5: + switchPublisherNames_6: + switchPublisherNames_7: + switchPublisherNames_8: + switchPublisherNames_9: + switchPublisherNames_10: + switchPublisherNames_11: + switchIcons_0: {fileID: 0} + switchIcons_1: {fileID: 0} + switchIcons_2: {fileID: 0} + switchIcons_3: {fileID: 0} + switchIcons_4: {fileID: 0} + switchIcons_5: {fileID: 0} + switchIcons_6: {fileID: 0} + switchIcons_7: {fileID: 0} + switchIcons_8: {fileID: 0} + switchIcons_9: {fileID: 0} + switchIcons_10: {fileID: 0} + switchIcons_11: {fileID: 0} + switchSmallIcons_0: {fileID: 0} + switchSmallIcons_1: {fileID: 0} + switchSmallIcons_2: {fileID: 0} + switchSmallIcons_3: {fileID: 0} + switchSmallIcons_4: {fileID: 0} + switchSmallIcons_5: {fileID: 0} + switchSmallIcons_6: {fileID: 0} + switchSmallIcons_7: {fileID: 0} + switchSmallIcons_8: {fileID: 0} + switchSmallIcons_9: {fileID: 0} + switchSmallIcons_10: {fileID: 0} + switchSmallIcons_11: {fileID: 0} + switchManualHTML: + switchAccessibleURLs: + switchLegalInformation: + switchMainThreadStackSize: 1048576 + switchPresenceGroupId: + switchLogoHandling: 0 + switchReleaseVersion: 0 + switchDisplayVersion: 1.0.0 + switchStartupUserAccount: 0 + switchTouchScreenUsage: 0 + switchSupportedLanguagesMask: 0 + switchLogoType: 0 + switchApplicationErrorCodeCategory: + switchUserAccountSaveDataSize: 0 + switchUserAccountSaveDataJournalSize: 0 + switchApplicationAttribute: 0 + switchCardSpecSize: -1 + switchCardSpecClock: -1 + switchRatingsMask: 0 + switchRatingsInt_0: 0 + switchRatingsInt_1: 0 + switchRatingsInt_2: 0 + switchRatingsInt_3: 0 + switchRatingsInt_4: 0 + switchRatingsInt_5: 0 + switchRatingsInt_6: 0 + switchRatingsInt_7: 0 + switchRatingsInt_8: 0 + switchRatingsInt_9: 0 + switchRatingsInt_10: 0 + switchRatingsInt_11: 0 + switchLocalCommunicationIds_0: + switchLocalCommunicationIds_1: + switchLocalCommunicationIds_2: + switchLocalCommunicationIds_3: + switchLocalCommunicationIds_4: + switchLocalCommunicationIds_5: + switchLocalCommunicationIds_6: + switchLocalCommunicationIds_7: + switchParentalControl: 0 + switchAllowsScreenshot: 1 + switchDataLossConfirmation: 0 + switchSupportedNpadStyles: 3 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchPlayerConnectionEnabled: 1 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -260,6 +401,7 @@ PlayerSettings: ps4ParamSfxPath: ps4VideoOutPixelFormat: 0 ps4VideoOutInitialWidth: 1920 + ps4VideoOutBaseModeInitialWidth: 1920 ps4VideoOutReprojectionRate: 120 ps4PronunciationXMLPath: ps4PronunciationSIGPath: @@ -282,8 +424,8 @@ PlayerSettings: ps4ApplicationParam4: 0 ps4DownloadDataSize: 0 ps4GarlicHeapSize: 2048 + ps4ProGarlicHeapSize: 2560 ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ - ps4UseDebugIl2cppLibs: 0 ps4pnSessions: 1 ps4pnPresence: 1 ps4pnFriends: 1 @@ -307,6 +449,9 @@ PlayerSettings: ps4attribShareSupport: 0 ps4attribExclusiveVR: 0 ps4disableAutoHideSplash: 0 + ps4videoRecordingFeaturesUsed: 0 + ps4contentSearchFeaturesUsed: 0 + ps4attribEyeToEyeDistanceSettingVR: 0 ps4IncludedModules: [] monoEnv: psp2Splashimage: {fileID: 0} @@ -355,13 +500,14 @@ PlayerSettings: psp2UseLibLocation: 0 psp2InfoBarOnStartup: 0 psp2InfoBarColor: 0 - psp2UseDebugIl2cppLibs: 0 + psp2ScriptOptimizationLevel: 0 psmSplashimage: {fileID: 0} splashScreenBackgroundSourceLandscape: {fileID: 0} splashScreenBackgroundSourcePortrait: {fileID: 0} spritePackerPolicy: webGLMemorySize: 256 webGLExceptionSupport: 1 + webGLNameFilesAsHashes: 0 webGLDataCaching: 0 webGLDebugSymbols: 0 webGLEmscriptenArgs: @@ -376,6 +522,8 @@ PlayerSettings: scriptingBackend: {} incrementalIl2cppBuild: {} additionalIl2CppArgs: + scriptingRuntimeVersion: 0 + apiCompatibilityLevelPerPlatform: {} m_RenderingPath: 1 m_MobileRenderingPath: 1 metroPackageName: UnityProject @@ -411,7 +559,7 @@ PlayerSettings: tizenMicrophonePermissions: 0 tizenDeploymentTarget: tizenDeploymentTargetType: 0 - tizenMinOSVersion: 0 + tizenMinOSVersion: 1 n3dsUseExtSaveData: 0 n3dsCompressStaticMem: 1 n3dsExtSaveDataNumber: 0x12345 @@ -451,9 +599,17 @@ PlayerSettings: XboxOneSplashScreen: {fileID: 0} XboxOneAllowedProductIds: [] XboxOnePersistentLocalStorageSize: 0 - vrEditorSettings: {} + xboxOneScriptCompiler: 0 + vrEditorSettings: + daydream: + daydreamIconForeground: {fileID: 0} + daydreamIconBackground: {fileID: 0} cloudServicesEnabled: {} + facebookSdkVersion: 7.9.4 + apiCompatibilityLevel: 2 cloudProjectId: projectName: organizationId: cloudEnabled: 0 + enableNativePlatformBackendsForNewInputSystem: 0 + disableOldInputManagerSupport: 0 diff --git a/unity/TestProject/ProjectSettings/ProjectVersion.txt b/unity/TestProject/ProjectSettings/ProjectVersion.txt index 66e05aa78..7a6fffb8b 100644 --- a/unity/TestProject/ProjectSettings/ProjectVersion.txt +++ b/unity/TestProject/ProjectSettings/ProjectVersion.txt @@ -1 +1 @@ -m_EditorVersion: 5.5.0f3 +m_EditorVersion: 2017.2.0f3 From 1789e9da1352c390f7f43bd3d7d5b6bd505e06a4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 15 Oct 2018 13:18:40 -0400 Subject: [PATCH 452/567] Ability to login with a token --- octorun/src/octokit.js | 5 +-- octorun/version | 2 +- src/GitHub.Api/Application/ApiClient.cs | 21 +++++++++++ src/GitHub.Api/Application/IApiClient.cs | 1 + .../Authentication/ILoginManager.cs | 4 +++ src/GitHub.Api/Authentication/LoginManager.cs | 35 ++++++++++++++++--- .../Services/AuthenticationService.cs | 5 ++- .../UI/GitHubEnterpriseAuthenticationView.cs | 19 +++++++++- 8 files changed, 81 insertions(+), 11 deletions(-) diff --git a/octorun/src/octokit.js b/octorun/src/octokit.js index fefde29e7..b0ab0a42f 100644 --- a/octorun/src/octokit.js +++ b/octorun/src/octokit.js @@ -9,8 +9,9 @@ var createOctokit = function (appName, host) { } }; - if(host) { - octokitConfiguration.baseUrl = "https://" + host; + if (host) { + octokitConfiguration.host = host; + octokitConfiguration.pathPrefix = 'api/v3'; } return Octokit(octokitConfiguration); diff --git a/octorun/version b/octorun/version index 3bf708bc6..15585c9c2 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -902910f45 \ No newline at end of file +902910f46 \ No newline at end of file diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 22cdcc6fb..334920573 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -248,6 +248,27 @@ public void GetCurrentUser(Action onSuccess, Action onErr .Start(); } + public void LoginWithToken(string token, Action result) + { + Guard.ArgumentNotNull(token, "token"); + Guard.ArgumentNotNull(result, "result"); + + new FuncTask(taskManager.Token, + () => loginManager.LoginWithToken(OriginalUrl, token)) + .FinallyInUI((success, ex, res) => + { + if (!success) + { + logger.Warning(ex); + result(false); + return; + } + + result(res); + }) + .Start(); + } + public void Login(string username, string password, Action need2faCode, Action result) { Guard.ArgumentNotNull(need2faCode, "need2faCode"); diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index 0f4c3270f..a5bf88323 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -11,6 +11,7 @@ void CreateRepository(string name, string description, bool isPrivate, void GetOrganizations(Action onSuccess, Action onError = null); void Login(string username, string password, Action need2faCode, Action result); void ContinueLogin(LoginResult loginResult, string code); + void LoginWithToken(string token, Action result); ITask Logout(UriString host); void GetCurrentUser(Action onSuccess, Action onError = null); void GetServerMeta(Action onSuccess, Action onError = null); diff --git a/src/GitHub.Api/Authentication/ILoginManager.cs b/src/GitHub.Api/Authentication/ILoginManager.cs index 41a9704e4..7235d7fac 100644 --- a/src/GitHub.Api/Authentication/ILoginManager.cs +++ b/src/GitHub.Api/Authentication/ILoginManager.cs @@ -27,5 +27,9 @@ interface ILoginManager /// The address of the server. /// ITask Logout(UriString hostAddress); + + bool LoginWithToken( + UriString host, + string token); } } diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 2f6cf5977..d4edce511 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -44,6 +44,33 @@ public LoginManager( this.environment = environment; } + public bool LoginWithToken( + UriString host, + string token) + { + Guard.ArgumentNotNull(host, nameof(host)); + Guard.ArgumentNotNullOrWhiteSpace(token, nameof(token)); + + var keychainAdapter = keychain.Connect(host); + keychainAdapter.Set(new Credential(host, "[token]", token)); + + try + { + var username = RetrieveUsername(token, host); + keychainAdapter.Update(token, username); + keychain.SaveToSystem(host); + + return true; + } + catch (Exception e) + { + logger.Warning(e, "Login Exception"); + + keychain.Clear(host, false); + return false; + } + } + /// public LoginResultData Login( UriString host, @@ -73,7 +100,7 @@ public LoginResultData Login( if (loginResultData.Code == LoginResultCodes.Success) { - username = RetrieveUsername(loginResultData.Token, username, host); + username = RetrieveUsername(loginResultData.Token, host, username); keychainAdapter.Update(loginResultData.Token, username); keychain.SaveToSystem(host); } @@ -113,7 +140,7 @@ public LoginResultData ContinueLogin(LoginResultData loginResultData, string two } keychainAdapter.Update(loginResultData.Token, username); - username = RetrieveUsername(loginResultData.Token, username, host); + username = RetrieveUsername(loginResultData.Token, host, username); keychainAdapter.Update(loginResultData.Token, username); keychain.SaveToSystem(host); @@ -180,9 +207,9 @@ private LoginResultData TryLogin( return new LoginResultData(LoginResultCodes.Failed, ret.GetApiErrorMessage() ?? "Failed.", host); } - private string RetrieveUsername(string token, string username, UriString host) + private string RetrieveUsername(string token, UriString host, string username = null) { - if (!username.Contains("@")) + if (username != null && !username.Contains("@")) { return username; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index 053f5badf..3501662f8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -28,12 +28,11 @@ public void Login(string username, string password, Action twofaRequired }, authResult); } - public void LoginWithToken(string token, Action twofaRequired, Action authResult) + public void LoginWithToken(string token, Action authResult) { - Login("[token]", token, twofaRequired, authResult); + client.LoginWithToken(token, authResult); } - public void LoginWith2fa(string code) { if (loginResultData == null) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs index 84c03c8bd..3d5d3ff93 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -249,7 +249,7 @@ private void OnGUITokenLogin() GUI.FocusControl(null); isBusy = true; GetAuthenticationService(serverAddress) - .LoginWithToken(token, DoRequire2fa, DoResult); + .LoginWithToken(token, DoTokenResult); } } GUILayout.EndHorizontal(); @@ -346,6 +346,23 @@ private void DoResult(bool success, string msg) } } + private void DoTokenResult(bool success) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = "Error validating token."; + Redraw(); + } + } + private void ShowMessage() { if (message != null) From e160b6fbb57410fc7074abba0619bd05603668b7 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 17 Oct 2018 07:46:06 -0400 Subject: [PATCH 453/567] Progress --- octorun/version | 2 +- src/GitHub.Api/Application/ApiClient.cs | 51 +++++++++------- .../Application/ApplicationManagerBase.cs | 2 + src/GitHub.Api/Application/IApiClient.cs | 3 +- src/GitHub.Api/Authentication/LoginManager.cs | 21 +++++-- src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Metrics/UsageTracker.cs | 2 +- src/GitHub.Api/Primitives/HostAddress.cs | 19 ++++-- src/GitHub.Api/Resources/octorun.zip | 4 +- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- .../Services/AuthenticationService.cs | 6 +- .../GitHub.Unity/UI/AuthenticationView.cs | 39 ++++++++++-- .../Editor/GitHub.Unity/UI/BaseWindow.cs | 2 +- .../Editor/GitHub.Unity/UI/PopupWindow.cs | 60 ++++++++++++------- .../Editor/GitHub.Unity/UI/PublishView.cs | 32 +++++++--- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 54 ++++++++++++++--- 16 files changed, 216 insertions(+), 85 deletions(-) diff --git a/octorun/version b/octorun/version index 15585c9c2..55d6b723d 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -902910f46 \ No newline at end of file +902910f47 \ No newline at end of file diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 334920573..7b77432b5 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -16,7 +16,6 @@ class ApiClient : IApiClient private static readonly Regex httpStatusErrorRegex = new Regex("(?<=[a-z])([A-Z])", RegexOptions.Compiled); public HostAddress HostAddress { get; } - public UriString OriginalUrl { get; } private readonly IKeychain keychain; private readonly IProcessManager processManager; @@ -24,19 +23,20 @@ class ApiClient : IApiClient private readonly ILoginManager loginManager; private readonly IEnvironment environment; - public ApiClient(UriString hostUrl, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment) + public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment): + this(UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri), keychain, processManager, taskManager, environment) { - Guard.ArgumentNotNull(keychain, nameof(keychain)); + } - var host = String.IsNullOrEmpty(hostUrl) - ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) - : new UriString(hostUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + public ApiClient(UriString host, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment) + { + Guard.ArgumentNotNull(host, nameof(host)); + Guard.ArgumentNotNull(keychain, nameof(keychain)); + host = new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); HostAddress = HostAddress.Create(host); - OriginalUrl = host; - logger.Trace("OriginalUrl: {1}", HostAddress.ToString(), OriginalUrl.ToString()); + logger.Trace("OriginalUrl: {0}", HostAddress.ApiUri.Host); this.keychain = keychain; this.processManager = processManager; @@ -60,8 +60,13 @@ public void CreateRepository(string name, string description, bool isPrivate, // this validates the user, again GetCurrentUser(); - var command = new StringBuilder("publish -h "); - command.Append(OriginalUrl.Host); + var command = new StringBuilder("publish"); + + if (!HostAddress.IsGitHubDotCom()) + { + command.Append(" -h "); + command.Append(HostAddress.ApiUri.Host); + } command.Append(" -r \""); command.Append(name); @@ -86,7 +91,7 @@ public void CreateRepository(string name, string description, bool isPrivate, command.Append(" -p"); } - var adapter = keychain.Connect(OriginalUrl); + var adapter = keychain.Connect(HostAddress.ApiUri.Host); if (adapter.Credential == null) { throw new ApiClientException("No Credentials found"); @@ -120,12 +125,12 @@ public void CreateRepository(string name, string description, bool isPrivate, .Start(); } - public void GetServerMeta(Action onSuccess, Action onError = null) + public void GetEnterpriseServerMeta(Action onSuccess, Action onError = null) { Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); new FuncTask(taskManager.Token, () => { - var octorunTask = new OctorunTask(taskManager.Token, environment, "meta -h " + OriginalUrl.Host) + var octorunTask = new OctorunTask(taskManager.Token, environment, "meta -h " + HostAddress.ApiUri.Host) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -133,7 +138,7 @@ public void GetServerMeta(Action onSuccess, Action on { var deserializeObject = SimpleJson.DeserializeObject>(ret.Output[0]); - return new GitHubHostMeta() + return new GitHubHostMeta { InstalledVersion = (string)deserializeObject["installed_version"], GithubServicesSha = (string)deserializeObject["github_services_sha"], @@ -194,14 +199,15 @@ public void GetOrganizations(Action onSuccess, Action Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); new FuncTask(taskManager.Token, () => { - var adapter = keychain.Connect(OriginalUrl); + var adapter = keychain.Connect(HostAddress.ApiUri.Host); if (adapter.Credential == null) { throw new ApiClientException("No Credentials found"); } + var command = HostAddress.IsGitHubDotCom() ? "organizations" : "organizations -h " + HostAddress.ApiUri.Host; var octorunTask = new OctorunTask(taskManager.Token, environment, - "organizations -h " + OriginalUrl.Host, adapter.Credential.Token) + command, adapter.Credential.Token) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -254,7 +260,7 @@ public void LoginWithToken(string token, Action result) Guard.ArgumentNotNull(result, "result"); new FuncTask(taskManager.Token, - () => loginManager.LoginWithToken(OriginalUrl, token)) + () => loginManager.LoginWithToken(HostAddress.ApiUri.Host, token)) .FinallyInUI((success, ex, res) => { if (!success) @@ -275,7 +281,7 @@ public void Login(string username, string password, Action need2faC Guard.ArgumentNotNull(result, "result"); new FuncTask(taskManager.Token, - () => loginManager.Login(OriginalUrl, username, password)) + () => loginManager.Login(HostAddress.ApiUri.Host, username, password)) .FinallyInUI((success, ex, res) => { if (!success) @@ -320,7 +326,7 @@ public void ContinueLogin(LoginResult loginResult, string code) public GitHubUser GetCurrentUser() { - var keychainConnection = keychain.Connections.FirstOrDefault(x => x.Host == OriginalUrl); + var keychainConnection = keychain.Connections.FirstOrDefault(x => x.Host == (UriString)HostAddress.ApiUri.Host); if (keychainConnection == null) throw new KeychainEmptyException(); @@ -365,7 +371,10 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha throw new ApiClientException("No Credentials found"); } - var octorunTask = new OctorunTask(taskManager.Token, environment, "validate -h " + OriginalUrl.Host, keychainAdapter.Credential.Token) + logger.Trace("GetValidatedGitHubUser with GitHub Token: {0} {1}", keychainAdapter.Credential.Host, keychainAdapter.Credential.Token); + + var command = HostAddress.IsGitHubDotCom() ? "validate" : "validate -h " + HostAddress.ApiUri.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, command, keychainAdapter.Credential.Token) .Configure(processManager); var ret = octorunTask.RunSynchronously(); diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 80f9fe666..15178a52f 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -210,6 +210,8 @@ public void SetupGit(GitInstaller.GitInstallationState state) return true; }).RunSynchronously(); + Logger.Trace("Credential Helper: {0}", credentialHelper); + if (string.IsNullOrEmpty(credentialHelper)) { Logger.Warning("No Windows CredentialHelper found: Setting to wincred"); diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index a5bf88323..c8baeab0a 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -5,7 +5,6 @@ namespace GitHub.Unity interface IApiClient { HostAddress HostAddress { get; } - UriString OriginalUrl { get; } void CreateRepository(string name, string description, bool isPrivate, Action callback, string organization = null); void GetOrganizations(Action onSuccess, Action onError = null); @@ -14,6 +13,6 @@ void CreateRepository(string name, string description, bool isPrivate, void LoginWithToken(string token, Action result); ITask Logout(UriString host); void GetCurrentUser(Action onSuccess, Action onError = null); - void GetServerMeta(Action onSuccess, Action onError = null); + void GetEnterpriseServerMeta(Action onSuccess, Action onError = null); } } diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index d4edce511..8e02b698c 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -1,4 +1,5 @@ using System; +using System.Text; using GitHub.Logging; namespace GitHub.Unity @@ -174,9 +175,20 @@ private LoginResultData TryLogin( { var hasTwoFactorCode = code != null; - var arguments = (hasTwoFactorCode ? "login --twoFactor -h " : "login -h ") + host.Host; - - var loginTask = new OctorunTask(taskManager.Token, environment, arguments); + var command = new StringBuilder("login"); + + if (hasTwoFactorCode) + { + command.Append(" --twoFactor"); + } + + if (!HostAddress.IsGitHubDotCom(host)) + { + command.Append(" -h "); + command.Append(host.Host); + } + + var loginTask = new OctorunTask(taskManager.Token, environment, command.ToString()); loginTask.Configure(processManager, withInput: true); loginTask.OnStartProcess += proc => { @@ -214,7 +226,8 @@ private string RetrieveUsername(string token, UriString host, string username = return username; } - var octorunTask = new OctorunTask(taskManager.Token, environment, "validate -h " + host.Host, token) + var command = HostAddress.IsGitHubDotCom(host) ? "validate" : "validate -h " + host.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, command, token) .Configure(processManager); var validateResult = octorunTask.RunSynchronously(); diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index fce67ac2d..78d841e55 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -86,7 +86,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "902910f45"; + public const string PackageVersion = "902910f47"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Metrics/UsageTracker.cs b/src/GitHub.Api/Metrics/UsageTracker.cs index ca68471cc..0e8bce059 100644 --- a/src/GitHub.Api/Metrics/UsageTracker.cs +++ b/src/GitHub.Api/Metrics/UsageTracker.cs @@ -401,7 +401,7 @@ protected override string GetUsername() { string username = ""; try { - var apiClient = new ApiClient("", Keychain, ProcessManager, TaskManager, Environment); + var apiClient = new ApiClient(Keychain, ProcessManager, TaskManager, Environment); var user = apiClient.GetCurrentUser(); username = user.Login; } catch { diff --git a/src/GitHub.Api/Primitives/HostAddress.cs b/src/GitHub.Api/Primitives/HostAddress.cs index fd738b649..a53bd8772 100644 --- a/src/GitHub.Api/Primitives/HostAddress.cs +++ b/src/GitHub.Api/Primitives/HostAddress.cs @@ -75,14 +75,21 @@ public static bool IsGitHubDotCom(Uri hostUri) || hostUri.IsSameHost(gistUri); } - public static bool IsGitHubDotCom(string url) + public static bool IsGitHubDotCom(UriString hostUri) { - if (String.IsNullOrEmpty(url)) - return false; - Uri uri = null; - if (!Uri.TryCreate(url, UriKind.Absolute, out uri)) + return hostUri.Host == GitHubDotComHostAddress.WebUri.Host + || hostUri.Host == GitHubDotComHostAddress.ApiUri.Host + || hostUri.Host == gistUri.Host; + } + + public static bool IsGitHubDotCom(Connection connection) + { + if (connection == null || String.IsNullOrEmpty(connection.Host)) return false; - return IsGitHubDotCom(uri); + + return connection.Host == GitHubDotComHostAddress.WebUri.Host + || connection.Host == GitHubDotComHostAddress.ApiUri.Host + || connection.Host == gistUri.Host; } public bool IsGitHubDotCom() diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 0511ffd5f..d4739a56c 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffc11593937b7a03f3ef5450ec07699a0ae2f23d0785eb11fb827c1eccc82b20 -size 220342 +oid sha256:38edc11e8332150a2f642a7058980735477e8afd93e3b37faceee9f1fd00cf12 +size 220347 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 849770bbb..1032f8b46 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -0e2d411bfe82bc5b579703f5604ceed0 +977df0fef82ae85e55419b042b4078d7 \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index 3501662f8..b41c6a430 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -13,7 +13,9 @@ public AuthenticationService(UriString host, IKeychain keychain, IEnvironment environment ) { - client = new ApiClient(host, keychain, processManager, taskManager, environment); + client = host == null + ? new ApiClient(keychain, processManager, taskManager, environment) + : new ApiClient(host, keychain, processManager, taskManager, environment); } public HostAddress HostAddress { get { return client.HostAddress; } } @@ -43,7 +45,7 @@ public void LoginWith2fa(string code) public void GetServerMeta(Action serverMeta, Action error) { loginResultData = null; - client.GetServerMeta(data => + client.GetEnterpriseServerMeta(data => { serverMeta(data); }, exception => { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index 07d24dc8a..f2caf459d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using UnityEngine; using UnityEditor; @@ -12,13 +13,13 @@ class AuthenticationView : Subview private const string WindowTitle = "Authenticate"; - [NonSerialized] private bool isBusy; - [SerializeField] private SubTab changeTab = SubTab.GitHub; [SerializeField] private SubTab activeTab = SubTab.GitHub; [SerializeField] private GitHubAuthenticationView gitHubAuthenticationView; [SerializeField] private GitHubEnterpriseAuthenticationView gitHubEnterpriseAuthenticationView; + [SerializeField] private bool hasGitHubDotComConnection; + [SerializeField] private bool hasGitHubEnterpriseConnection; public override void InitializeView(IView parent) { @@ -31,6 +32,15 @@ public override void InitializeView(IView parent) gitHubAuthenticationView.InitializeView(parent); gitHubEnterpriseAuthenticationView.InitializeView(parent); + + hasGitHubDotComConnection = Platform.Keychain.Connections.Any(HostAddress.IsGitHubDotCom); + hasGitHubEnterpriseConnection = Platform.Keychain.Connections.Any(connection => !HostAddress.IsGitHubDotCom(connection)); + + if (hasGitHubDotComConnection) + { + changeTab = SubTab.GitHubEnterprise; + UpdateActiveTab(); + } } public void Initialize(Exception exception) @@ -46,7 +56,17 @@ public override void OnGUI() public override bool IsBusy { - get { return isBusy; } + get { return (gitHubAuthenticationView != null && gitHubAuthenticationView.IsBusy) || (gitHubEnterpriseAuthenticationView != null && gitHubEnterpriseAuthenticationView.IsBusy); } + } + + public override void OnDataUpdate() + { + base.OnDataUpdate(); + MaybeUpdateData(); + } + + private void MaybeUpdateData() + { } private static SubTab TabButton(SubTab tab, string title, SubTab currentTab) @@ -67,8 +87,17 @@ private void DoToolbarGUI() { EditorGUI.BeginChangeCheck(); { - changeTab = TabButton(SubTab.GitHub, "GitHub", changeTab); - changeTab = TabButton(SubTab.GitHubEnterprise, "GitHub Enterprise", changeTab); + EditorGUI.BeginDisabledGroup(hasGitHubDotComConnection || IsBusy); + { + changeTab = TabButton(SubTab.GitHub, "GitHub", changeTab); + } + EditorGUI.EndDisabledGroup(); + + EditorGUI.BeginDisabledGroup(hasGitHubEnterpriseConnection || IsBusy); + { + changeTab = TabButton(SubTab.GitHubEnterprise, "GitHub Enterprise", changeTab); + } + EditorGUI.EndDisabledGroup(); } if (EditorGUI.EndChangeCheck()) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index fa21bebf3..10d72469d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -181,4 +181,4 @@ protected ILogging Logger } } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs index d7da7cfbe..f18452cea 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs @@ -14,7 +14,7 @@ public enum PopupViewType AuthenticationView, } - [NonSerialized] private IApiClient client; +// [NonSerialized] private IApiClient client; [SerializeField] private PopupViewType activeViewType; [SerializeField] private AuthenticationView authenticationView; @@ -114,17 +114,35 @@ private void Open(PopupViewType popupViewType, Action onClose) OnClose = null; var viewNeedsAuthentication = popupViewType == PopupViewType.PublishView; + if (viewNeedsAuthentication) { - Client.GetCurrentUser(user => + var userHasAuthentication = false; + foreach (var keychainConnection in Platform.Keychain.Connections) + { + var apiClient = new ApiClient(keychainConnection.Host, Platform.Keychain, Platform.ProcessManager, TaskManager, + Environment); + + try + { + apiClient.GetCurrentUser(); + userHasAuthentication = true; + break; + } + catch (Exception ex) + { + Logger.Trace(ex, "Exception validating host {0}", keychainConnection.Host); + } + } + + if (userHasAuthentication) { OpenInternal(popupViewType, onClose); shouldCloseOnFinish = true; - - }, - exception => + } + else { - authenticationView.Initialize(exception); + authenticationView.Initialize(null); OpenInternal(PopupViewType.AuthenticationView, completedAuthentication => { if (completedAuthentication) @@ -132,8 +150,9 @@ private void Open(PopupViewType popupViewType, Action onClose) Open(popupViewType, onClose); } }); + shouldCloseOnFinish = false; - }); + } } else { @@ -168,20 +187,19 @@ private void SwitchView(Subview fromView, Subview toView) Repaint(); } - public IApiClient Client - { - get - { - if (client == null) - { - var repository = Environment.Repository; - UriString host = repository != null ? repository.CloneUrl : null; - client = new ApiClient(host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment); - } - - return client; - } - } +// public IApiClient Client +// { +// get +// { +// if (client == null) +// { +// var repository = Environment.Repository; +// UriString host = repository != null ? repository.CloneUrl : null; +// } +// +// return client; +// } +// } private Subview ActiveView { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index 0671bcf06..d9858902d 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -22,6 +22,10 @@ class PublishView : Subview private const string PublishLimitPrivateRepositoriesError = "You are currently at your limit of private repositories"; private const string PublishToGithubLabel = "Publish to GitHub"; + [SerializeField] private Connection[] connections; + [SerializeField] private string[] connectionLabels; + [SerializeField] private int selectedConnection; + [SerializeField] private string username; [SerializeField] private string[] owners = { OwnersDefaultText }; [SerializeField] private string[] publishOwners; @@ -33,19 +37,15 @@ class PublishView : Subview [NonSerialized] private IApiClient client; [NonSerialized] private bool isBusy; [NonSerialized] private string error; + [NonSerialized] private bool connectionsNeedLoading; [NonSerialized] private bool ownersNeedLoading; - private Connection Connection { get { return Platform.Keychain.Connections.First(); } } +// private Connection Connection { get { return Platform.Keychain.Connections.First(); } } public IApiClient Client { get { - if (client == null) - { - client = new ApiClient(Connection.Host, Platform.Keychain, Manager.ProcessManager, TaskManager, Environment); - } - return client; } } @@ -54,6 +54,7 @@ public override void OnEnable() { base.OnEnable(); ownersNeedLoading = publishOwners == null && !isBusy; + connectionsNeedLoading = connections == null && !isBusy; } public override void OnDataUpdate() @@ -64,10 +65,18 @@ public override void OnDataUpdate() private void MaybeUpdateData() { + if (connectionsNeedLoading) + { + connectionsNeedLoading = false; + connections = Platform.Keychain.Connections; + connectionLabels = connections.Select(connection => + HostAddress.IsGitHubDotCom(connection) ? "GitHub" : connection.Host).ToArray(); + } + if (ownersNeedLoading) { - ownersNeedLoading = false; - LoadOwners(); +// ownersNeedLoading = false; +// LoadOwners(); } } @@ -83,7 +92,7 @@ private void LoadOwners() isBusy = true; //TODO: ONE_USER_LOGIN This assumes only ever one user can login - username = Connection.Username; +// username = Connection.Username; Client.GetOrganizations(orgs => { @@ -123,6 +132,11 @@ public override void OnGUI() EditorGUI.BeginDisabledGroup(isBusy); { + if (connections.Length > 1) + { + selectedConnection = EditorGUILayout.Popup("Connections:", selectedConnection, connectionLabels); + } + selectedOwner = EditorGUILayout.Popup(SelectedOwnerLabel, selectedOwner, owners); repoName = EditorGUILayout.TextField(RepositoryNameLabel, repoName); repoDescription = EditorGUILayout.TextField(DescriptionLabel, repoDescription); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 026bdd822..d39983cb0 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -2,6 +2,7 @@ using System.Linq; using UnityEditor; using UnityEngine; +using Object = System.Object; namespace GitHub.Unity { @@ -55,7 +56,7 @@ class Window : BaseWindow [SerializeField] private string repositoryProgressMessage; [SerializeField] private float appManagerProgressValue; [SerializeField] private string appManagerProgressMessage; - [SerializeField] private Connection connection; + [SerializeField] private Connection[] connections; [MenuItem(Menu_Window_GitHub)] public static void Window_GitHub() @@ -218,9 +219,13 @@ private void MaybeUpdateData() { var firstConnection = Platform.Keychain.Connections.FirstOrDefault(); if (firstConnection != null) + { host = firstConnection.Host; + } else + { host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); + } } else { @@ -228,8 +233,8 @@ private void MaybeUpdateData() .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); } - connection = Platform.Keychain.Connections.FirstOrDefault(x => x.Host.ToUriString() == host); - + connections = Platform.Keychain.Connections.OrderBy(x => x.Host.ToUriString() == host).ToArray(); + if (repositoryProgressHasUpdate) { if (repositoryProgress != null) @@ -588,13 +593,14 @@ private void DoToolbarGUI() { GUILayout.FlexibleSpace(); - if (connection == null) + if (!connections.Any()) { if (GUILayout.Button("Sign in", EditorStyles.toolbarButton)) SignIn(null); } else { + var connection = connections.First(); if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) { DoAccountDropdown(); @@ -670,13 +676,14 @@ private void DoActionbarGUI() GUILayout.FlexibleSpace(); - if (connection == null) + if (!connections.Any()) { if (GUILayout.Button("Sign in", EditorStyles.toolbarButton)) SignIn(null); } else { + var connection = connections.First(); if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) { DoAccountDropdown(); @@ -848,9 +855,38 @@ private void SwitchView(Subview fromView, Subview toView) private void DoAccountDropdown() { GenericMenu accountMenu = new GenericMenu(); - accountMenu.AddItem(new GUIContent("Go to Profile"), false, GoToProfile, "profile"); - accountMenu.AddSeparator(""); - accountMenu.AddItem(new GUIContent("Sign out"), false, SignOut, "sign out"); + + if (connections.Length == 1) + { + var connection = connections.First(); + accountMenu.AddItem(new GUIContent("Go to Profile"), false, GoToProfile, connection); + accountMenu.AddItem(new GUIContent("Sign out"), false, SignOut, connection); + accountMenu.AddSeparator(""); + accountMenu.AddItem(new GUIContent("Sign In"), false, SignIn, "sign in"); + } + else + { + for (var index = 0; index < connections.Length; index++) + { + var connection = connections[index]; + var isGitHubDotCom = HostAddress.IsGitHubDotCom(connection); + + string rootPath; + if (isGitHubDotCom) + { + rootPath = "GitHub/"; + } + else + { + var uriString = connection.Host.ToUriString(); + rootPath = uriString.Host + "/"; + } + + accountMenu.AddItem(new GUIContent(rootPath + "Go to Profile"), false, GoToProfile, connection); + accountMenu.AddItem(new GUIContent(rootPath + "Sign out"), false, SignOut, connection); + } + } + accountMenu.ShowAsContext(); } @@ -861,12 +897,14 @@ private void SignIn(object obj) private void GoToProfile(object obj) { + var connection = (Connection) obj; var uriString = new UriString(connection.Host).Combine(connection.Username); Application.OpenURL(uriString); } private void SignOut(object obj) { + var connection = (Connection)obj; var loginManager = new LoginManager(Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); loginManager.Logout(connection.Host).FinallyInUI((s, e) => Redraw()); } From 70b0c41bce679dead56acd56a429fba890377e44 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 17 Oct 2018 12:30:22 -0400 Subject: [PATCH 454/567] Seems like I have GitHub Enterprise support --- src/GitHub.Api/Application/ApiClient.cs | 111 +++++++++++------- .../Authentication/ICredentialManager.cs | 1 - src/GitHub.Api/Authentication/Keychain.cs | 2 + src/GitHub.Api/Git/GitCredentialManager.cs | 24 ++-- .../Editor/GitHub.Unity/UI/PopupWindow.cs | 22 +--- .../Editor/GitHub.Unity/UI/PublishView.cs | 65 ++++++---- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 40 +++++-- 7 files changed, 153 insertions(+), 112 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 7b77432b5..aef1685d4 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -22,6 +22,8 @@ class ApiClient : IApiClient private readonly ITaskManager taskManager; private readonly ILoginManager loginManager; private readonly IEnvironment environment; + private IKeychainAdapter keychainAdapter; + private Connection connection; public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment): this(UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri), keychain, processManager, taskManager, environment) @@ -36,8 +38,6 @@ public ApiClient(UriString host, IKeychain keychain, IProcessManager processMana host = new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); HostAddress = HostAddress.Create(host); - logger.Trace("OriginalUrl: {0}", HostAddress.ApiUri.Host); - this.keychain = keychain; this.processManager = processManager; this.taskManager = taskManager; @@ -57,8 +57,7 @@ public void CreateRepository(string name, string description, bool isPrivate, new FuncTask(taskManager.Token, () => { - // this validates the user, again - GetCurrentUser(); + EnsureValidCredentials(); var command = new StringBuilder("publish"); @@ -91,11 +90,7 @@ public void CreateRepository(string name, string description, bool isPrivate, command.Append(" -p"); } - var adapter = keychain.Connect(HostAddress.ApiUri.Host); - if (adapter.Credential == null) - { - throw new ApiClientException("No Credentials found"); - } + var adapter = EnsureKeychainAdapter(); var octorunTask = new OctorunTask(taskManager.Token, environment, command.ToString(), adapter.Credential.Token) .Configure(processManager); @@ -199,11 +194,7 @@ public void GetOrganizations(Action onSuccess, Action Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); new FuncTask(taskManager.Token, () => { - var adapter = keychain.Connect(HostAddress.ApiUri.Host); - if (adapter.Credential == null) - { - throw new ApiClientException("No Credentials found"); - } + var adapter = EnsureKeychainAdapter(); var command = HostAddress.IsGitHubDotCom() ? "organizations" : "organizations -h " + HostAddress.ApiUri.Host; var octorunTask = new OctorunTask(taskManager.Token, environment, @@ -240,6 +231,17 @@ public void GetOrganizations(Action onSuccess, Action .Start(); } + private IKeychainAdapter EnsureKeychainAdapter() + { + var adapter = KeychainAdapter; + if (adapter.Credential == null) + { + throw new ApiClientException("No Credentials found"); + } + + return adapter; + } + public void GetCurrentUser(Action onSuccess, Action onError = null) { Guard.ArgumentNotNull(onSuccess, nameof(onSuccess)); @@ -324,57 +326,76 @@ public void ContinueLogin(LoginResult loginResult, string code) .Start(); } - public GitHubUser GetCurrentUser() + public void EnsureValidCredentials() { - var keychainConnection = keychain.Connections.FirstOrDefault(x => x.Host == (UriString)HostAddress.ApiUri.Host); - if (keychainConnection == null) - throw new KeychainEmptyException(); - - var keychainAdapter = GetValidatedKeychainAdapter(keychainConnection); + GetCurrentUser(); + } + public GitHubUser GetCurrentUser() + { // we can't trust that the system keychain has the username filled out correctly. // if it doesn't, we need to grab the username from the server and check it // unfortunately this means that things will be slower when the keychain doesn't have all the info - if (keychainConnection.User == null || keychainAdapter.Credential.Username != keychainConnection.Username) + if (Connection.User == null || KeychainAdapter.Credential.Username != Connection.Username) { - keychainConnection.User = GetValidatedGitHubUser(keychainConnection, keychainAdapter); + Connection.User = GetValidatedGitHubUser(); } - return keychainConnection.User; + + return Connection.User; } - private IKeychainAdapter GetValidatedKeychainAdapter(Connection keychainConnection) + private Connection Connection { - var keychainAdapter = keychain.LoadFromSystem(keychainConnection.Host); - if (keychainAdapter == null) - throw new KeychainEmptyException(); - - if (string.IsNullOrEmpty(keychainAdapter.Credential?.Username)) + get { - logger.Warning("LoadKeychainInternal: Username is empty"); - throw new TokenUsernameMismatchException(keychainConnection.Username); - } + if (connection == null) + { + connection = keychain.Connections.FirstOrDefault(x => x.Host == (UriString)HostAddress.ApiUri.Host); + } - if (keychainAdapter.Credential.Username != keychainConnection.Username) - { - logger.Warning("LoadKeychainInternal: Token username does not match"); + return connection; } - - return keychainAdapter; } - private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeychainAdapter keychainAdapter) + private IKeychainAdapter KeychainAdapter { - try + get { - if (keychainAdapter.Credential == null) + if (keychainAdapter == null) { - throw new ApiClientException("No Credentials found"); + if (Connection == null) + throw new KeychainEmptyException(); + + var loadedKeychainAdapter = keychain.LoadFromSystem(Connection.Host); + if (loadedKeychainAdapter == null) + throw new KeychainEmptyException(); + + if (string.IsNullOrEmpty(loadedKeychainAdapter.Credential?.Username)) + { + logger.Warning("LoadKeychainInternal: Username is empty"); + throw new TokenUsernameMismatchException(connection.Username); + } + + if (loadedKeychainAdapter.Credential.Username != connection.Username) + { + logger.Warning("LoadKeychainInternal: Token username does not match"); + } + + keychainAdapter = loadedKeychainAdapter; } - logger.Trace("GetValidatedGitHubUser with GitHub Token: {0} {1}", keychainAdapter.Credential.Host, keychainAdapter.Credential.Token); + return keychainAdapter; + } + } + + private GitHubUser GetValidatedGitHubUser() + { + try + { + var adapter = EnsureKeychainAdapter(); var command = HostAddress.IsGitHubDotCom() ? "validate" : "validate -h " + HostAddress.ApiUri.Host; - var octorunTask = new OctorunTask(taskManager.Token, environment, command, keychainAdapter.Credential.Token) + var octorunTask = new OctorunTask(taskManager.Token, environment, command, adapter.Credential.Token) .Configure(processManager); var ret = octorunTask.RunSynchronously(); @@ -382,10 +403,10 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha { var login = ret.Output[1]; - if (login != keychainConnection.Username) + if (login != Connection.Username) { logger.Trace("LoadKeychainInternal: Api username does not match"); - throw new TokenUsernameMismatchException(keychainConnection.Username, login); + throw new TokenUsernameMismatchException(Connection.Username, login); } return new GitHubUser diff --git a/src/GitHub.Api/Authentication/ICredentialManager.cs b/src/GitHub.Api/Authentication/ICredentialManager.cs index dde06bcc7..94aef5a97 100644 --- a/src/GitHub.Api/Authentication/ICredentialManager.cs +++ b/src/GitHub.Api/Authentication/ICredentialManager.cs @@ -17,6 +17,5 @@ public interface ICredentialManager void Save(ICredential cred); void Delete(UriString host); bool HasCredentials(); - ICredential CachedCredentials { get; } } } diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index a32cc3c42..b1f038ce3 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -133,6 +133,8 @@ private KeychainAdapter FindOrCreateAdapter(UriString host) KeychainAdapter value; if (!keychainAdapters.TryGetValue(host, out value)) { + logger.Trace("Creating Adapter {0}", host); + value = new KeychainAdapter(); keychainAdapters.Add(host, value); } diff --git a/src/GitHub.Api/Git/GitCredentialManager.cs b/src/GitHub.Api/Git/GitCredentialManager.cs index 2dcb61eb7..289e5811d 100644 --- a/src/GitHub.Api/Git/GitCredentialManager.cs +++ b/src/GitHub.Api/Git/GitCredentialManager.cs @@ -1,6 +1,7 @@ using GitHub.Logging; using System; using System.Collections.Generic; +using System.Linq; namespace GitHub.Unity { @@ -8,11 +9,11 @@ class GitCredentialManager : ICredentialManager { private static ILogging Logger { get; } = LogHelper.GetLogger(); - private ICredential credential; private string credHelper = null; private readonly IProcessManager processManager; private readonly ITaskManager taskManager; + private readonly Dictionary credentials = new Dictionary(); public GitCredentialManager(IProcessManager processManager, ITaskManager taskManager) @@ -23,11 +24,9 @@ public GitCredentialManager(IProcessManager processManager, public bool HasCredentials() { - return credential != null; + return credentials != null && credentials.Any(); } - public ICredential CachedCredentials { get { return credential; } } - public void Delete(UriString host) { if (!LoadCredentialHelper()) @@ -39,12 +38,13 @@ public void Delete(UriString host) String.Format("protocol={0}", host.Protocol), String.Format("host={0}", host.Host) }).RunSynchronously(); - credential = null; + credentials.Remove(host); } public ICredential Load(UriString host) { - if (credential == null) + ICredential credential; + if (!credentials.TryGetValue(host, out credential)) { if (!LoadCredentialHelper()) return null; @@ -87,23 +87,25 @@ public ICredential Load(UriString host) } credential = new Credential(host, user, password); + credentials.Add(host, credential); } + return credential; } public void Save(ICredential cred) { - this.credential = cred; + this.credentials.Add(cred.Host, cred); if (!LoadCredentialHelper()) return; var data = new List { - String.Format("protocol={0}", credential.Host.Protocol), - String.Format("host={0}", credential.Host.Host), - String.Format("username={0}", credential.Username), - String.Format("password={0}", credential.Token) + String.Format("protocol={0}", cred.Host.Protocol), + String.Format("host={0}", cred.Host.Host), + String.Format("username={0}", cred.Username), + String.Format("password={0}", cred.Token) }; var task = RunCredentialHelper("store", data.ToArray()); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs index f18452cea..cfb1f67f8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEngine; @@ -14,8 +16,6 @@ public enum PopupViewType AuthenticationView, } -// [NonSerialized] private IApiClient client; - [SerializeField] private PopupViewType activeViewType; [SerializeField] private AuthenticationView authenticationView; [SerializeField] private LoadingView loadingView; @@ -118,14 +118,14 @@ private void Open(PopupViewType popupViewType, Action onClose) if (viewNeedsAuthentication) { var userHasAuthentication = false; - foreach (var keychainConnection in Platform.Keychain.Connections) + foreach (var keychainConnection in Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom)) { var apiClient = new ApiClient(keychainConnection.Host, Platform.Keychain, Platform.ProcessManager, TaskManager, Environment); try { - apiClient.GetCurrentUser(); + apiClient.EnsureValidCredentials(); userHasAuthentication = true; break; } @@ -187,20 +187,6 @@ private void SwitchView(Subview fromView, Subview toView) Repaint(); } -// public IApiClient Client -// { -// get -// { -// if (client == null) -// { -// var repository = Environment.Repository; -// UriString host = repository != null ? repository.CloneUrl : null; -// } -// -// return client; -// } -// } - private Subview ActiveView { get diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index d9858902d..c1c9fab51 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -26,7 +26,6 @@ class PublishView : Subview [SerializeField] private string[] connectionLabels; [SerializeField] private int selectedConnection; - [SerializeField] private string username; [SerializeField] private string[] owners = { OwnersDefaultText }; [SerializeField] private string[] publishOwners; [SerializeField] private int selectedOwner; @@ -34,22 +33,13 @@ class PublishView : Subview [SerializeField] private string repoDescription = ""; [SerializeField] private bool togglePrivate; - [NonSerialized] private IApiClient client; + [NonSerialized] private Dictionary clients = new Dictionary(); + [NonSerialized] private IApiClient selectedClient; [NonSerialized] private bool isBusy; [NonSerialized] private string error; [NonSerialized] private bool connectionsNeedLoading; [NonSerialized] private bool ownersNeedLoading; -// private Connection Connection { get { return Platform.Keychain.Connections.First(); } } - - public IApiClient Client - { - get - { - return client; - } - } - public override void OnEnable() { base.OnEnable(); @@ -68,16 +58,35 @@ private void MaybeUpdateData() if (connectionsNeedLoading) { connectionsNeedLoading = false; - connections = Platform.Keychain.Connections; - connectionLabels = connections.Select(connection => - HostAddress.IsGitHubDotCom(connection) ? "GitHub" : connection.Host).ToArray(); + connections = Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom).ToArray(); + connectionLabels = connections.Select(c => HostAddress.IsGitHubDotCom(c) ? "GitHub" : c.Host).ToArray(); + + var connection = connections.First(); + selectedConnection = 0; + selectedClient = GetApiClient(connection); } if (ownersNeedLoading) { -// ownersNeedLoading = false; -// LoadOwners(); + ownersNeedLoading = false; + LoadOwners(); + } + } + + private IApiClient GetApiClient(Connection connection) + { + IApiClient client; + + if (!clients.TryGetValue(connection.Host, out client)) + { + client = HostAddress.IsGitHubDotCom(connection) + ? new ApiClient(Platform.Keychain, Platform.ProcessManager, TaskManager, Environment) + : new ApiClient(connection.Host, Platform.Keychain, Platform.ProcessManager, TaskManager, Environment); + + clients.Add(connection.Host, client); } + + return client; } public override void InitializeView(IView parent) @@ -91,17 +100,14 @@ private void LoadOwners() { isBusy = true; - //TODO: ONE_USER_LOGIN This assumes only ever one user can login -// username = Connection.Username; - - Client.GetOrganizations(orgs => + selectedClient.GetOrganizations(orgs => { publishOwners = orgs .OrderBy(organization => organization.Login) .Select(organization => organization.Login) .ToArray(); - owners = new[] { OwnersDefaultText, username }.Union(publishOwners).ToArray(); + owners = new[] { OwnersDefaultText, connections[selectedConnection].Username }.Union(publishOwners).ToArray(); isBusy = false; @@ -134,7 +140,16 @@ public override void OnGUI() { if (connections.Length > 1) { - selectedConnection = EditorGUILayout.Popup("Connections:", selectedConnection, connectionLabels); + EditorGUI.BeginChangeCheck(); + { + selectedConnection = EditorGUILayout.Popup("Connections:", selectedConnection, connectionLabels); + } + if (EditorGUI.EndChangeCheck()) + { + selectedClient = GetApiClient(connections[selectedConnection]); + ownersNeedLoading = true; + Redraw(); + } } selectedOwner = EditorGUILayout.Popup(SelectedOwnerLabel, selectedOwner, owners); @@ -155,12 +170,12 @@ public override void OnGUI() GUI.FocusControl(null); isBusy = true; - var organization = owners[selectedOwner] == username ? null : owners[selectedOwner]; + var organization = owners[selectedOwner] == connections[selectedConnection].Username ? null : owners[selectedOwner]; var cleanRepoDescription = repoDescription.Trim(); cleanRepoDescription = string.IsNullOrEmpty(cleanRepoDescription) ? null : cleanRepoDescription; - Client.CreateRepository(repoName, cleanRepoDescription, togglePrivate, (repository, ex) => + selectedClient.CreateRepository(repoName, cleanRepoDescription, togglePrivate, (repository, ex) => { if (ex != null) { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index d39983cb0..1b772c5e2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -57,6 +57,7 @@ class Window : BaseWindow [SerializeField] private float appManagerProgressValue; [SerializeField] private string appManagerProgressMessage; [SerializeField] private Connection[] connections; + [SerializeField] private string primaryConnectionUsername; [MenuItem(Menu_Window_GitHub)] public static void Window_GitHub() @@ -214,27 +215,43 @@ private void ValidateCachedData(IRepository repository) private void MaybeUpdateData() { - UriString host = null; - if (!HasRepository || String.IsNullOrEmpty(Repository.CloneUrl)) + if (HasRepository && !string.IsNullOrEmpty(Repository.CloneUrl)) { - var firstConnection = Platform.Keychain.Connections.FirstOrDefault(); - if (firstConnection != null) + UriString host = new UriString(Repository.CloneUrl.ToRepositoryUri() + .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + + connections = Platform.Keychain.Connections.OrderByDescending(x => x.Host.ToUriString() == host).ToArray(); + } + else + { + connections = Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom).ToArray(); + } + + var connectionCount = connections.Length; + if (connectionCount > 1) + { + var connection = connections.First(); + var isGitHubDotCom = HostAddress.IsGitHubDotCom(connection); + + if (isGitHubDotCom) { - host = firstConnection.Host; + primaryConnectionUsername = "GitHub: " + connection.Username; } else { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); + primaryConnectionUsername = connection.Host + ": " + connection.Username; } } + else if(connectionCount == 1) + { + primaryConnectionUsername = connections.First().Username; + } else { - host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + primaryConnectionUsername = null; } - connections = Platform.Keychain.Connections.OrderBy(x => x.Host.ToUriString() == host).ToArray(); - + if (repositoryProgressHasUpdate) { if (repositoryProgress != null) @@ -600,8 +617,7 @@ private void DoToolbarGUI() } else { - var connection = connections.First(); - if (GUILayout.Button(connection.Username, EditorStyles.toolbarDropDown)) + if (GUILayout.Button(primaryConnectionUsername, EditorStyles.toolbarDropDown)) { DoAccountDropdown(); } From a90431f60fe799492bc852c4fb4ec468ea4ec600 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 18 Oct 2018 08:25:39 -0400 Subject: [PATCH 455/567] Progress --- .../Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs | 3 +-- .../GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs | 8 ++++++++ .../Assets/Editor/GitHub.Unity/UI/Window.cs | 7 ++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs index 84264f67a..46526cf72 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -243,8 +243,7 @@ private AuthenticationService AuthenticationService { if (authenticationService == null) { - UriString host = Repository != null ? Repository.CloneUrl : null; - AuthenticationService = new AuthenticationService(host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + AuthenticationService = new AuthenticationService(HostAddress.GitHubDotComHostAddress.WebUri.Host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); } return authenticationService; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs index 3d5d3ff93..15353537b 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -203,6 +203,14 @@ private void OnGUIUserPasswordLogin() GUILayout.BeginHorizontal(); { GUILayout.FlexibleSpace(); + if (GUILayout.Button("Back")) + { + GUI.FocusControl(null); + + hasServerMeta = false; + Redraw(); + } + if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) { GUI.FocusControl(null); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 1b772c5e2..274f873d2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -217,10 +217,11 @@ private void MaybeUpdateData() { if (HasRepository && !string.IsNullOrEmpty(Repository.CloneUrl)) { - UriString host = new UriString(Repository.CloneUrl.ToRepositoryUri() - .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + var host = Repository.CloneUrl + .ToRepositoryUri() + .GetComponents(UriComponents.Host, UriFormat.SafeUnescaped); - connections = Platform.Keychain.Connections.OrderByDescending(x => x.Host.ToUriString() == host).ToArray(); + connections = Platform.Keychain.Connections.OrderByDescending(x => x.Host == host).ToArray(); } else { From a609e7a35b3f4631502b1de6921c38fa9b9aed57 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 18 Oct 2018 17:21:35 -0400 Subject: [PATCH 456/567] Functionality to login via link --- octorun/src/bin/app.js | 1 + octorun/version | 2 +- src/GitHub.Api/Application/ApiClient.cs | 42 +++++++++- src/GitHub.Api/Application/IApiClient.cs | 1 + src/GitHub.Api/GitHub.Api.45.csproj | 2 + src/GitHub.Api/GitHub.Api.csproj | 2 + src/GitHub.Api/Installer/OctorunInstaller.cs | 2 +- src/GitHub.Api/Resources/octorun.zip | 4 +- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- .../Services/AuthenticationService.cs | 81 ++++++++++++++++++- .../UI/GitHubAuthenticationView.cs | 30 +++++++ 11 files changed, 162 insertions(+), 7 deletions(-) diff --git a/octorun/src/bin/app.js b/octorun/src/bin/app.js index 0eacfdc5e..095292965 100644 --- a/octorun/src/bin/app.js +++ b/octorun/src/bin/app.js @@ -9,5 +9,6 @@ commander .command('organizations', 'Get Organizations') .command('publish', 'Publish') .command('usage', 'Usage') + .command('token', 'Create OAuth Token') .command('meta', 'Get Server Meta Data') .parse(process.argv); \ No newline at end of file diff --git a/octorun/version b/octorun/version index 55d6b723d..998379c47 100644 --- a/octorun/version +++ b/octorun/version @@ -1 +1 @@ -902910f47 \ No newline at end of file +902910f48 \ No newline at end of file diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index aef1685d4..8718d2707 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -6,6 +6,7 @@ using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using GitHub.Unity.Json; namespace GitHub.Unity @@ -14,6 +15,7 @@ class ApiClient : IApiClient { private static readonly ILogging logger = LogHelper.GetLogger(); private static readonly Regex httpStatusErrorRegex = new Regex("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + private static readonly Regex accessTokenRegex = new Regex("access_token=(.*?)&", RegexOptions.Compiled); public HostAddress HostAddress { get; } @@ -25,7 +27,7 @@ class ApiClient : IApiClient private IKeychainAdapter keychainAdapter; private Connection connection; - public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment): + public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment) : this(UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri), keychain, processManager, taskManager, environment) { } @@ -277,6 +279,44 @@ public void LoginWithToken(string token, Action result) .Start(); } + public void CreateOAuthToken(string code, Action result) + { + var command = "token -h " + HostAddress.WebUri.Host; + var octorunTask = new OctorunTask(taskManager.Token, environment, command, code) + .Configure(processManager); + + octorunTask + .Then((b, octorunResult) => + { + if (b && octorunResult.IsSuccess) + { + var first = octorunResult.Output.FirstOrDefault(); + if (first == null) + { + result(false, "Error validating token."); + return; + } + + var match = accessTokenRegex.Match(first); + if (match.Success) + { + var token = match.Groups[1].Value; + LoginWithToken(token, b1 => result(b1, "Error validating token.")); + } + else + { + result(false, octorunResult.Output.FirstOrDefault()); + } + } + else + { + result(false, octorunResult.Output.FirstOrDefault()); + } + }) + .Catch(exception => result(false, exception.ToString())) + .Start(); + } + public void Login(string username, string password, Action need2faCode, Action result) { Guard.ArgumentNotNull(need2faCode, "need2faCode"); diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index c8baeab0a..83bfa17c9 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -14,5 +14,6 @@ void CreateRepository(string name, string description, bool isPrivate, ITask Logout(UriString host); void GetCurrentUser(Action onSuccess, Action onError = null); void GetEnterpriseServerMeta(Action onSuccess, Action onError = null); + void CreateOAuthToken(string code, Action result); } } diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj index 480e35974..32554ceaa 100644 --- a/src/GitHub.Api/GitHub.Api.45.csproj +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -66,12 +66,14 @@ + + diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index bf916ed46..613ba08f9 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -77,12 +77,14 @@ $(SolutionDir).\packages\TaskParallelLibrary.1.0.3333.0\lib\Net35\System.Threading.dll True + + diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 78d841e55..ad26b13ed 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -86,7 +86,7 @@ public class OctorunInstallDetails public const string DefaultZipMd5Url = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip.md5"; public const string DefaultZipUrl = "http://github-vs.s3.amazonaws.com/unity/octorun/octorun.zip"; - public const string PackageVersion = "902910f47"; + public const string PackageVersion = "902910f48"; private const string PackageName = "octorun"; private const string zipFile = "octorun.zip"; diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index d4739a56c..7eaca1f69 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38edc11e8332150a2f642a7058980735477e8afd93e3b37faceee9f1fd00cf12 -size 220347 +oid sha256:2b38e3eb9c0178d67d0b33a8a2bc6118430b2d21b7ea7f8562a9cbf10b2df2f2 +size 221289 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 1032f8b46..22a7a5055 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -977df0fef82ae85e55419b042b4078d7 \ No newline at end of file +7e9bb4522ee6cc4d42ee10bd88f849a8 \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index b41c6a430..567302a43 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -1,18 +1,28 @@ using System; +using System.Text; +using System.Threading; +using GitHub.Logging; namespace GitHub.Unity { - class AuthenticationService + class AuthenticationService: IDisposable { + private readonly ITaskManager taskManager; + private static readonly ILogging logger = LogHelper.GetLogger(); + private readonly IApiClient client; private LoginResult loginResultData; + private IOAuthCallbackListener oauthCallbackListener; + private CancellationTokenSource oauthCallbackCancellationToken; + private string oauthCallbackState; public AuthenticationService(UriString host, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment ) { + this.taskManager = taskManager; client = host == null ? new ApiClient(keychain, processManager, taskManager, environment) : new ApiClient(host, keychain, processManager, taskManager, environment); @@ -52,5 +62,74 @@ public void GetServerMeta(Action serverMeta, Action erro error(exception.Message); }); } + + public Uri StartOAuthListener(Action onSuccess, Action onError) + { + if (oauthCallbackListener == null) + { + logger.Trace("Start OAuthCallbackListener"); + oauthCallbackListener = new OAuthCallbackListener(); + oauthCallbackCancellationToken = new CancellationTokenSource(); + + oauthCallbackState = Guid.NewGuid().ToString(); + oauthCallbackListener.Listen( + oauthCallbackState, + oauthCallbackCancellationToken.Token, + code => { + logger.Trace("OAuthCallbackListener Response: {0}", code); + + client.CreateOAuthToken(code, (b, s) => { + if (b) + { + onSuccess(); + } + else + { + onError(s); + } + }); + }); + } + + return GetLoginUrl(oauthCallbackState); + } + + public void StopOAuthListener() + { + if (oauthCallbackCancellationToken != null) + { + oauthCallbackCancellationToken.Cancel(); + } + + oauthCallbackListener = null; + oauthCallbackCancellationToken = null; + } + + private Uri GetLoginUrl(string state) + { + var query = new StringBuilder(); + + query.Append("client_id="); + query.Append(Uri.EscapeDataString(ApplicationInfo.ClientId)); + query.Append("&redirect_uri="); + query.Append(Uri.EscapeDataString("http://localhost:42424/callback")); + query.Append("&scope="); + query.Append(Uri.EscapeDataString("user,repo")); + query.Append("&state="); + query.Append(Uri.EscapeDataString(state)); + + var uri = new Uri(HostAddress.WebUri, "login/oauth/authorize"); + var uriBuilder = new UriBuilder(uri) + { + Query = query.ToString() + }; + return uriBuilder.Uri; + } + + public void Dispose() + { + if (oauthCallbackCancellationToken != null) + oauthCallbackCancellationToken.Dispose(); + } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs index 46526cf72..19c23c409 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -145,10 +145,40 @@ private void OnGUILogin() } } GUILayout.EndHorizontal(); + + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Signin with your browser", Styles.HyperlinkStyle)) + { + GUI.FocusControl(null); + StartOAuthListener(); + } + } + GUILayout.EndHorizontal(); } EditorGUI.EndDisabledGroup(); } + private void StartOAuthListener() + { + try + { + var uri = AuthenticationService.StartOAuthListener(() => DoResult(true, null), + s => { + errorMessage = s; + TaskManager.RunInUI(Redraw); + }); + Application.OpenURL(uri.ToString()); + } + catch (Exception ex) + { + errorMessage = ex.Message; + Redraw(); + } + } + private void OnGUI2FA() { GUILayout.BeginVertical(); From 43b63f0ac4456d5c26a347ecffafff4072c62aa1 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 19 Oct 2018 07:14:50 -0400 Subject: [PATCH 457/567] Missing changes --- octorun/bin/octorun-token | 3 + octorun/src/bin/app-token.js | 66 +++++++++++++++++ .../Authentication/OAuthCallbackListener.cs | 71 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 octorun/bin/octorun-token create mode 100644 octorun/src/bin/app-token.js create mode 100644 src/GitHub.Api/Authentication/OAuthCallbackListener.cs diff --git a/octorun/bin/octorun-token b/octorun/bin/octorun-token new file mode 100644 index 000000000..3ea7b1500 --- /dev/null +++ b/octorun/bin/octorun-token @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('../src/bin/app-token.js'); diff --git a/octorun/src/bin/app-token.js b/octorun/src/bin/app-token.js new file mode 100644 index 000000000..5811be43e --- /dev/null +++ b/octorun/src/bin/app-token.js @@ -0,0 +1,66 @@ + +var commander = require('commander'); +var package = require('../../package.json'); +var output = require('../output'); +var config = require("../configuration"); +var querystring = require('querystring'); + +commander + .version(package.version) + .option('-h, --host ') + .parse(process.argv); + +var host = commander.host; +var port = 443; +var scheme = 'https'; + +var valid = host && config.clientId && config.clientSecret && config.token; +if (valid) { + var https = require(scheme); + + var postData = querystring.stringify({ + client_id: config.clientId, + client_secret: config.clientSecret, + code: config.token + }); + + var options = { + protocol: scheme + ':', + hostname: host, + port: port, + path: '/login/oauth/access_token', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length + } + }; + + var req = https.request(options, function (res) { + var success = res.statusCode == 200; + + if(!success) { + output.error(res.statusCode); + } else { + res.on('data', function (d) { + output.custom("success", d, true); + }); + + res.on('end', function (d) { + process.exit(); + }); + } + }); + + req.on('error', function (error) { + output.error(error); + }); + + req.write(postData); + + req.end(); +} +else { + commander.help(); + process.exit(-1); +} \ No newline at end of file diff --git a/src/GitHub.Api/Authentication/OAuthCallbackListener.cs b/src/GitHub.Api/Authentication/OAuthCallbackListener.cs new file mode 100644 index 000000000..7bfc0df9f --- /dev/null +++ b/src/GitHub.Api/Authentication/OAuthCallbackListener.cs @@ -0,0 +1,71 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using GitHub.Logging; + +namespace GitHub.Unity +{ + public interface IOAuthCallbackListener + { + void Listen(string state, CancellationToken cancel, Action codeCallback); + } + + public class OAuthCallbackListener : IOAuthCallbackListener + { + private static readonly ILogging logger = LogHelper.GetLogger(); + + const int CallbackPort = 42424; + + static readonly string CallbackUrl = $"http://localhost:{CallbackPort}/"; + private string state; + private CancellationToken cancel; + private Action codeCallback; + private HttpListener httpListener; + + public void Listen(string state, CancellationToken cancel, Action codeCallback) + { + this.state = state; + this.cancel = cancel; + this.codeCallback = codeCallback; + + httpListener = new HttpListener(); + httpListener.Prefixes.Add(CallbackUrl); + httpListener.Start(); + Task.Factory.StartNew(Start, cancel); + } + + private void Start() + { + try + { + using (httpListener) + { + using (cancel.Register(httpListener.Stop)) + { + while (true) + { + var context = httpListener.GetContext(); + var queryParts = HttpUtility.ParseQueryString(context.Request.Url.Query); + + if (queryParts["state"] == state) + { + context.Response.Close(); + codeCallback(queryParts["code"]); + } + } + } + } + } + catch (Exception ex) + { + logger.Warning(ex, "OAuthCallbackListener Error"); + } + finally + { + httpListener = null; + } + } + } +} From 354c1f31af391446409eea953e56f35dbfcb3af2 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 19 Oct 2018 15:27:06 -0400 Subject: [PATCH 458/567] Getting it to work --- .../Application/ApplicationManagerBase.cs | 16 +++ .../Application/IApplicationManager.cs | 3 +- .../Authentication/OAuthCallbackListener.cs | 71 ------------ .../Authentication/OAuthCallbackManager.cs | 106 ++++++++++++++++++ src/GitHub.Api/GitHub.Api.45.csproj | 2 +- src/GitHub.Api/GitHub.Api.csproj | 22 +--- src/GitHub.Api/Platform/IEnvironment.cs | 2 +- .../Services/AuthenticationService.cs | 60 +--------- .../GitHub.Unity/UI/AuthenticationView.cs | 15 +++ .../UI/GitHubAuthenticationView.cs | 73 +++++++----- .../UI/GitHubEnterpriseAuthenticationView.cs | 66 ++++++++++- .../Assets/Editor/GitHub.Unity/UI/Subview.cs | 1 + .../IntegrationTestEnvironment.cs | 1 + 13 files changed, 258 insertions(+), 180 deletions(-) delete mode 100644 src/GitHub.Api/Authentication/OAuthCallbackListener.cs create mode 100644 src/GitHub.Api/Authentication/OAuthCallbackManager.cs diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 15178a52f..755a2f22c 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -320,6 +320,8 @@ protected virtual void InitializeUI() {} protected virtual void InitializationComplete() {} private bool disposed = false; + private IOAuthCallbackManager oAuthCallbackManager; + protected virtual void Dispose(bool disposing) { if (disposing) @@ -358,6 +360,20 @@ public void Dispose() public ISettings SystemSettings { get { return Environment.SystemSettings; } } public ISettings UserSettings { get { return Environment.UserSettings; } } public IUsageTracker UsageTracker { get; protected set; } + + public IOAuthCallbackManager OAuthCallbackManager + { + get + { + if (oAuthCallbackManager == null) + { + oAuthCallbackManager = new OAuthCallbackManager(); + } + + return oAuthCallbackManager; + } + } + public bool IsBusy { get { return isBusy; } } protected TaskScheduler UIScheduler { get; private set; } protected SynchronizationContext SynchronizationContext { get; private set; } diff --git a/src/GitHub.Api/Application/IApplicationManager.cs b/src/GitHub.Api/Application/IApplicationManager.cs index 9b8b5f638..ab82a27d3 100644 --- a/src/GitHub.Api/Application/IApplicationManager.cs +++ b/src/GitHub.Api/Application/IApplicationManager.cs @@ -16,6 +16,7 @@ public interface IApplicationManager : IDisposable ITaskManager TaskManager { get; } IGitClient GitClient { get; } IUsageTracker UsageTracker { get; } + IOAuthCallbackManager OAuthCallbackManager { get; } bool IsBusy { get; } void Run(); void InitializeRepository(); @@ -23,4 +24,4 @@ public interface IApplicationManager : IDisposable void SetupGit(GitInstaller.GitInstallationState state); void RestartRepository(); } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Authentication/OAuthCallbackListener.cs b/src/GitHub.Api/Authentication/OAuthCallbackListener.cs deleted file mode 100644 index 7bfc0df9f..000000000 --- a/src/GitHub.Api/Authentication/OAuthCallbackListener.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using GitHub.Logging; - -namespace GitHub.Unity -{ - public interface IOAuthCallbackListener - { - void Listen(string state, CancellationToken cancel, Action codeCallback); - } - - public class OAuthCallbackListener : IOAuthCallbackListener - { - private static readonly ILogging logger = LogHelper.GetLogger(); - - const int CallbackPort = 42424; - - static readonly string CallbackUrl = $"http://localhost:{CallbackPort}/"; - private string state; - private CancellationToken cancel; - private Action codeCallback; - private HttpListener httpListener; - - public void Listen(string state, CancellationToken cancel, Action codeCallback) - { - this.state = state; - this.cancel = cancel; - this.codeCallback = codeCallback; - - httpListener = new HttpListener(); - httpListener.Prefixes.Add(CallbackUrl); - httpListener.Start(); - Task.Factory.StartNew(Start, cancel); - } - - private void Start() - { - try - { - using (httpListener) - { - using (cancel.Register(httpListener.Stop)) - { - while (true) - { - var context = httpListener.GetContext(); - var queryParts = HttpUtility.ParseQueryString(context.Request.Url.Query); - - if (queryParts["state"] == state) - { - context.Response.Close(); - codeCallback(queryParts["code"]); - } - } - } - } - } - catch (Exception ex) - { - logger.Warning(ex, "OAuthCallbackListener Error"); - } - finally - { - httpListener = null; - } - } - } -} diff --git a/src/GitHub.Api/Authentication/OAuthCallbackManager.cs b/src/GitHub.Api/Authentication/OAuthCallbackManager.cs new file mode 100644 index 000000000..b8f00d033 --- /dev/null +++ b/src/GitHub.Api/Authentication/OAuthCallbackManager.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using GitHub.Logging; + +namespace GitHub.Unity +{ + public interface IOAuthCallbackManager + { + event Action OnCallback; + bool IsRunning { get; } + void Start(); + void Stop(); + } + + public class OAuthCallbackManager : IOAuthCallbackManager + { + const int CallbackPort = 42424; + public static readonly Uri CallbackUrl = new Uri($"http://localhost:{CallbackPort}/callback"); + + private static readonly ILogging logger = LogHelper.GetLogger(); + private static readonly object _lock = new object(); + + + private readonly CancellationTokenSource cancelSource; + + private HttpListener httpListener; + public bool IsRunning { get; private set; } + + public event Action OnCallback; + + public OAuthCallbackManager() + { + cancelSource = new CancellationTokenSource(); + } + + public void Start() + { + if (!IsRunning) + { + lock(_lock) + { + if (!IsRunning) + { + logger.Trace("Starting"); + + httpListener = new HttpListener(); + httpListener.Prefixes.Add(CallbackUrl.AbsoluteUri + "/"); + httpListener.Start(); + Task.Factory.StartNew(Listen, cancelSource.Token); + IsRunning = true; + } + } + } + } + + public void Stop() + { + logger.Debug("Stop"); + } + + private void Listen() + { + try + { + using (httpListener) + { + using (cancelSource.Token.Register(httpListener.Stop)) + { + while (true) + { + var context = httpListener.GetContext(); + var queryParts = HttpUtility.ParseQueryString(context.Request.Url.Query); + + var state = queryParts["state"]; + var code = queryParts["code"]; + + logger.Trace("OnCallback: {0}", state); + if (OnCallback != null) + { + OnCallback(state, code); + } + + context.Response.StatusCode = 200; + context.Response.OutputStream.Flush(); + context.Response.Close(); + } + } + } + } + catch (Exception ex) + { + logger.Warning(ex, "OAuthCallbackManager Error"); + } + finally + { + IsRunning = false; + httpListener = null; + } + } + } +} diff --git a/src/GitHub.Api/GitHub.Api.45.csproj b/src/GitHub.Api/GitHub.Api.45.csproj index 32554ceaa..df6496785 100644 --- a/src/GitHub.Api/GitHub.Api.45.csproj +++ b/src/GitHub.Api/GitHub.Api.45.csproj @@ -73,7 +73,7 @@ - + diff --git a/src/GitHub.Api/GitHub.Api.csproj b/src/GitHub.Api/GitHub.Api.csproj index 613ba08f9..af58e5d0d 100644 --- a/src/GitHub.Api/GitHub.Api.csproj +++ b/src/GitHub.Api/GitHub.Api.csproj @@ -84,7 +84,7 @@ - + @@ -338,23 +338,9 @@ --> - - - - - + + + \ No newline at end of file diff --git a/src/GitHub.Api/Platform/IEnvironment.cs b/src/GitHub.Api/Platform/IEnvironment.cs index 24a5130fb..df543ab62 100644 --- a/src/GitHub.Api/Platform/IEnvironment.cs +++ b/src/GitHub.Api/Platform/IEnvironment.cs @@ -42,4 +42,4 @@ public interface IEnvironment ISettings SystemSettings { get; } ISettings UserSettings { get; } } -} \ No newline at end of file +} diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index 567302a43..42b9e9494 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -5,24 +5,17 @@ namespace GitHub.Unity { - class AuthenticationService: IDisposable + class AuthenticationService { - private readonly ITaskManager taskManager; - private static readonly ILogging logger = LogHelper.GetLogger(); - private readonly IApiClient client; private LoginResult loginResultData; - private IOAuthCallbackListener oauthCallbackListener; - private CancellationTokenSource oauthCallbackCancellationToken; - private string oauthCallbackState; public AuthenticationService(UriString host, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment ) { - this.taskManager = taskManager; client = host == null ? new ApiClient(keychain, processManager, taskManager, environment) : new ApiClient(host, keychain, processManager, taskManager, environment); @@ -63,56 +56,14 @@ public void GetServerMeta(Action serverMeta, Action erro }); } - public Uri StartOAuthListener(Action onSuccess, Action onError) - { - if (oauthCallbackListener == null) - { - logger.Trace("Start OAuthCallbackListener"); - oauthCallbackListener = new OAuthCallbackListener(); - oauthCallbackCancellationToken = new CancellationTokenSource(); - - oauthCallbackState = Guid.NewGuid().ToString(); - oauthCallbackListener.Listen( - oauthCallbackState, - oauthCallbackCancellationToken.Token, - code => { - logger.Trace("OAuthCallbackListener Response: {0}", code); - - client.CreateOAuthToken(code, (b, s) => { - if (b) - { - onSuccess(); - } - else - { - onError(s); - } - }); - }); - } - - return GetLoginUrl(oauthCallbackState); - } - - public void StopOAuthListener() - { - if (oauthCallbackCancellationToken != null) - { - oauthCallbackCancellationToken.Cancel(); - } - - oauthCallbackListener = null; - oauthCallbackCancellationToken = null; - } - - private Uri GetLoginUrl(string state) + public Uri GetLoginUrl(string state) { var query = new StringBuilder(); query.Append("client_id="); query.Append(Uri.EscapeDataString(ApplicationInfo.ClientId)); query.Append("&redirect_uri="); - query.Append(Uri.EscapeDataString("http://localhost:42424/callback")); + query.Append(Uri.EscapeDataString(OAuthCallbackManager.CallbackUrl.ToString())); query.Append("&scope="); query.Append(Uri.EscapeDataString("user,repo")); query.Append("&state="); @@ -126,10 +77,9 @@ private Uri GetLoginUrl(string state) return uriBuilder.Uri; } - public void Dispose() + public void LoginWithOAuthCode(string code, Action result) { - if (oauthCallbackCancellationToken != null) - oauthCallbackCancellationToken.Dispose(); + client.CreateOAuthToken(code, result); } } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index f2caf459d..d66199141 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -30,6 +30,15 @@ public override void InitializeView(IView parent) gitHubAuthenticationView = gitHubAuthenticationView ?? new GitHubAuthenticationView(); gitHubEnterpriseAuthenticationView = gitHubEnterpriseAuthenticationView ?? new GitHubEnterpriseAuthenticationView(); + try + { + OAuthCallbackManager.Start(); + } + catch (Exception ex) + { + Logger.Trace(ex, "Error Starting OAuthCallbackManager"); + } + gitHubAuthenticationView.InitializeView(parent); gitHubEnterpriseAuthenticationView.InitializeView(parent); @@ -65,6 +74,12 @@ public override void OnDataUpdate() MaybeUpdateData(); } + public override void Finish(bool result) + { + base.Finish(result); + OAuthCallbackManager.Stop(); + } + private void MaybeUpdateData() { } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs index 19c23c409..b31dd9ea5 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -34,15 +34,23 @@ class GitHubAuthenticationView : Subview [NonSerialized] private bool enterPressed; [NonSerialized] private string password = string.Empty; [NonSerialized] private AuthenticationService authenticationService; - + [NonSerialized] private string oAuthState; + [NonSerialized] private string oAuthOpenUrl; public override void InitializeView(IView parent) { + Logger.Trace("InitializeView"); + base.InitializeView(parent); need2fa = isBusy = false; message = errorMessage = null; Title = WindowTitle; Size = viewSize; + + oAuthState = Guid.NewGuid().ToString(); + oAuthOpenUrl = AuthenticationService.GetLoginUrl(oAuthState).ToString(); + + OAuthCallbackManager.OnCallback += OnOAuthCallback; } public void Initialize(Exception exception) @@ -117,7 +125,7 @@ private void OnGUILogin() GUILayout.BeginHorizontal(); { - username = EditorGUILayout.TextField(UsernameLabel ,username, Styles.TextFieldStyle); + username = EditorGUILayout.TextField(UsernameLabel, username, Styles.TextFieldStyle); } GUILayout.EndHorizontal(); @@ -146,39 +154,24 @@ private void OnGUILogin() } GUILayout.EndHorizontal(); - GUILayout.Space(Styles.BaseSpacing + 3); - GUILayout.BeginHorizontal(); + if (OAuthCallbackManager.IsRunning) { - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Signin with your browser", Styles.HyperlinkStyle)) + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); { - GUI.FocusControl(null); - StartOAuthListener(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Signin with your browser", Styles.HyperlinkStyle)) + { + GUI.FocusControl(null); + Application.OpenURL(oAuthOpenUrl); + } } + GUILayout.EndHorizontal(); } - GUILayout.EndHorizontal(); } EditorGUI.EndDisabledGroup(); } - private void StartOAuthListener() - { - try - { - var uri = AuthenticationService.StartOAuthListener(() => DoResult(true, null), - s => { - errorMessage = s; - TaskManager.RunInUI(Redraw); - }); - Application.OpenURL(uri.ToString()); - } - catch (Exception ex) - { - errorMessage = ex.Message; - Redraw(); - } - } - private void OnGUI2FA() { GUILayout.BeginVertical(); @@ -218,6 +211,15 @@ private void OnGUI2FA() GUILayout.EndVertical(); } + private void OnOAuthCallback(string state, string code) + { + if (state.Equals(oAuthState)) + { + isBusy = true; + authenticationService.LoginWithOAuthCode(code, DoOAuthCodeResult); + } + } + private void DoRequire2fa(string msg) { need2fa = true; @@ -251,6 +253,23 @@ private void DoResult(bool success, string msg) } } + private void DoOAuthCodeResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + private void ShowMessage() { if (message != null) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs index 15353537b..fe3643964 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -40,6 +40,8 @@ class GitHubEnterpriseAuthenticationView : Subview [NonSerialized] private string password = string.Empty; [NonSerialized] private string token = string.Empty; [NonSerialized] private AuthenticationService authenticationService; + [NonSerialized] private string oAuthState; + [NonSerialized] private string oAuthOpenUrl; public override void InitializeView(IView parent) { @@ -48,6 +50,8 @@ public override void InitializeView(IView parent) message = errorMessage = null; Title = WindowTitle; Size = viewSize; + + OAuthCallbackManager.OnCallback += OnOAuthCallback; } public void Initialize(Exception exception) @@ -115,6 +119,21 @@ public override void OnGUI() { OnGUITokenLogin(); } + + if (OAuthCallbackManager.IsRunning) + { + GUILayout.Space(Styles.BaseSpacing + 3); + GUILayout.BeginHorizontal(); + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Signin with your browser", Styles.HyperlinkStyle)) + { + GUI.FocusControl(null); + Application.OpenURL(oAuthOpenUrl); + } + } + GUILayout.EndHorizontal(); + } } else { @@ -206,9 +225,7 @@ private void OnGUIUserPasswordLogin() if (GUILayout.Button("Back")) { GUI.FocusControl(null); - - hasServerMeta = false; - Redraw(); + BackToGetServerMeta(); } if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) @@ -224,6 +241,14 @@ private void OnGUIUserPasswordLogin() EditorGUI.EndDisabledGroup(); } + private void BackToGetServerMeta() + { + hasServerMeta = false; + oAuthOpenUrl = null; + oAuthState = null; + Redraw(); + } + private void OnGUITokenLogin() { EditorGUI.BeginDisabledGroup(isBusy); @@ -247,9 +272,7 @@ private void OnGUITokenLogin() if (GUILayout.Button("Back")) { GUI.FocusControl(null); - - hasServerMeta = false; - Redraw(); + BackToGetServerMeta(); } if (GUILayout.Button(LoginButton) || (!isBusy && enterPressed)) @@ -305,6 +328,17 @@ private void OnGUI2FA() GUILayout.EndVertical(); } + private void OnOAuthCallback(string state, string code) + { + TaskManager.RunInUI(() => { + if (state.Equals(oAuthState)) + { + isBusy = true; + authenticationService.LoginWithOAuthCode(code, DoOAuthCodeResult); + } + }); + } + private void DoServerMetaResult(GitHubHostMeta gitHubHostMeta) { hasServerMeta = true; @@ -371,6 +405,23 @@ private void DoTokenResult(bool success) } } + private void DoOAuthCodeResult(bool success, string msg) + { + isBusy = false; + if (success) + { + UsageTracker.IncrementAuthenticationViewButtonAuthentication(); + + Clear(); + Finish(true); + } + else + { + errorMessage = msg; + Redraw(); + } + } + private void ShowMessage() { if (message != null) @@ -400,6 +451,9 @@ private AuthenticationService GetAuthenticationService(string host) Manager.ProcessManager, Manager.TaskManager, Environment); + + oAuthState = Guid.NewGuid().ToString(); + oAuthOpenUrl = authenticationService.GetLoginUrl(oAuthState).ToString(); } return authenticationService; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs index c3c3f1514..4fbfb81e8 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Subview.cs @@ -92,6 +92,7 @@ public virtual void DoneRefreshing() protected IEnvironment Environment { get { return Manager.Environment; } } protected IPlatform Platform { get { return Manager.Platform; } } protected IUsageTracker UsageTracker { get { return Manager.UsageTracker; } } + protected IOAuthCallbackManager OAuthCallbackManager { get { return Manager.OAuthCallbackManager; } } public bool HasFocus { get { return Parent != null && Parent.HasFocus; } } public virtual bool IsBusy diff --git a/src/tests/IntegrationTests/IntegrationTestEnvironment.cs b/src/tests/IntegrationTests/IntegrationTestEnvironment.cs index 004864ab7..eb65b8187 100644 --- a/src/tests/IntegrationTests/IntegrationTestEnvironment.cs +++ b/src/tests/IntegrationTests/IntegrationTestEnvironment.cs @@ -133,5 +133,6 @@ public string GetSpecialFolder(Environment.SpecialFolder folder) public ISettings LocalSettings => defaultEnvironment.LocalSettings; public ISettings SystemSettings => defaultEnvironment.SystemSettings; public ISettings UserSettings => defaultEnvironment.UserSettings; + public IOAuthCallbackManager OAuthCallbackListener { get; } } } From 0b32077cff619be69441c93231611bb7c49ab11f Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 19 Oct 2018 15:27:25 -0400 Subject: [PATCH 459/567] Simplify ApiClient --- src/GitHub.Api/Application/ApiClient.cs | 19 +++++++++++-------- .../Authentication/OAuthCallbackManager.cs | 6 +++--- .../Services/AuthenticationService.cs | 4 +--- .../GitHub.Unity/UI/AuthenticationView.cs | 6 +++--- .../Editor/GitHub.Unity/UI/PopupWindow.cs | 4 ++-- .../Editor/GitHub.Unity/UI/PublishView.cs | 4 +--- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 8718d2707..25dbe2ca6 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -27,17 +27,20 @@ class ApiClient : IApiClient private IKeychainAdapter keychainAdapter; private Connection connection; - public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment) : - this(UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri), keychain, processManager, taskManager, environment) + public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, + IEnvironment environment, UriString host = null) { - } - - public ApiClient(UriString host, IKeychain keychain, IProcessManager processManager, ITaskManager taskManager, IEnvironment environment) - { - Guard.ArgumentNotNull(host, nameof(host)); Guard.ArgumentNotNull(keychain, nameof(keychain)); - host = new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + if (host == null) + { + host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); + } + else + { + host = new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); + } + HostAddress = HostAddress.Create(host); this.keychain = keychain; diff --git a/src/GitHub.Api/Authentication/OAuthCallbackManager.cs b/src/GitHub.Api/Authentication/OAuthCallbackManager.cs index b8f00d033..b5b75b094 100644 --- a/src/GitHub.Api/Authentication/OAuthCallbackManager.cs +++ b/src/GitHub.Api/Authentication/OAuthCallbackManager.cs @@ -60,7 +60,8 @@ public void Start() public void Stop() { - logger.Debug("Stop"); + logger.Trace("Stopping"); + cancelSource.Cancel(); } private void Listen() @@ -86,7 +87,6 @@ private void Listen() } context.Response.StatusCode = 200; - context.Response.OutputStream.Flush(); context.Response.Close(); } } @@ -94,7 +94,7 @@ private void Listen() } catch (Exception ex) { - logger.Warning(ex, "OAuthCallbackManager Error"); + logger.Trace(ex.Message); } finally { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs index 42b9e9494..b756bfa94 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Services/AuthenticationService.cs @@ -16,9 +16,7 @@ public AuthenticationService(UriString host, IKeychain keychain, IEnvironment environment ) { - client = host == null - ? new ApiClient(keychain, processManager, taskManager, environment) - : new ApiClient(host, keychain, processManager, taskManager, environment); + client = new ApiClient(keychain, processManager, taskManager, environment, host); } public HostAddress HostAddress { get { return client.HostAddress; } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs index d66199141..dbd51c4c7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/AuthenticationView.cs @@ -39,8 +39,8 @@ public override void InitializeView(IView parent) Logger.Trace(ex, "Error Starting OAuthCallbackManager"); } - gitHubAuthenticationView.InitializeView(parent); - gitHubEnterpriseAuthenticationView.InitializeView(parent); + gitHubAuthenticationView.InitializeView(this); + gitHubEnterpriseAuthenticationView.InitializeView(this); hasGitHubDotComConnection = Platform.Keychain.Connections.Any(HostAddress.IsGitHubDotCom); hasGitHubEnterpriseConnection = Platform.Keychain.Connections.Any(connection => !HostAddress.IsGitHubDotCom(connection)); @@ -76,8 +76,8 @@ public override void OnDataUpdate() public override void Finish(bool result) { - base.Finish(result); OAuthCallbackManager.Stop(); + base.Finish(result); } private void MaybeUpdateData() diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs index cfb1f67f8..abeff1ab4 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs @@ -120,8 +120,8 @@ private void Open(PopupViewType popupViewType, Action onClose) var userHasAuthentication = false; foreach (var keychainConnection in Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom)) { - var apiClient = new ApiClient(keychainConnection.Host, Platform.Keychain, Platform.ProcessManager, TaskManager, - Environment); + var apiClient = new ApiClient(Platform.Keychain, Platform.ProcessManager, TaskManager, + Environment, keychainConnection.Host); try { diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index c1c9fab51..d057adee7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -79,9 +79,7 @@ private IApiClient GetApiClient(Connection connection) if (!clients.TryGetValue(connection.Host, out client)) { - client = HostAddress.IsGitHubDotCom(connection) - ? new ApiClient(Platform.Keychain, Platform.ProcessManager, TaskManager, Environment) - : new ApiClient(connection.Host, Platform.Keychain, Platform.ProcessManager, TaskManager, Environment); + client = new ApiClient(Platform.Keychain, Platform.ProcessManager, TaskManager, Environment, connection.Host); clients.Add(connection.Host, client); } From 5615c0fd57d82ae6ffdc2a61ba77430618d32748 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Fri, 19 Oct 2018 15:32:01 -0400 Subject: [PATCH 460/567] Making sure the octorun zip is up to date --- src/GitHub.Api/Resources/octorun.zip | 4 ++-- src/GitHub.Api/Resources/octorun.zip.md5 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Resources/octorun.zip b/src/GitHub.Api/Resources/octorun.zip index 7eaca1f69..7b1374c9d 100644 --- a/src/GitHub.Api/Resources/octorun.zip +++ b/src/GitHub.Api/Resources/octorun.zip @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b38e3eb9c0178d67d0b33a8a2bc6118430b2d21b7ea7f8562a9cbf10b2df2f2 -size 221289 +oid sha256:95fe1967a6d00af4abb3d34b897769451f78bdf87ad132ba2b526264ac36e14a +size 214195 diff --git a/src/GitHub.Api/Resources/octorun.zip.md5 b/src/GitHub.Api/Resources/octorun.zip.md5 index 22a7a5055..70d55debe 100644 --- a/src/GitHub.Api/Resources/octorun.zip.md5 +++ b/src/GitHub.Api/Resources/octorun.zip.md5 @@ -1 +1 @@ -7e9bb4522ee6cc4d42ee10bd88f849a8 \ No newline at end of file +3ad23df7f5076a6fbd7d3ce03ad919cc From 0563f008d476382ff12fd78d95c1849d17c21760 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 09:08:11 -0400 Subject: [PATCH 461/567] Fixing CopyHelper error --- src/GitHub.Api/Installer/CopyHelper.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs index 22ec43579..0536426b5 100644 --- a/src/GitHub.Api/Installer/CopyHelper.cs +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -12,6 +12,8 @@ public static class CopyHelper public static void Copy(NPath fromPath, NPath toPath) { + Logger.Trace("Error copying from " + fromPath + " to " + toPath + "."); + try { @@ -19,7 +21,7 @@ public static void Copy(NPath fromPath, NPath toPath) } catch (Exception ex1) { - Logger.Warning(ex1, "Error copying from " + fromPath + " to " + toPath + ". Attempting to copy contents."); + Logger.Warning(ex1, "Error copying."); try { @@ -27,7 +29,7 @@ public static void Copy(NPath fromPath, NPath toPath) } catch (Exception ex2) { - Logger.Error(ex2, "Error copying from " + fromPath + " to " + toPath + "."); + Logger.Error(ex1, "Error copying contents."); throw; } } @@ -39,7 +41,7 @@ public static void Copy(NPath fromPath, NPath toPath) public static void CopyFolder(NPath fromPath, NPath toPath) { Logger.Trace("CopyFolder fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); - + toPath.DeleteIfExists(); toPath.EnsureParentDirectoryExists(); fromPath.Move(toPath); } From a3f9d3ca319b4bb4f2af3b68b0a94daef08cc5ca Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 09:12:33 -0400 Subject: [PATCH 462/567] Bumping version --- common/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/SolutionInfo.cs b/common/SolutionInfo.cs index aae47c51d..28e548bbb 100644 --- a/common/SolutionInfo.cs +++ b/common/SolutionInfo.cs @@ -32,8 +32,8 @@ namespace System { internal static class AssemblyVersionInformation { // this is for the AssemblyVersion and AssemblyVersion attributes, which can't handle alphanumerics - internal const string VersionForAssembly = "1.1.0"; + internal const string VersionForAssembly = "1.1.1"; // Actual real version - internal const string Version = "1.1.0"; + internal const string Version = "1.1.1"; } } From 9223adea56996e497d2e787a380e3330cbf814d6 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 09:30:41 -0400 Subject: [PATCH 463/567] Avoiding using the output of the zip Task because it will shorten the path names --- src/GitHub.Api/Installer/CopyHelper.cs | 1 - src/GitHub.Api/Installer/GitInstaller.cs | 12 ++++++------ src/GitHub.Api/Installer/OctorunInstaller.cs | 7 ++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs index 0536426b5..c489f2e19 100644 --- a/src/GitHub.Api/Installer/CopyHelper.cs +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -49,7 +49,6 @@ public static void CopyFolder(NPath fromPath, NPath toPath) public static void CopyFolderContents(NPath fromPath, NPath toPath) { Logger.Trace("CopyFolderContents fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); - toPath.DeleteContents(); fromPath.MoveFiles(toPath, true); } diff --git a/src/GitHub.Api/Installer/GitInstaller.cs b/src/GitHub.Api/Installer/GitInstaller.cs index 67b502a94..2c38cff89 100644 --- a/src/GitHub.Api/Installer/GitInstaller.cs +++ b/src/GitHub.Api/Installer/GitInstaller.cs @@ -301,13 +301,13 @@ private GitInstallationState ExtractGit(GitInstallationState state) return true; }); unzipTask.Progress(p => Progress.UpdateProgress(40 + (long)(20 * p.Percentage), 100, unzipTask.Message)); - var source = unzipTask.RunSynchronously(); + unzipTask.RunSynchronously(); var target = state.GitInstallationPath; if (unzipTask.Successful) { - Logger.Trace("Moving Git source:{0} target:{1}", source.ToString(), target.ToString()); + Logger.Trace("Moving Git source:{0} target:{1}", gitExtractPath.ToString(), target.ToString()); - CopyHelper.Copy(source, target); + CopyHelper.Copy(gitExtractPath, target); state.GitIsValid = true; @@ -327,13 +327,13 @@ private GitInstallationState ExtractGit(GitInstallationState state) return true; }); unzipTask.Progress(p => Progress.UpdateProgress(60 + (long)(20 * p.Percentage), 100, unzipTask.Message)); - var source = unzipTask.RunSynchronously(); + unzipTask.RunSynchronously(); var target = state.GitLfsInstallationPath; if (unzipTask.Successful) { - Logger.Trace("Moving GitLFS source:{0} target:{1}", source.ToString(), target.ToString()); + Logger.Trace("Moving GitLFS source:{0} target:{1}", gitLfsExtractPath.ToString(), target.ToString()); - CopyHelper.Copy(source, target); + CopyHelper.Copy(gitLfsExtractPath, target); state.GitLfsIsValid = true; } diff --git a/src/GitHub.Api/Installer/OctorunInstaller.cs b/src/GitHub.Api/Installer/OctorunInstaller.cs index 0aa655d93..6b989677b 100644 --- a/src/GitHub.Api/Installer/OctorunInstaller.cs +++ b/src/GitHub.Api/Installer/OctorunInstaller.cs @@ -33,12 +33,13 @@ public NPath SetupOctorunIfNeeded() GrabZipFromResources(); - var tempZipExtractPath = NPath.CreateTempDirectory("octorun_extract_archive_path"); + var extractPath = NPath.CreateTempDirectory("octorun_extract_archive_path"); var unzipTask = new UnzipTask(taskManager.Token, installDetails.ZipFile, - tempZipExtractPath, sharpZipLibHelper, + extractPath, sharpZipLibHelper, fileSystem) .Catch(e => { Logger.Error(e, "Error extracting octorun"); return true; }); - var extractPath = unzipTask.RunSynchronously(); + unzipTask.RunSynchronously(); + if (unzipTask.Successful) path = MoveOctorun(extractPath.Combine("octorun")); return path; From be418849906545c1557ab2976964c559340ee416 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 09:59:25 -0400 Subject: [PATCH 464/567] Fixing log message --- src/GitHub.Api/Installer/CopyHelper.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs index c489f2e19..3b7f52290 100644 --- a/src/GitHub.Api/Installer/CopyHelper.cs +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -12,11 +12,10 @@ public static class CopyHelper public static void Copy(NPath fromPath, NPath toPath) { - Logger.Trace("Error copying from " + fromPath + " to " + toPath + "."); + Logger.Trace("Copying from " + fromPath + " to " + toPath + "."); try { - CopyFolder(fromPath, toPath); } catch (Exception ex1) @@ -48,7 +47,7 @@ public static void CopyFolder(NPath fromPath, NPath toPath) public static void CopyFolderContents(NPath fromPath, NPath toPath) { - Logger.Trace("CopyFolderContents fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + Logger.Trace("CopyFolder Contents fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); toPath.DeleteContents(); fromPath.MoveFiles(toPath, true); } From 3ae57aee7101e2aafb6ffd04d49a51bebd173736 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 13:09:59 -0400 Subject: [PATCH 465/567] Reverting some unintentional changes --- .../GitHub/Editor/AsyncBridge.Net35.dll.meta | 46 ++-- .../Plugins/GitHub/Editor/GitHub.Api.dll.meta | 46 ++-- .../GitHub/Editor/GitHub.Unity.dll.meta | 46 ++-- .../ReadOnlyCollectionsInterfaces.dll.meta | 46 ++-- .../GitHub/Editor/System.Threading.dll.meta | 46 ++-- .../ProjectSettings/DynamicsManager.asset | 2 - .../ProjectSettings/Physics2DSettings.asset | 7 +- .../ProjectSettings/DynamicsManager.asset | 2 - .../ProjectSettings/Physics2DSettings.asset | 7 +- .../ProjectSettings/ProjectSettings.asset | 212 +++--------------- .../ProjectSettings/ProjectVersion.txt | 2 +- 11 files changed, 153 insertions(+), 309 deletions(-) diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta index c0727ba52..1c1d85763 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/AsyncBridge.Net35.dll.meta @@ -1,32 +1,34 @@ fileFormatVersion: 2 guid: d516f2a1bec6a9645a084ef8c9237132 -timeCreated: 1539278074 +timeCreated: 1491391262 licenseType: Free PluginImporter: - externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta index 6cddd7b96..d12a12326 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Api.dll.meta @@ -1,32 +1,34 @@ fileFormatVersion: 2 guid: c743ae24ee231884887054d20ccdd0ae -timeCreated: 1539278075 +timeCreated: 1491391261 licenseType: Free PluginImporter: - externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta index 4520eb5a5..a70aca527 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/GitHub.Unity.dll.meta @@ -1,32 +1,34 @@ fileFormatVersion: 2 guid: 68c7e4565cde54155bb78d8e935f1dd4 -timeCreated: 1539278078 +timeCreated: 1527097377 licenseType: Free PluginImporter: - externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta index ae75f164a..98b231bec 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/ReadOnlyCollectionsInterfaces.dll.meta @@ -1,32 +1,34 @@ fileFormatVersion: 2 guid: 48c22d5d7479fcb49ab3be0cdd2ccec0 -timeCreated: 1539278074 +timeCreated: 1491391260 licenseType: Free PluginImporter: - externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta b/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta index 01e7881ac..ea6a32d4c 100644 --- a/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta +++ b/unity/PackageProject/Assets/Plugins/GitHub/Editor/System.Threading.dll.meta @@ -1,32 +1,34 @@ fileFormatVersion: 2 guid: 790749ba7e4b18141953e39cb13f1b79 -timeCreated: 1539278074 +timeCreated: 1491392717 licenseType: Free PluginImporter: - externalObjects: {} serializedVersion: 2 iconMap: {} executionOrder: {} isPreloaded: 0 isOverridable: 0 platformData: - - first: - Any: - second: - enabled: 0 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - DefaultValueInitialized: true - - first: - Windows Store Apps: WindowsStoreApps - second: - enabled: 0 - settings: - CPU: AnyCPU - userData: - assetBundleName: - assetBundleVariant: + data: + first: + Any: + second: + enabled: 0 + settings: {} + data: + first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + data: + first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/PackageProject/ProjectSettings/DynamicsManager.asset b/unity/PackageProject/ProjectSettings/DynamicsManager.asset index 0be3d787c..6be69106a 100644 --- a/unity/PackageProject/ProjectSettings/DynamicsManager.asset +++ b/unity/PackageProject/ProjectSettings/DynamicsManager.asset @@ -16,5 +16,3 @@ PhysicsManager: m_EnableAdaptiveForce: 0 m_EnablePCM: 1 m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - m_AutoSimulation: 1 - m_AutoSyncTransforms: 1 diff --git a/unity/PackageProject/ProjectSettings/Physics2DSettings.asset b/unity/PackageProject/ProjectSettings/Physics2DSettings.asset index 132ee6bc8..dd4738c2f 100644 --- a/unity/PackageProject/ProjectSettings/Physics2DSettings.asset +++ b/unity/PackageProject/ProjectSettings/Physics2DSettings.asset @@ -3,7 +3,7 @@ --- !u!19 &1 Physics2DSettings: m_ObjectHideFlags: 0 - serializedVersion: 3 + serializedVersion: 2 m_Gravity: {x: 0, y: -9.81} m_DefaultMaterial: {fileID: 0} m_VelocityIterations: 8 @@ -13,18 +13,15 @@ Physics2DSettings: m_MaxAngularCorrection: 8 m_MaxTranslationSpeed: 100 m_MaxRotationSpeed: 360 + m_MinPenetrationForPenalty: 0.01 m_BaumgarteScale: 0.2 m_BaumgarteTimeOfImpactScale: 0.75 m_TimeToSleep: 0.5 m_LinearSleepTolerance: 0.01 m_AngularSleepTolerance: 2 - m_DefaultContactOffset: 0.01 - m_AutoSimulation: 1 m_QueriesHitTriggers: 1 m_QueriesStartInColliders: 1 m_ChangeStopsCallbacks: 0 - m_CallbacksOnDisable: 1 - m_AutoSyncTransforms: 1 m_AlwaysShowColliders: 0 m_ShowColliderSleep: 1 m_ShowColliderContacts: 0 diff --git a/unity/TestProject/ProjectSettings/DynamicsManager.asset b/unity/TestProject/ProjectSettings/DynamicsManager.asset index 0be3d787c..6be69106a 100644 --- a/unity/TestProject/ProjectSettings/DynamicsManager.asset +++ b/unity/TestProject/ProjectSettings/DynamicsManager.asset @@ -16,5 +16,3 @@ PhysicsManager: m_EnableAdaptiveForce: 0 m_EnablePCM: 1 m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - m_AutoSimulation: 1 - m_AutoSyncTransforms: 1 diff --git a/unity/TestProject/ProjectSettings/Physics2DSettings.asset b/unity/TestProject/ProjectSettings/Physics2DSettings.asset index 132ee6bc8..dd4738c2f 100644 --- a/unity/TestProject/ProjectSettings/Physics2DSettings.asset +++ b/unity/TestProject/ProjectSettings/Physics2DSettings.asset @@ -3,7 +3,7 @@ --- !u!19 &1 Physics2DSettings: m_ObjectHideFlags: 0 - serializedVersion: 3 + serializedVersion: 2 m_Gravity: {x: 0, y: -9.81} m_DefaultMaterial: {fileID: 0} m_VelocityIterations: 8 @@ -13,18 +13,15 @@ Physics2DSettings: m_MaxAngularCorrection: 8 m_MaxTranslationSpeed: 100 m_MaxRotationSpeed: 360 + m_MinPenetrationForPenalty: 0.01 m_BaumgarteScale: 0.2 m_BaumgarteTimeOfImpactScale: 0.75 m_TimeToSleep: 0.5 m_LinearSleepTolerance: 0.01 m_AngularSleepTolerance: 2 - m_DefaultContactOffset: 0.01 - m_AutoSimulation: 1 m_QueriesHitTriggers: 1 m_QueriesStartInColliders: 1 m_ChangeStopsCallbacks: 0 - m_CallbacksOnDisable: 1 - m_AutoSyncTransforms: 1 m_AlwaysShowColliders: 0 m_ShowColliderSleep: 1 m_ShowColliderContacts: 0 diff --git a/unity/TestProject/ProjectSettings/ProjectSettings.asset b/unity/TestProject/ProjectSettings/ProjectSettings.asset index 019193686..4a6b72fcf 100644 --- a/unity/TestProject/ProjectSettings/ProjectSettings.asset +++ b/unity/TestProject/ProjectSettings/ProjectSettings.asset @@ -3,10 +3,9 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 13 + serializedVersion: 10 productGUID: 0190cf875796f4b46a0ef8d5e39cdfd9 AndroidProfiler: 0 - AndroidFilterTouchesWhenObscured: 0 defaultScreenOrientation: 4 targetDevice: 2 useOnDemandResources: 0 @@ -15,7 +14,7 @@ PlayerSettings: productName: UnityProject defaultCursor: {fileID: 0} cursorHotspot: {x: 0, y: 0} - m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_SplashScreenBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21176471, a: 1} m_ShowUnitySplashScreen: 1 m_ShowUnitySplashLogo: 1 m_SplashScreenOverlayOpacity: 1 @@ -39,6 +38,8 @@ PlayerSettings: width: 1 height: 1 m_SplashScreenLogos: [] + m_SplashScreenBackgroundLandscape: {fileID: 0} + m_SplashScreenBackgroundPortrait: {fileID: 0} m_VirtualRealitySplashScreen: {fileID: 0} m_HolographicTrackingLossScreen: {fileID: 0} defaultScreenWidth: 1024 @@ -48,6 +49,7 @@ PlayerSettings: m_StereoRenderingPath: 0 m_ActiveColorSpace: 0 m_MTRendering: 1 + m_MobileMTRendering: 0 m_StackTraceTypes: 010000000100000001000000010000000100000001000000 iosShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1 @@ -62,22 +64,18 @@ PlayerSettings: useOSAutorotation: 1 use32BitDisplayBuffer: 1 disableDepthAndStencilBuffers: 0 - androidBlitType: 0 defaultIsFullScreen: 1 defaultIsNativeResolution: 1 - macRetinaSupport: 1 runInBackground: 0 captureSingleScreen: 0 muteOtherAudioSources: 0 Prepare IOS For Recording: 0 - Force IOS Speakers When Recording: 0 submitAnalytics: 1 usePlayerLog: 1 bakeCollisionMeshes: 0 forceSingleInstance: 0 resizableWindow: 0 useMacAppStoreValidation: 0 - macAppStoreCategory: public.app-category.games gpuSkinning: 0 graphicsJobs: 0 xboxPIXTextureCapture: 0 @@ -87,7 +85,6 @@ PlayerSettings: xboxEnableFitness: 0 visibleInBackground: 0 allowFullscreenSwitch: 1 - graphicsJobMode: 0 macFullscreenMode: 2 d3d9FullscreenMode: 1 d3d11FullscreenMode: 1 @@ -95,16 +92,14 @@ PlayerSettings: xboxEnableHeadOrientation: 0 xboxEnableGuest: 0 xboxEnablePIXSampling: 0 - metalFramebufferOnly: 0 n3dsDisableStereoscopicView: 0 n3dsEnableSharedListOpt: 1 n3dsEnableVSync: 0 + uiUse16BitDepthBuffer: 0 ignoreAlphaClear: 0 xboxOneResolution: 0 xboxOneMonoLoggingLevel: 0 xboxOneLoggingLevel: 1 - xboxOneDisableEsram: 0 - xboxOnePresentImmediateThreshold: 0 videoMemoryForVertexBuffers: 0 psp2PowerMode: 0 psp2AcquireBGM: 1 @@ -123,60 +118,36 @@ PlayerSettings: 16:10: 1 16:9: 1 Others: 1 + bundleIdentifier: com.Company.ProductName bundleVersion: 1.0 preloadedAssets: [] metroInputSource: 0 m_HolographicPauseOnTrackingLoss: 1 xboxOneDisableKinectGpuReservation: 0 - xboxOneEnable7thCore: 0 - vrSettings: - cardboard: - depthFormat: 0 - enableTransitionView: 0 - daydream: - depthFormat: 0 - useSustainedPerformanceMode: 0 - enableVideoLayer: 0 - useProtectedVideoMemory: 0 - hololens: - depthFormat: 1 protectGraphicsMemory: 0 - useHDRDisplay: 0 - m_ColorGamuts: 00000000 - targetPixelDensity: 0 - resolutionScalingMode: 0 - androidSupportedAspectRatio: 1 - androidMaxAspectRatio: 2.1 - applicationIdentifier: - Android: com.Company.ProductName - Standalone: unity.DefaultCompany.UnityProject - Tizen: com.Company.ProductName - iOS: com.Company.ProductName - tvOS: com.Company.ProductName - buildNumber: - iOS: 0 AndroidBundleVersionCode: 1 - AndroidMinSdkVersion: 16 - AndroidTargetSdkVersion: 0 + AndroidMinSdkVersion: 9 AndroidPreferredInstallLocation: 1 aotOptions: + apiCompatibilityLevel: 2 stripEngineCode: 1 iPhoneStrippingLevel: 0 iPhoneScriptCallOptimization: 0 + iPhoneBuildNumber: 0 ForceInternetPermission: 0 ForceSDCardPermission: 0 CreateWallpaper: 0 APKExpansionFiles: 0 - keepLoadedShadersAlive: 0 + preloadShaders: 0 StripUnusedMeshComponents: 0 VertexChannelCompressionMask: serializedVersion: 2 m_Bits: 238 iPhoneSdkVersion: 988 - iOSTargetOSVersionString: 7.0 + iOSTargetOSVersionString: tvOSSdkVersion: 0 tvOSRequireExtendedGameController: 0 - tvOSTargetOSVersionString: 9.0 + tvOSTargetOSVersionString: uIPrerenderedIcon: 0 uIRequiresPersistentWiFi: 0 uIRequiresFullScreen: 1 @@ -219,13 +190,7 @@ PlayerSettings: iOSURLSchemes: [] iOSBackgroundModes: 0 iOSMetalForceHardShadows: 0 - metalEditorSupport: 1 - metalAPIValidation: 1 - iOSRenderExtraFrameOnPause: 1 appleDeveloperTeamID: - iOSManualSigningProvisioningProfileID: - tvOSManualSigningProvisioningProfileID: - appleEnableAutomaticSigning: 0 AndroidTargetDevice: 0 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} @@ -233,9 +198,7 @@ PlayerSettings: AndroidKeyaliasName: AndroidTVCompatibility: 1 AndroidIsGame: 1 - AndroidEnableTango: 0 androidEnableBanner: 1 - androidUseLowAccuracyLocation: 0 m_AndroidBanners: - width: 320 height: 180 @@ -246,13 +209,10 @@ PlayerSettings: m_BuildTargetBatching: [] m_BuildTargetGraphicsAPIs: [] m_BuildTargetVRSettings: [] - m_BuildTargetEnableVuforiaSettings: [] openGLRequireES31: 0 openGLRequireES31AEP: 0 + webPlayerTemplate: APPLICATION:Default m_TemplateCustomTags: {} - mobileMTRendering: - iPhone: 1 - tvOS: 1 wiiUTitleID: 0005000011000000 wiiUGroupID: 00010000 wiiUCommonSaveSize: 4096 @@ -271,7 +231,6 @@ PlayerSettings: wiiUGamePadStartupScreen: {fileID: 0} wiiUDrcBufferDisabled: 0 wiiUProfilerLibPath: - playModeTestRunnerEnabled: 0 actionOnDotNetUnhandledException: 1 enableInternalProfiler: 0 logObjCUncaughtExceptions: 1 @@ -279,116 +238,16 @@ PlayerSettings: cameraUsageDescription: locationUsageDescription: microphoneUsageDescription: - switchNetLibKey: - switchSocketMemoryPoolSize: 6144 - switchSocketAllocatorPoolSize: 128 - switchSocketConcurrencyLimit: 14 - switchScreenResolutionBehavior: 2 - switchUseCPUProfiler: 0 - switchApplicationID: 0x01004b9000490000 - switchNSODependencies: - switchTitleNames_0: - switchTitleNames_1: - switchTitleNames_2: - switchTitleNames_3: - switchTitleNames_4: - switchTitleNames_5: - switchTitleNames_6: - switchTitleNames_7: - switchTitleNames_8: - switchTitleNames_9: - switchTitleNames_10: - switchTitleNames_11: - switchPublisherNames_0: - switchPublisherNames_1: - switchPublisherNames_2: - switchPublisherNames_3: - switchPublisherNames_4: - switchPublisherNames_5: - switchPublisherNames_6: - switchPublisherNames_7: - switchPublisherNames_8: - switchPublisherNames_9: - switchPublisherNames_10: - switchPublisherNames_11: - switchIcons_0: {fileID: 0} - switchIcons_1: {fileID: 0} - switchIcons_2: {fileID: 0} - switchIcons_3: {fileID: 0} - switchIcons_4: {fileID: 0} - switchIcons_5: {fileID: 0} - switchIcons_6: {fileID: 0} - switchIcons_7: {fileID: 0} - switchIcons_8: {fileID: 0} - switchIcons_9: {fileID: 0} - switchIcons_10: {fileID: 0} - switchIcons_11: {fileID: 0} - switchSmallIcons_0: {fileID: 0} - switchSmallIcons_1: {fileID: 0} - switchSmallIcons_2: {fileID: 0} - switchSmallIcons_3: {fileID: 0} - switchSmallIcons_4: {fileID: 0} - switchSmallIcons_5: {fileID: 0} - switchSmallIcons_6: {fileID: 0} - switchSmallIcons_7: {fileID: 0} - switchSmallIcons_8: {fileID: 0} - switchSmallIcons_9: {fileID: 0} - switchSmallIcons_10: {fileID: 0} - switchSmallIcons_11: {fileID: 0} - switchManualHTML: - switchAccessibleURLs: - switchLegalInformation: - switchMainThreadStackSize: 1048576 - switchPresenceGroupId: - switchLogoHandling: 0 - switchReleaseVersion: 0 - switchDisplayVersion: 1.0.0 - switchStartupUserAccount: 0 - switchTouchScreenUsage: 0 - switchSupportedLanguagesMask: 0 - switchLogoType: 0 - switchApplicationErrorCodeCategory: - switchUserAccountSaveDataSize: 0 - switchUserAccountSaveDataJournalSize: 0 - switchApplicationAttribute: 0 - switchCardSpecSize: -1 - switchCardSpecClock: -1 - switchRatingsMask: 0 - switchRatingsInt_0: 0 - switchRatingsInt_1: 0 - switchRatingsInt_2: 0 - switchRatingsInt_3: 0 - switchRatingsInt_4: 0 - switchRatingsInt_5: 0 - switchRatingsInt_6: 0 - switchRatingsInt_7: 0 - switchRatingsInt_8: 0 - switchRatingsInt_9: 0 - switchRatingsInt_10: 0 - switchRatingsInt_11: 0 - switchLocalCommunicationIds_0: - switchLocalCommunicationIds_1: - switchLocalCommunicationIds_2: - switchLocalCommunicationIds_3: - switchLocalCommunicationIds_4: - switchLocalCommunicationIds_5: - switchLocalCommunicationIds_6: - switchLocalCommunicationIds_7: - switchParentalControl: 0 - switchAllowsScreenshot: 1 - switchDataLossConfirmation: 0 - switchSupportedNpadStyles: 3 - switchSocketConfigEnabled: 0 - switchTcpInitialSendBufferSize: 32 - switchTcpInitialReceiveBufferSize: 64 - switchTcpAutoSendBufferSizeMax: 256 - switchTcpAutoReceiveBufferSizeMax: 256 - switchUdpSendBufferSize: 9 - switchUdpReceiveBufferSize: 42 - switchSocketBufferEfficiency: 4 - switchSocketInitializeEnabled: 1 - switchNetworkInterfaceManagerInitializeEnabled: 1 - switchPlayerConnectionEnabled: 1 + XboxTitleId: + XboxImageXexPath: + XboxSpaPath: + XboxGenerateSpa: 0 + XboxDeployKinectResources: 0 + XboxSplashScreen: {fileID: 0} + xboxEnableSpeech: 0 + xboxAdditionalTitleMemorySize: 0 + xboxDeployKinectHeadOrientation: 0 + xboxDeployKinectHeadPosition: 0 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -401,7 +260,6 @@ PlayerSettings: ps4ParamSfxPath: ps4VideoOutPixelFormat: 0 ps4VideoOutInitialWidth: 1920 - ps4VideoOutBaseModeInitialWidth: 1920 ps4VideoOutReprojectionRate: 120 ps4PronunciationXMLPath: ps4PronunciationSIGPath: @@ -424,8 +282,8 @@ PlayerSettings: ps4ApplicationParam4: 0 ps4DownloadDataSize: 0 ps4GarlicHeapSize: 2048 - ps4ProGarlicHeapSize: 2560 ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ + ps4UseDebugIl2cppLibs: 0 ps4pnSessions: 1 ps4pnPresence: 1 ps4pnFriends: 1 @@ -449,9 +307,6 @@ PlayerSettings: ps4attribShareSupport: 0 ps4attribExclusiveVR: 0 ps4disableAutoHideSplash: 0 - ps4videoRecordingFeaturesUsed: 0 - ps4contentSearchFeaturesUsed: 0 - ps4attribEyeToEyeDistanceSettingVR: 0 ps4IncludedModules: [] monoEnv: psp2Splashimage: {fileID: 0} @@ -500,14 +355,13 @@ PlayerSettings: psp2UseLibLocation: 0 psp2InfoBarOnStartup: 0 psp2InfoBarColor: 0 - psp2ScriptOptimizationLevel: 0 + psp2UseDebugIl2cppLibs: 0 psmSplashimage: {fileID: 0} splashScreenBackgroundSourceLandscape: {fileID: 0} splashScreenBackgroundSourcePortrait: {fileID: 0} spritePackerPolicy: webGLMemorySize: 256 webGLExceptionSupport: 1 - webGLNameFilesAsHashes: 0 webGLDataCaching: 0 webGLDebugSymbols: 0 webGLEmscriptenArgs: @@ -522,8 +376,6 @@ PlayerSettings: scriptingBackend: {} incrementalIl2cppBuild: {} additionalIl2CppArgs: - scriptingRuntimeVersion: 0 - apiCompatibilityLevelPerPlatform: {} m_RenderingPath: 1 m_MobileRenderingPath: 1 metroPackageName: UnityProject @@ -559,7 +411,7 @@ PlayerSettings: tizenMicrophonePermissions: 0 tizenDeploymentTarget: tizenDeploymentTargetType: 0 - tizenMinOSVersion: 1 + tizenMinOSVersion: 0 n3dsUseExtSaveData: 0 n3dsCompressStaticMem: 1 n3dsExtSaveDataNumber: 0x12345 @@ -599,17 +451,9 @@ PlayerSettings: XboxOneSplashScreen: {fileID: 0} XboxOneAllowedProductIds: [] XboxOnePersistentLocalStorageSize: 0 - xboxOneScriptCompiler: 0 - vrEditorSettings: - daydream: - daydreamIconForeground: {fileID: 0} - daydreamIconBackground: {fileID: 0} + vrEditorSettings: {} cloudServicesEnabled: {} - facebookSdkVersion: 7.9.4 - apiCompatibilityLevel: 2 cloudProjectId: projectName: organizationId: cloudEnabled: 0 - enableNativePlatformBackendsForNewInputSystem: 0 - disableOldInputManagerSupport: 0 diff --git a/unity/TestProject/ProjectSettings/ProjectVersion.txt b/unity/TestProject/ProjectSettings/ProjectVersion.txt index 7a6fffb8b..66e05aa78 100644 --- a/unity/TestProject/ProjectSettings/ProjectVersion.txt +++ b/unity/TestProject/ProjectSettings/ProjectVersion.txt @@ -1 +1 @@ -m_EditorVersion: 2017.2.0f3 +m_EditorVersion: 5.5.0f3 From 872389b864a4bf5f3221eb9f92396187f7c7fd29 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 13:10:25 -0400 Subject: [PATCH 466/567] Code cleanup --- src/GitHub.Api/Tasks/OctorunTask.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/GitHub.Api/Tasks/OctorunTask.cs b/src/GitHub.Api/Tasks/OctorunTask.cs index be90c835a..06c3c6c0c 100644 --- a/src/GitHub.Api/Tasks/OctorunTask.cs +++ b/src/GitHub.Api/Tasks/OctorunTask.cs @@ -71,27 +71,7 @@ public OctorunTask(CancellationToken token, IEnvironment environment, this.pathToNodeJs = environment.NodeJsExecutablePath; this.pathToOctorunJs = environment.OctorunScriptPath; this.arguments = $"\"{pathToOctorunJs}\" {arguments}"; - this.userToken = userToken; - -// var cloneUrl = environment.Repository?.CloneUrl; -// var host = String.IsNullOrEmpty(cloneUrl) -// ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) -// : new UriString(cloneUrl.ToRepositoryUri() -// .GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); -// -// var adapter = keychain.Connect(host); -// if (adapter.Credential?.Token != null) -// { -// userToken = adapter.Credential.Token; -// } -// else -// { -// // use a cached adapter if there is one filled out -// adapter = keychain.LoadFromSystem(host); -// if (adapter != null) -// userToken = adapter.Credential.Token; -// } } public override void Configure(ProcessStartInfo psi) From 67aa02d359a3b799b62e4d09d8f14ee4a7b2eb58 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Mon, 22 Oct 2018 13:19:22 -0400 Subject: [PATCH 467/567] More cleanup --- src/GitHub.Api/Application/ApiClient.cs | 11 +++-------- src/GitHub.Api/Application/ApplicationManagerBase.cs | 2 -- src/GitHub.Api/Authentication/ILoginManager.cs | 12 ++++++++---- src/GitHub.Api/Authentication/Keychain.cs | 2 -- src/GitHub.Api/Authentication/LoginManager.cs | 4 +--- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 1 - 6 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 25dbe2ca6..c5c648f79 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -32,14 +32,9 @@ public ApiClient(IKeychain keychain, IProcessManager processManager, ITaskManage { Guard.ArgumentNotNull(keychain, nameof(keychain)); - if (host == null) - { - host = UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri); - } - else - { - host = new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); - } + host = host == null + ? UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri) + : new UriString(host.ToRepositoryUri().GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)); HostAddress = HostAddress.Create(host); diff --git a/src/GitHub.Api/Application/ApplicationManagerBase.cs b/src/GitHub.Api/Application/ApplicationManagerBase.cs index 755a2f22c..dd2b60893 100644 --- a/src/GitHub.Api/Application/ApplicationManagerBase.cs +++ b/src/GitHub.Api/Application/ApplicationManagerBase.cs @@ -210,8 +210,6 @@ public void SetupGit(GitInstaller.GitInstallationState state) return true; }).RunSynchronously(); - Logger.Trace("Credential Helper: {0}", credentialHelper); - if (string.IsNullOrEmpty(credentialHelper)) { Logger.Warning("No Windows CredentialHelper found: Setting to wincred"); diff --git a/src/GitHub.Api/Authentication/ILoginManager.cs b/src/GitHub.Api/Authentication/ILoginManager.cs index 7235d7fac..c78112c0e 100644 --- a/src/GitHub.Api/Authentication/ILoginManager.cs +++ b/src/GitHub.Api/Authentication/ILoginManager.cs @@ -10,7 +10,7 @@ interface ILoginManager /// /// Attempts to log into a GitHub server with a username and password. /// - /// + /// The host. /// The username. /// The password. /// The logged in user. @@ -28,8 +28,12 @@ interface ILoginManager /// ITask Logout(UriString hostAddress); - bool LoginWithToken( - UriString host, - string token); + /// + /// Attempts to log into a GitHub server with a token. + /// + /// The host. + /// The token. + /// + bool LoginWithToken(UriString host, string token); } } diff --git a/src/GitHub.Api/Authentication/Keychain.cs b/src/GitHub.Api/Authentication/Keychain.cs index b1f038ce3..a32cc3c42 100644 --- a/src/GitHub.Api/Authentication/Keychain.cs +++ b/src/GitHub.Api/Authentication/Keychain.cs @@ -133,8 +133,6 @@ private KeychainAdapter FindOrCreateAdapter(UriString host) KeychainAdapter value; if (!keychainAdapters.TryGetValue(host, out value)) { - logger.Trace("Creating Adapter {0}", host); - value = new KeychainAdapter(); keychainAdapters.Add(host, value); } diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 8e02b698c..e78f1b099 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -45,9 +45,7 @@ public LoginManager( this.environment = environment; } - public bool LoginWithToken( - UriString host, - string token) + public bool LoginWithToken(UriString host, string token) { Guard.ArgumentNotNull(host, nameof(host)); Guard.ArgumentNotNullOrWhiteSpace(token, nameof(token)); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 274f873d2..7487ae3d3 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -2,7 +2,6 @@ using System.Linq; using UnityEditor; using UnityEngine; -using Object = System.Object; namespace GitHub.Unity { From c7c9a1557da838e7b3d155bc4d9ef6021f97f0e6 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 23 Oct 2018 11:38:29 -0400 Subject: [PATCH 468/567] Correcting usage of WebUri and ApiUri --- src/GitHub.Api/Application/ApiClient.cs | 8 ++++---- src/GitHub.Api/Primitives/HostAddress.cs | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index c5c648f79..dc3a0f6e5 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -262,7 +262,7 @@ public void LoginWithToken(string token, Action result) Guard.ArgumentNotNull(result, "result"); new FuncTask(taskManager.Token, - () => loginManager.LoginWithToken(HostAddress.ApiUri.Host, token)) + () => loginManager.LoginWithToken(HostAddress.WebUri.Host, token)) .FinallyInUI((success, ex, res) => { if (!success) @@ -279,7 +279,7 @@ public void LoginWithToken(string token, Action result) public void CreateOAuthToken(string code, Action result) { - var command = "token -h " + HostAddress.WebUri.Host; + var command = "token -h " + HostAddress.ApiUri.Host; var octorunTask = new OctorunTask(taskManager.Token, environment, command, code) .Configure(processManager); @@ -321,7 +321,7 @@ public void Login(string username, string password, Action need2faC Guard.ArgumentNotNull(result, "result"); new FuncTask(taskManager.Token, - () => loginManager.Login(HostAddress.ApiUri.Host, username, password)) + () => loginManager.Login(HostAddress.WebUri.Host, username, password)) .FinallyInUI((success, ex, res) => { if (!success) @@ -388,7 +388,7 @@ private Connection Connection { if (connection == null) { - connection = keychain.Connections.FirstOrDefault(x => x.Host == (UriString)HostAddress.ApiUri.Host); + connection = keychain.Connections.FirstOrDefault(x => x.Host == (UriString)HostAddress.WebUri.Host); } return connection; diff --git a/src/GitHub.Api/Primitives/HostAddress.cs b/src/GitHub.Api/Primitives/HostAddress.cs index a53bd8772..2382f98c1 100644 --- a/src/GitHub.Api/Primitives/HostAddress.cs +++ b/src/GitHub.Api/Primitives/HostAddress.cs @@ -87,9 +87,11 @@ public static bool IsGitHubDotCom(Connection connection) if (connection == null || String.IsNullOrEmpty(connection.Host)) return false; - return connection.Host == GitHubDotComHostAddress.WebUri.Host - || connection.Host == GitHubDotComHostAddress.ApiUri.Host - || connection.Host == gistUri.Host; + var connectionHost = connection.Host.ToUriString(); + + return connectionHost.Host == GitHubDotComHostAddress.WebUri.Host + || connectionHost.Host == GitHubDotComHostAddress.ApiUri.Host + || connectionHost.Host == gistUri.Host; } public bool IsGitHubDotCom() From 26a9d0a09a29ea307a7ef4c629ed70dc917169f4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 23 Oct 2018 11:54:26 -0400 Subject: [PATCH 469/567] Fixing the logging of CopyHelper --- src/GitHub.Api/Installer/CopyHelper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/GitHub.Api/Installer/CopyHelper.cs b/src/GitHub.Api/Installer/CopyHelper.cs index 3b7f52290..0876ec19b 100644 --- a/src/GitHub.Api/Installer/CopyHelper.cs +++ b/src/GitHub.Api/Installer/CopyHelper.cs @@ -12,7 +12,7 @@ public static class CopyHelper public static void Copy(NPath fromPath, NPath toPath) { - Logger.Trace("Copying from " + fromPath + " to " + toPath + "."); + Logger.Trace("Copying from {0} to {1}", fromPath, toPath); try { @@ -28,7 +28,7 @@ public static void Copy(NPath fromPath, NPath toPath) } catch (Exception ex2) { - Logger.Error(ex1, "Error copying contents."); + Logger.Error(ex2, "Error copying contents."); throw; } } @@ -39,7 +39,7 @@ public static void Copy(NPath fromPath, NPath toPath) } public static void CopyFolder(NPath fromPath, NPath toPath) { - Logger.Trace("CopyFolder fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + Logger.Trace("CopyFolder from {0} to {1}", fromPath, toPath); toPath.DeleteIfExists(); toPath.EnsureParentDirectoryExists(); fromPath.Move(toPath); @@ -47,7 +47,7 @@ public static void CopyFolder(NPath fromPath, NPath toPath) public static void CopyFolderContents(NPath fromPath, NPath toPath) { - Logger.Trace("CopyFolder Contents fromPath: {0} toPath:{1}", fromPath.ToString(), toPath.ToString()); + Logger.Trace("CopyFolder Contents from {0} to {1}", fromPath, toPath); toPath.DeleteContents(); fromPath.MoveFiles(toPath, true); } From 94d7fa1082e6739901f95e608694c050e03f7b49 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 23 Oct 2018 15:43:18 -0400 Subject: [PATCH 470/567] Tiny fixes with hostnames and web/api urls --- src/GitHub.Api/Application/ApiClient.cs | 6 +++--- .../Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs | 4 ++-- .../GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs | 2 +- .../Assets/Editor/GitHub.Unity/UI/PublishView.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index dc3a0f6e5..0f0c8a7c6 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -262,7 +262,7 @@ public void LoginWithToken(string token, Action result) Guard.ArgumentNotNull(result, "result"); new FuncTask(taskManager.Token, - () => loginManager.LoginWithToken(HostAddress.WebUri.Host, token)) + () => loginManager.LoginWithToken(UriString.ToUriString(HostAddress.WebUri), token)) .FinallyInUI((success, ex, res) => { if (!success) @@ -279,7 +279,7 @@ public void LoginWithToken(string token, Action result) public void CreateOAuthToken(string code, Action result) { - var command = "token -h " + HostAddress.ApiUri.Host; + var command = "token -h " + HostAddress.WebUri.Host; var octorunTask = new OctorunTask(taskManager.Token, environment, command, code) .Configure(processManager); @@ -388,7 +388,7 @@ private Connection Connection { if (connection == null) { - connection = keychain.Connections.FirstOrDefault(x => x.Host == (UriString)HostAddress.WebUri.Host); + connection = keychain.Connections.FirstOrDefault(x => x.Host.ToUriString().Host == HostAddress.WebUri.Host); } return connection; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs index b31dd9ea5..80e2a3aa4 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -216,7 +216,7 @@ private void OnOAuthCallback(string state, string code) if (state.Equals(oAuthState)) { isBusy = true; - authenticationService.LoginWithOAuthCode(code, DoOAuthCodeResult); + authenticationService.LoginWithOAuthCode(code, (b, s) => TaskManager.RunInUI(() => DoOAuthCodeResult(b, s))); } } @@ -292,7 +292,7 @@ private AuthenticationService AuthenticationService { if (authenticationService == null) { - AuthenticationService = new AuthenticationService(HostAddress.GitHubDotComHostAddress.WebUri.Host, Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); + AuthenticationService = new AuthenticationService(UriString.ToUriString(HostAddress.GitHubDotComHostAddress.WebUri), Platform.Keychain, Manager.ProcessManager, Manager.TaskManager, Environment); } return authenticationService; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs index fe3643964..da02f3a84 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -334,7 +334,7 @@ private void OnOAuthCallback(string state, string code) if (state.Equals(oAuthState)) { isBusy = true; - authenticationService.LoginWithOAuthCode(code, DoOAuthCodeResult); + authenticationService.LoginWithOAuthCode(code, (b, s) => TaskManager.RunInUI(() => DoOAuthCodeResult(b, s))); } }); } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs index d057adee7..832cfa7c7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PublishView.cs @@ -59,7 +59,7 @@ private void MaybeUpdateData() { connectionsNeedLoading = false; connections = Platform.Keychain.Connections.OrderByDescending(HostAddress.IsGitHubDotCom).ToArray(); - connectionLabels = connections.Select(c => HostAddress.IsGitHubDotCom(c) ? "GitHub" : c.Host).ToArray(); + connectionLabels = connections.Select(c => HostAddress.IsGitHubDotCom(c) ? "GitHub" : c.Host.ToUriString().Host).ToArray(); var connection = connections.First(); selectedConnection = 0; From 0f623d5ee2eb6c6da528ce20cf3aec0c5091dd34 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Wed, 24 Oct 2018 18:23:05 -0400 Subject: [PATCH 471/567] Fixing Prompt --- .../Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs | 2 +- .../GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs index 80e2a3aa4..921d190b0 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubAuthenticationView.cs @@ -160,7 +160,7 @@ private void OnGUILogin() GUILayout.BeginHorizontal(); { GUILayout.FlexibleSpace(); - if (GUILayout.Button("Signin with your browser", Styles.HyperlinkStyle)) + if (GUILayout.Button("Sign in with your browser", Styles.HyperlinkStyle)) { GUI.FocusControl(null); Application.OpenURL(oAuthOpenUrl); diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs index da02f3a84..29f2e93e7 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/GitHubEnterpriseAuthenticationView.cs @@ -126,7 +126,7 @@ public override void OnGUI() GUILayout.BeginHorizontal(); { GUILayout.FlexibleSpace(); - if (GUILayout.Button("Signin with your browser", Styles.HyperlinkStyle)) + if (GUILayout.Button("Sign in with your browser", Styles.HyperlinkStyle)) { GUI.FocusControl(null); Application.OpenURL(oAuthOpenUrl); From 5ae520f12b37bdd6c73e8a188a73c0dc602faea4 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Thu, 1 Nov 2018 17:17:37 -0400 Subject: [PATCH 472/567] Update how-to-build.md --- docs/contributing/how-to-build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/how-to-build.md b/docs/contributing/how-to-build.md index 0a8aba41d..13ef8c2fa 100644 --- a/docs/contributing/how-to-build.md +++ b/docs/contributing/how-to-build.md @@ -13,7 +13,7 @@ This repository is LFS-enabled. To clone it, you should use a git client that su ### MacOS -- Mono 4.x required. You can install it via brew with `brew tap shana/mono && brew install mono@4.8` +- [Mono 4.x](https://download.mono-project.com/archive/4.8.1/macos-10-universal/) required. You can install it via brew with `brew tap shana/mono && brew install mono@4.8` - Mono 5.x will not work - `UnityEngine.dll` and `UnityEditor.dll`. - If you've installed Unity in the default location of `/Applications/Unity`, the build will be able to reference these DLLs automatically. Otherwise, you'll need to copy these DLLs from `[Unity installation path]/Unity.app/Contents/Managed` into the `lib` directory in order for the build to work From 94c4344d900ed36f0c1a17c9c269d36114999464 Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 6 Nov 2018 16:31:23 -0500 Subject: [PATCH 473/567] Always get the username from the Api because we have no idea what the user gave us Co-authored-by: iamwillshepherd --- src/GitHub.Api/Authentication/LoginManager.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/GitHub.Api/Authentication/LoginManager.cs b/src/GitHub.Api/Authentication/LoginManager.cs index 9a67986af..8ab6f9f18 100644 --- a/src/GitHub.Api/Authentication/LoginManager.cs +++ b/src/GitHub.Api/Authentication/LoginManager.cs @@ -73,7 +73,7 @@ public LoginResultData Login( if (loginResultData.Code == LoginResultCodes.Success) { - username = RetrieveUsername(loginResultData, username); + username = RetrieveUsername(); keychainAdapter.Update(loginResultData.Token, username); keychain.SaveToSystem(host); } @@ -113,7 +113,7 @@ public LoginResultData ContinueLogin(LoginResultData loginResultData, string two } keychainAdapter.Update(loginResultData.Token, username); - username = RetrieveUsername(loginResultData, username); + username = RetrieveUsername(); keychainAdapter.Update(loginResultData.Token, username); keychain.SaveToSystem(host); @@ -180,13 +180,8 @@ private LoginResultData TryLogin( return new LoginResultData(LoginResultCodes.Failed, ret.GetApiErrorMessage() ?? "Failed.", host); } - private string RetrieveUsername(LoginResultData loginResultData, string username) + private string RetrieveUsername() { - if (!username.Contains("@")) - { - return username; - } - var octorunTask = new OctorunTask(taskManager.Token, keychain, environment, "validate") .Configure(processManager); From 6fc7964690374703071a14d2322ea1e870a1a7fe Mon Sep 17 00:00:00 2001 From: Stanley Goldman Date: Tue, 6 Nov 2018 16:31:57 -0500 Subject: [PATCH 474/567] Ignore case when comparing username Co-authored-by: iamwillshepherd --- src/GitHub.Api/Application/ApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 01d189def..b73f63aca 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -255,7 +255,7 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha { var login = ret.Output[1]; - if (login != keychainConnection.Username) + if (!string.Equals(login, keychainConnection.Username, StringComparison.InvariantCultureIgnoreCase)) { logger.Trace("LoadKeychainInternal: Api username does not match"); throw new TokenUsernameMismatchException(keychainConnection.Username, login); From afec12d40756006c20fc9dcac08bce52c6d82d8c Mon Sep 17 00:00:00 2001 From: Sam Christiansen Date: Thu, 8 Nov 2018 15:25:10 -0800 Subject: [PATCH 475/567] File History --- src/GitHub.Api/Application/ApiClient.cs | 4 +- src/GitHub.Api/Application/IApiClient.cs | 2 +- src/GitHub.Api/Application/Organization.cs | 4 +- src/GitHub.Api/Git/GitClient.cs | 34 +++ src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs | 23 +- src/GitHub.Api/Git/Tasks/GitLogTask.cs | 16 +- .../Editor/GitHub.Unity/UI/BaseWindow.cs | 2 +- .../Editor/GitHub.Unity/UI/ContextMenu.cs | 36 +++ .../GitHub.Unity/UI/FileHistoryWindow.cs | 244 ++++++++++++++++++ .../Editor/GitHub.Unity/UI/HistoryView.cs | 2 +- .../Editor/GitHub.Unity/UI/PopupWindow.cs | 2 +- 11 files changed, 359 insertions(+), 10 deletions(-) create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ContextMenu.cs create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs diff --git a/src/GitHub.Api/Application/ApiClient.cs b/src/GitHub.Api/Application/ApiClient.cs index 01d189def..51d13fe38 100644 --- a/src/GitHub.Api/Application/ApiClient.cs +++ b/src/GitHub.Api/Application/ApiClient.cs @@ -283,13 +283,13 @@ private GitHubUser GetValidatedGitHubUser(Connection keychainConnection, IKeycha } } - class GitHubUser + public class GitHubUser { public string Name { get; set; } public string Login { get; set; } } - class GitHubRepository + public class GitHubRepository { public string Name { get; set; } public string CloneUrl { get; set; } diff --git a/src/GitHub.Api/Application/IApiClient.cs b/src/GitHub.Api/Application/IApiClient.cs index 650595ce2..1bd5fc070 100644 --- a/src/GitHub.Api/Application/IApiClient.cs +++ b/src/GitHub.Api/Application/IApiClient.cs @@ -2,7 +2,7 @@ namespace GitHub.Unity { - interface IApiClient + public interface IApiClient { HostAddress HostAddress { get; } UriString OriginalUrl { get; } diff --git a/src/GitHub.Api/Application/Organization.cs b/src/GitHub.Api/Application/Organization.cs index e78849dd6..8deea7d99 100644 --- a/src/GitHub.Api/Application/Organization.cs +++ b/src/GitHub.Api/Application/Organization.cs @@ -1,8 +1,8 @@ namespace GitHub.Unity { - class Organization + public class Organization { public string Name { get; set; } public string Login { get; set; } } -} \ No newline at end of file +} diff --git a/src/GitHub.Api/Git/GitClient.cs b/src/GitHub.Api/Git/GitClient.cs index 5e454f6fa..d0e97787c 100644 --- a/src/GitHub.Api/Git/GitClient.cs +++ b/src/GitHub.Api/Git/GitClient.cs @@ -199,6 +199,15 @@ public interface IGitClient /// String output of git command ITask DiscardAll(IOutputProcessor processor = null); + /// + /// Executes at least one `git checkout` command to checkout files at the given changeset + /// + /// The md5 of the changeset + /// The files to check out + /// A custom output processor instance + /// String output of git command + ITask CheckoutVersion(string changeset, IEnumerable files, IOutputProcessor processor = null); + /// /// Executes at least one `git reset HEAD` command to remove files from the git index. /// @@ -241,6 +250,13 @@ public interface IGitClient /// of output ITask> Log(BaseOutputListProcessor processor = null); + /// + /// Executes `git log -- ` to get the history of a specific file. + /// + /// A custom output processor instance + /// of output + ITask> LogFile(NPath file, BaseOutputListProcessor processor = null); + /// /// Executes `git --version` to get the git version. /// @@ -332,6 +348,17 @@ public ITask> Log(BaseOutputListProcessor process .Then((success, list) => success ? list : new List()); } + /// + public ITask> LogFile(NPath file, BaseOutputListProcessor processor = null) + { + return new GitLogTask(file, new GitObjectFactory(environment), cancellationToken, processor) + .Configure(processManager) + .Catch(exception => exception is ProcessException && + exception.Message.StartsWith("fatal: your current branch") && + exception.Message.EndsWith("does not have any commits yet")) + .Then((success, list) => success ? list : new List()); + } + /// public ITask Version(IOutputProcessor processor = null) { @@ -549,6 +576,13 @@ public ITask DiscardAll(IOutputProcessor processor = null) .Configure(processManager); } + /// + public ITask CheckoutVersion(string changeset, IEnumerable files, IOutputProcessor processor = null) + { + return new GitCheckoutTask(changeset, files, cancellationToken, processor) + .Configure(processManager); + } + /// public ITask Remove(IList files, IOutputProcessor processor = null) diff --git a/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs b/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs index ea2e9f4d3..401281e22 100644 --- a/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitCheckoutTask.cs @@ -30,8 +30,29 @@ public GitCheckoutTask(CancellationToken token, arguments = "checkout -- ."; } + public GitCheckoutTask( + string changeset, + IEnumerable files, + CancellationToken token, + IOutputProcessor processor = null) : base(token, processor ?? new SimpleOutputProcessor()) + { + Guard.ArgumentNotNull(files, "files"); + Name = TaskName; + + arguments = "checkout "; + arguments += changeset; + arguments += " -- "; + + foreach (var file in files) + { + arguments += " \"" + file.ToNPath().ToString(SlashMode.Forward) + "\""; + } + + Message = "Checking out files at rev " + changeset.Substring(0, 7); + } + public override string ProcessArguments { get { return arguments; } } public override TaskAffinity Affinity { get { return TaskAffinity.Exclusive; } } - public override string Message { get; set; } = "Checking out branch..."; + public override string Message { get; set; } = "Checking out files..."; } } \ No newline at end of file diff --git a/src/GitHub.Api/Git/Tasks/GitLogTask.cs b/src/GitHub.Api/Git/Tasks/GitLogTask.cs index 955521a61..a55e72e5c 100644 --- a/src/GitHub.Api/Git/Tasks/GitLogTask.cs +++ b/src/GitHub.Api/Git/Tasks/GitLogTask.cs @@ -5,17 +5,31 @@ namespace GitHub.Unity class GitLogTask : ProcessTaskWithListOutput { private const string TaskName = "git log"; + private const string baseArguments = @"-c i18n.logoutputencoding=utf8 -c core.quotepath=false log --pretty=format:""%H%n%P%n%aN%n%aE%n%aI%n%cN%n%cE%n%cI%n%B---GHUBODYEND---"" --name-status"; + private readonly string arguments; public GitLogTask(IGitObjectFactory gitObjectFactory, CancellationToken token, BaseOutputListProcessor processor = null) : base(token, processor ?? new LogEntryOutputProcessor(gitObjectFactory)) { Name = TaskName; + arguments = baseArguments; + } + + public GitLogTask(NPath file, + IGitObjectFactory gitObjectFactory, + CancellationToken token, BaseOutputListProcessor processor = null) + : base(token, processor ?? new LogEntryOutputProcessor(gitObjectFactory)) + { + Name = TaskName; + arguments = baseArguments; + arguments += " -- "; + arguments += " \"" + file.ToString(SlashMode.Forward) + "\""; } public override string ProcessArguments { - get { return @"-c i18n.logoutputencoding=utf8 -c core.quotepath=false log --pretty=format:""%H%n%P%n%aN%n%aE%n%aI%n%cN%n%cE%n%cI%n%B---GHUBODYEND---"" --name-status"; } + get { return arguments; } } public override string Message { get; set; } = "Loading the history..."; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs index fa21bebf3..134af3c2a 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/BaseWindow.cs @@ -7,7 +7,7 @@ namespace GitHub.Unity { - abstract class BaseWindow : EditorWindow, IView + public abstract class BaseWindow : EditorWindow, IView { [NonSerialized] private bool initialized = false; [NonSerialized] private IUser cachedUser; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ContextMenu.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ContextMenu.cs new file mode 100644 index 000000000..95872fb45 --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/ContextMenu.cs @@ -0,0 +1,36 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + public class ContextMenu + { + [MenuItem("Assets/Git/History", false)] + private static void GitFileHistory() + { + if (Selection.assetGUIDs != null) + { + int maxWindowsToOpen = 10; + int windowsOpened = 0; + foreach(var guid in Selection.assetGUIDs) + { + var assetPath = AssetDatabase.GUIDToAssetPath(guid); + FileHistoryWindow.OpenWindow(assetPath); + windowsOpened++; + if (windowsOpened >= maxWindowsToOpen) + { + break; + } + } + } + } + + [MenuItem("Assets/Git/History", true)] + private static bool GitFileHistoryValidation() + { + return Selection.assetGUIDs != null && Selection.assetGUIDs.Length > 0; + } + } +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs new file mode 100644 index 000000000..bf4d6989f --- /dev/null +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/FileHistoryWindow.cs @@ -0,0 +1,244 @@ +using System.Collections.Generic; +using System.Linq; +using System; +using UnityEngine; +using UnityEditor; + +namespace GitHub.Unity +{ + public class FileHistoryWindow : BaseWindow + { + [SerializeField] private string assetPath; + [SerializeField] private List history; + [SerializeField] private Vector2 scroll; + [SerializeField] private Vector2 detailsScroll; + [NonSerialized] private bool busy; + [SerializeField] private HistoryControl historyControl; + [SerializeField] private GitLogEntry selectedEntry = GitLogEntry.Default; + [SerializeField] private ChangesTree treeChanges = new ChangesTree { IsSelectable = false, DisplayRootNode = false }; + + public static FileHistoryWindow OpenWindow(string assetPath) + { + var popupWindow = CreateInstance(); + + popupWindow.titleContent = new GUIContent(assetPath + " History"); + popupWindow.Open(assetPath); + + popupWindow.Show(); + + return popupWindow; + } + + public override bool IsBusy { get { return this.busy; } } + + public void Open(string assetPath) + { + this.assetPath = assetPath; + + this.RefreshLog(); + } + + public void RefreshLog() + { + var path = Application.dataPath.ToNPath().Parent.Combine(assetPath.ToNPath()); + this.busy = true; + this.GitClient.LogFile(path).ThenInUI((success, logEntries) => { + this.history = logEntries; + this.BuildHistoryControl(); + this.Repaint(); + this.busy = false; + }).Start(); + } + + private void CheckoutVersion(string commitID) + { + this.busy = true; + this.GitClient.CheckoutVersion(commitID, new string[]{assetPath}).ThenInUI((success, result) => { + AssetDatabase.Refresh(); + this.busy = false; + }).Start(); + } + + private void Checkout() + { + // TODO: This is a destructive, irreversible operation; we should prompt user if + // there are any changes to the file + this.CheckoutVersion(this.selectedEntry.CommitID); + } + + public override void OnUI() + { + // TODO: + // - should handle case where the file is outside of the repository (handle exceptional cases) + // - should display a spinner while history is still loading... + base.OnUI(); + GUILayout.BeginHorizontal(Styles.HeaderStyle); + { + GUILayout.Label("GIT File History for: ", Styles.BoldLabel); + if (HyperlinkLabel(this.assetPath)) + { + var asset = AssetDatabase.LoadMainAssetAtPath(this.assetPath); + Selection.activeObject = asset; + EditorGUIUtility.PingObject(asset); + } + GUILayout.FlexibleSpace(); + } + GUILayout.EndHorizontal(); + + if (historyControl != null) + { + var rect = GUILayoutUtility.GetLastRect(); + var historyControlRect = new Rect(0f, 0f, Position.width, Position.height - rect.height); + + var requiresRepaint = historyControl.Render(historyControlRect, + entry => { + selectedEntry = entry; + BuildTree(); + }, + entry => { }, entry => { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("Checkout version " + entry.ShortID), false, Checkout); + menu.ShowAsContext(); + }); + + if (requiresRepaint) + Redraw(); + } + + // DrawDetails is maybe irrelevant? Would be a nice place to put the short id perhaps? + DrawDetails(); + } + + private bool HyperlinkLabel(string label) + { + bool returnValue = false; + if (GUILayout.Button(label, HyperlinkStyle)) + { + returnValue = true; + } + var rect = GUILayoutUtility.GetLastRect(); + var size = HyperlinkStyle.CalcSize(new GUIContent(label)); + rect.width = size.x; + EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); + return returnValue; + } + + private void BuildHistoryControl() + { + if (historyControl == null) + { + historyControl = new HistoryControl(); + } + + historyControl.Load(0, this.history); + } + + private const string CommitDetailsTitle = "Commit details"; + private const string ClearSelectionButton = "×"; + + private void DrawDetails() + { + if (!selectedEntry.Equals(GitLogEntry.Default)) + { + // Top bar for scrolling to selection or clearing it + GUILayout.BeginHorizontal(EditorStyles.toolbar); + { + if (GUILayout.Button(CommitDetailsTitle, Styles.ToolbarButtonStyle)) + { + historyControl.ScrollTo(historyControl.SelectedIndex); + } + if (GUILayout.Button(ClearSelectionButton, Styles.ToolbarButtonStyle, GUILayout.ExpandWidth(false))) + { + selectedEntry = GitLogEntry.Default; + historyControl.SelectedIndex = -1; + } + } + GUILayout.EndHorizontal(); + + // Log entry details - including changeset tree (if any changes are found) + detailsScroll = GUILayout.BeginScrollView(detailsScroll, GUILayout.Height(250)); + { + HistoryDetailsEntry(selectedEntry); + + GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); + GUILayout.Label("Files changed", EditorStyles.boldLabel); + GUILayout.Space(-5); + + var rect = GUILayoutUtility.GetLastRect(); + GUILayout.BeginHorizontal(Styles.HistoryFileTreeBoxStyle); + GUILayout.BeginVertical(); + { + var borderLeft = Styles.Label.margin.left; + var treeControlRect = new Rect(rect.x + borderLeft, rect.y, Position.width - borderLeft * 2, Position.height - rect.height + Styles.CommitAreaPadding); + var treeRect = new Rect(0f, 0f, 0f, 0f); + if (treeChanges != null) + { + treeChanges.FolderStyle = Styles.Foldout; + treeChanges.TreeNodeStyle = Styles.TreeNode; + treeChanges.ActiveTreeNodeStyle = Styles.ActiveTreeNode; + treeChanges.FocusedTreeNodeStyle = Styles.FocusedTreeNode; + treeChanges.FocusedActiveTreeNodeStyle = Styles.FocusedActiveTreeNode; + + treeRect = treeChanges.Render(treeControlRect, detailsScroll, + node => { + }, + node => { + }, + node => { + }); + + if (treeChanges.RequiresRepaint) + Redraw(); + } + + GUILayout.Space(treeRect.y - treeControlRect.y); + } + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + + GUILayout.Space(EditorGUIUtility.standardVerticalSpacing); + } + GUILayout.EndScrollView(); + } + } + + private void HistoryDetailsEntry(GitLogEntry entry) + { + GUILayout.BeginVertical(Styles.HeaderBoxStyle); + GUILayout.Label(entry.Summary, Styles.HistoryDetailsTitleStyle); + + GUILayout.Space(-5); + + GUILayout.BeginHorizontal(); + GUILayout.Label(entry.PrettyTimeString, Styles.HistoryDetailsMetaInfoStyle); + GUILayout.Label(entry.AuthorName, Styles.HistoryDetailsMetaInfoStyle); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + + GUILayout.Space(3); + GUILayout.EndVertical(); + } + + private void BuildTree() + { + treeChanges.PathSeparator = Environment.FileSystem.DirectorySeparatorChar.ToString(); + treeChanges.Load(selectedEntry.changes.Select(entry => new GitStatusEntryTreeData(entry))); + Redraw(); + } + + protected static GUIStyle hyperlinkStyle = null; + + public static GUIStyle HyperlinkStyle + { + get + { + if (hyperlinkStyle == null) + { + hyperlinkStyle = new GUIStyle(EditorStyles.wordWrappedLabel); + hyperlinkStyle.normal.textColor = new Color(95.0f/255.0f, 170.0f/255.0f, 247.0f/255.0f); + } + return hyperlinkStyle; + } + } + } +} \ No newline at end of file diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs index 69b42e83e..f35423133 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/HistoryView.cs @@ -7,7 +7,7 @@ namespace GitHub.Unity { [Serializable] - class HistoryControl + public class HistoryControl { private const string HistoryEntryDetailFormat = "{0} {1}"; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs index d7da7cfbe..85fea7a02 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/PopupWindow.cs @@ -5,7 +5,7 @@ namespace GitHub.Unity { [Serializable] - class PopupWindow : BaseWindow + public class PopupWindow : BaseWindow { public enum PopupViewType { From f98513dd28d9bb68ab5bf58c4bc916b43038328c Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 19 Nov 2018 14:49:48 +0100 Subject: [PATCH 476/567] Refactor icon cache so we cache all the icons, not just some, and can easily invert and cache icons for dark themes --- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 202 +++--------------- .../Editor/GitHub.Unity/Misc/Utility.cs | 23 +- 2 files changed, 48 insertions(+), 177 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index 206843df9..99974d215 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -96,33 +96,6 @@ class Styles locksViewLockedByStyle, locksViewLockedBySelectedStyle; - private static Texture2D branchIcon, - activeBranchIcon, - trackingBranchIcon, - favoriteIconOn, - favoriteIconOff, - smallLogoIcon, - bigLogoIcon, - folderIcon, - mergeIcon, - dotIcon, - localCommitIcon, - repoIcon, - lockIcon, - emptyStateInit, - dropdownListIcon, - globeIcon, - spinnerInside, - spinnerOutside, - code, - rocket, - merge, - spinnerInsideInverted, - spinnerOutsideInverted, - codeInverted, - rocketInverted, - mergeInverted; - public static Texture2D GetFileStatusIcon(GitFileStatus status, bool isLocked) { if (isLocked) @@ -833,11 +806,7 @@ public static Texture2D ActiveBranchIcon { get { - if (activeBranchIcon == null) - { - activeBranchIcon = Utility.GetIcon("current-branch-indicator.png", "current-branch-indicator@2x.png"); - } - return activeBranchIcon; + return Utility.GetIcon("current-branch-indicator.png", "current-branch-indicator@2x.png"); } } @@ -845,11 +814,7 @@ public static Texture2D BranchIcon { get { - if (branchIcon == null) - { - branchIcon = Utility.GetIcon("branch.png", "branch@2x.png"); - } - return branchIcon; + return Utility.GetIcon("branch.png", "branch@2x.png"); } } @@ -857,12 +822,7 @@ public static Texture2D TrackingBranchIcon { get { - if (trackingBranchIcon == null) - { - trackingBranchIcon = Utility.GetIcon("tracked-branch-indicator.png"); - } - - return trackingBranchIcon; + return Utility.GetIcon("tracked-branch-indicator.png"); } } @@ -870,12 +830,7 @@ public static Texture2D FavoriteIconOn { get { - if (favoriteIconOn == null) - { - favoriteIconOn = Utility.GetIcon("favorite-branch-indicator.png"); - } - - return favoriteIconOn; + return Utility.GetIcon("favorite-branch-indicator.png"); } } @@ -883,12 +838,7 @@ public static Texture2D FavoriteIconOff { get { - if (favoriteIconOff == null) - { - favoriteIconOff = FolderIcon; - } - - return favoriteIconOff; + return FolderIcon; } } @@ -896,12 +846,7 @@ public static Texture2D SmallLogo { get { - if (smallLogoIcon == null) - { - smallLogoIcon = Utility.GetIcon("small-logo.png"); - } - - return smallLogoIcon; + return Utility.GetIcon("small-logo.png"); } } @@ -909,16 +854,7 @@ public static Texture2D BigLogo { get { - if (bigLogoIcon == null) - { - var defaultTextColor = Label.normal.textColor; - if (defaultTextColor.r > 0.5f && defaultTextColor.g > 0.5f && defaultTextColor.b > 0.5f) - bigLogoIcon = Utility.GetIcon("big-logo-light.png"); - else - bigLogoIcon = Utility.GetIcon("big-logo.png"); - } - - return bigLogoIcon; + return Utility.IsDarkTheme ? Utility.GetIcon("big-logo-light.png") : Utility.GetIcon("big-logo.png"); } } @@ -926,12 +862,7 @@ public static Texture2D MergeIcon { get { - if (mergeIcon == null) - { - mergeIcon = Utility.GetIcon("git-merge.png", "git-merge@2x.png"); - } - - return mergeIcon; + return Utility.GetIcon("git-merge.png", "git-merge@2x.png"); } } @@ -939,12 +870,7 @@ public static Texture2D DotIcon { get { - if (dotIcon == null) - { - dotIcon = Utility.GetIcon("dot.png", "dot@2x.png"); - } - - return dotIcon; + return Utility.GetIcon("dot.png", "dot@2x.png"); } } @@ -952,12 +878,7 @@ public static Texture2D LocalCommitIcon { get { - if (localCommitIcon == null) - { - localCommitIcon = Utility.GetIcon("local-commit-icon.png", "local-commit-icon@2x.png"); - } - - return localCommitIcon; + return Utility.GetIcon("local-commit-icon.png", "local-commit-icon@2x.png"); } } @@ -965,12 +886,7 @@ public static Texture2D FolderIcon { get { - if (folderIcon == null) - { - folderIcon = EditorGUIUtility.FindTexture("Folder Icon"); - } - - return folderIcon; + return EditorGUIUtility.FindTexture("Folder Icon"); } } @@ -978,11 +894,7 @@ public static Texture2D RepoIcon { get { - if (repoIcon == null) - { - repoIcon = Utility.GetIcon("repo.png", "repo@2x.png"); - } - return repoIcon; + return Utility.GetIcon("repo.png", "repo@2x.png"); } } @@ -990,11 +902,7 @@ public static Texture2D LockIcon { get { - if (lockIcon == null) - { - lockIcon = Utility.GetIcon("lock.png", "lock@2x.png"); - } - return lockIcon; + return Utility.GetIcon("lock.png", "lock@2x.png"); } } @@ -1002,24 +910,15 @@ public static Texture2D EmptyStateInit { get { - if (emptyStateInit == null) - { - emptyStateInit = Utility.GetIcon("empty-state-init.png", "empty-state-init@2x.png"); - } - return emptyStateInit; + return Utility.GetIcon("empty-state-init.png", "empty-state-init@2x.png"); } - } public static Texture2D DropdownListIcon { get { - if (dropdownListIcon == null) - { - dropdownListIcon = Utility.GetIcon("dropdown-list-icon.png", "dropdown-list-icon@2x.png"); - } - return dropdownListIcon; + return Utility.GetIcon("dropdown-list-icon.png", "dropdown-list-icon@2x.png"); } } @@ -1027,11 +926,7 @@ public static Texture2D GlobeIcon { get { - if (globeIcon == null) - { - globeIcon = Utility.GetIcon("globe.png", "globe@2x.png"); - } - return globeIcon; + return Utility.GetIcon("globe.png", "globe@2x.png"); } } @@ -1039,11 +934,7 @@ public static Texture2D SpinnerInside { get { - if (spinnerInside == null) - { - spinnerInside = Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png"); - } - return spinnerInside; + return Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png"); } } @@ -1051,11 +942,7 @@ public static Texture2D SpinnerOutside { get { - if (spinnerOutside == null) - { - spinnerOutside = Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png"); - } - return spinnerOutside; + return Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png"); } } @@ -1063,11 +950,7 @@ public static Texture2D Code { get { - if (code == null) - { - code = Utility.GetIcon("code.png", "code@2x.png"); - } - return code; + return Utility.GetIcon("code.png", "code@2x.png"); } } @@ -1075,11 +958,7 @@ public static Texture2D Rocket { get { - if (rocket == null) - { - rocket = Utility.GetIcon("rocket.png", "rocket@2x.png"); - } - return rocket; + return Utility.GetIcon("rocket.png", "rocket@2x.png"); } } @@ -1087,11 +966,7 @@ public static Texture2D Merge { get { - if (merge == null) - { - merge = Utility.GetIcon("merge.png", "merge@2x.png"); - } - return merge; + return Utility.GetIcon("merge.png", "merge@2x.png"); } } @@ -1099,12 +974,7 @@ public static Texture2D SpinnerInsideInverted { get { - if (spinnerInsideInverted == null) - { - spinnerInsideInverted = Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png"); - spinnerInsideInverted.InvertColors(); - } - return spinnerInsideInverted; + return Utility.GetIcon("spinner-inside.png", "spinner-inside@2x.png", true); } } @@ -1112,12 +982,7 @@ public static Texture2D SpinnerOutsideInverted { get { - if (spinnerOutsideInverted == null) - { - spinnerOutsideInverted = Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png"); - spinnerOutsideInverted.InvertColors(); - } - return spinnerOutsideInverted; + return Utility.GetIcon("spinner-outside.png", "spinner-outside@2x.png", true); } } @@ -1125,12 +990,7 @@ public static Texture2D CodeInverted { get { - if (codeInverted == null) - { - codeInverted = Utility.GetIcon("code.png", "code@2x.png"); - codeInverted.InvertColors(); - } - return codeInverted; + return Utility.GetIcon("code.png", "code@2x.png", true); } } @@ -1138,12 +998,7 @@ public static Texture2D RocketInverted { get { - if (rocketInverted == null) - { - rocketInverted = Utility.GetIcon("rocket.png", "rocket@2x.png"); - rocketInverted.InvertColors(); - } - return rocketInverted; + return Utility.GetIcon("rocket.png", "rocket@2x.png", true); } } @@ -1151,12 +1006,7 @@ public static Texture2D MergeInverted { get { - if (mergeInverted == null) - { - mergeInverted = Utility.GetIcon("merge.png", "merge@2x.png"); - mergeInverted.InvertColors(); - } - return mergeInverted; + return Utility.GetIcon("merge.png", "merge@2x.png", true); } } private static GUIStyle foldout; diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs index c11549630..990ba7061 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Utility.cs @@ -5,18 +5,35 @@ using System.Reflection; using UnityEditor; using UnityEngine; +using System.Collections.Generic; namespace GitHub.Unity { class Utility : ScriptableObject { - public static Texture2D GetIcon(string filename, string filename2x = "") + private static Dictionary iconCache = new Dictionary(); + + public static bool IsDarkTheme { + get { + var defaultTextColor = Styles.Label.normal.textColor; + return defaultTextColor.r > 0.5f && defaultTextColor.g > 0.5f && defaultTextColor.b > 0.5f; + } + } + + public static Texture2D GetIcon(string filename, string filename2x = "", bool invertColors = false) { if (EditorGUIUtility.pixelsPerPoint > 1f && !string.IsNullOrEmpty(filename2x)) { filename = filename2x; } + var key = invertColors ? "dark_" + filename : "light_" + filename; + + if (iconCache.ContainsKey(key)) + { + return iconCache[key]; + } + Texture2D texture2D = null; var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("GitHub.Unity.IconsAndLogos." + filename); @@ -33,6 +50,10 @@ public static Texture2D GetIcon(string filename, string filename2x = "") if (texture2D != null) { texture2D.hideFlags = HideFlags.HideAndDontSave; + if (invertColors) { + texture2D.InvertColors(); + } + iconCache.Add(key, texture2D); } return texture2D; From 1bd5a075f21d91c3e757e2a54e28c847d1603cd0 Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 19 Nov 2018 15:41:23 +0100 Subject: [PATCH 477/567] Fix icon colors on the dark theme --- .../Assets/Editor/GitHub.Unity/Misc/Styles.cs | 22 +++++++++---------- .../Assets/Editor/GitHub.Unity/UI/Window.cs | 9 ++++++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs index 99974d215..5a72b41ad 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/Misc/Styles.cs @@ -806,7 +806,7 @@ public static Texture2D ActiveBranchIcon { get { - return Utility.GetIcon("current-branch-indicator.png", "current-branch-indicator@2x.png"); + return Utility.GetIcon("current-branch-indicator.png", "current-branch-indicator@2x.png", Utility.IsDarkTheme); } } @@ -846,7 +846,7 @@ public static Texture2D SmallLogo { get { - return Utility.GetIcon("small-logo.png"); + return Utility.IsDarkTheme ? Utility.GetIcon("small-logo-light.png", "small-logo-light@2x.png") : Utility.GetIcon("small-logo.png", "small-logo@2x.png"); } } @@ -854,7 +854,7 @@ public static Texture2D BigLogo { get { - return Utility.IsDarkTheme ? Utility.GetIcon("big-logo-light.png") : Utility.GetIcon("big-logo.png"); + return Utility.IsDarkTheme ? Utility.GetIcon("big-logo-light.png", "big-logo-light@2x.png") : Utility.GetIcon("big-logo.png", "big-logo@2x.png"); } } @@ -870,7 +870,7 @@ public static Texture2D DotIcon { get { - return Utility.GetIcon("dot.png", "dot@2x.png"); + return Utility.GetIcon("dot.png", "dot@2x.png", Utility.IsDarkTheme); } } @@ -878,7 +878,7 @@ public static Texture2D LocalCommitIcon { get { - return Utility.GetIcon("local-commit-icon.png", "local-commit-icon@2x.png"); + return Utility.GetIcon("local-commit-icon.png", "local-commit-icon@2x.png", Utility.IsDarkTheme); } } @@ -894,7 +894,7 @@ public static Texture2D RepoIcon { get { - return Utility.GetIcon("repo.png", "repo@2x.png"); + return Utility.GetIcon("repo.png", "repo@2x.png", Utility.IsDarkTheme); } } @@ -908,10 +908,10 @@ public static Texture2D LockIcon public static Texture2D EmptyStateInit { - get - { - return Utility.GetIcon("empty-state-init.png", "empty-state-init@2x.png"); - } + get + { + return Utility.GetIcon("empty-state-init.png", "empty-state-init@2x.png"); + } } public static Texture2D DropdownListIcon @@ -926,7 +926,7 @@ public static Texture2D GlobeIcon { get { - return Utility.GetIcon("globe.png", "globe@2x.png"); + return Utility.GetIcon("globe.png", "globe@2x.png", Utility.IsDarkTheme); } } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs index 026bdd822..6d8f21702 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/Window.cs @@ -16,6 +16,7 @@ class Window : BaseWindow [NonSerialized] private Spinner spinner; [NonSerialized] private IProgress repositoryProgress; [NonSerialized] private IProgress appManagerProgress; + [NonSerialized] private bool firstOnGUI = true; [SerializeField] private double progressMessageClearTime = -1; [SerializeField] private double notificationClearTime = -1; @@ -112,8 +113,6 @@ public override void Initialize(IApplicationManager applicationManager) LocksView.InitializeView(this); InitProjectView.InitializeView(this); - titleContent = new GUIContent(Title, Styles.SmallLogo); - if (!HasRepository) { changeTab = activeTab = SubTab.InitProject; @@ -213,6 +212,12 @@ private void ValidateCachedData(IRepository repository) private void MaybeUpdateData() { + if (firstOnGUI) + { + titleContent = new GUIContent(Title, Styles.SmallLogo); + } + firstOnGUI = false; + UriString host = null; if (!HasRepository || String.IsNullOrEmpty(Repository.CloneUrl)) { From af45a791e0220d00a584efa872c9b61794cc25de Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Wed, 7 Nov 2018 15:13:14 +0100 Subject: [PATCH 478/567] Report the correct editing status to Unity. Fixes #934 There's two issues going on here - The `loggedInUser` field wasn't getting seeded with user information on load, so checking whether the current user owns a lock would always fail until the connections changed event got triggered somehow - `GetLock` didn't account for not having user information, so if the user was logged out, it would default to not allowing any edits. Since the info wasn't getting seeded, it would always report files as being locked by someone else. It now defaults to allowing edits if the user isn't signed in. Now, this isn't a bug, but if the user is not signed in, all bets are kinda off on validating locks. We should probably warn the user that we can't check whether they own the lock, but we should probably not lock down Unity entirely because of it. --- .../UI/LfsLocksModificationProcessor.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs index a4bf74cb9..7379f41bb 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LfsLocksModificationProcessor.cs @@ -21,6 +21,8 @@ public static void Initialize(IEnvironment env, IPlatform plat) environment = env; platform = plat; platform.Keychain.ConnectionsChanged += UserMayHaveChanged; + // we need to do this to get the initial user information up front + UserMayHaveChanged(); repository = environment.Repository; if (repository != null) @@ -37,19 +39,21 @@ public static string[] OnWillSaveAssets(string[] paths) public static AssetMoveResult OnWillMoveAsset(string oldPath, string newPath) { - return IsLocked(oldPath) || IsLocked(newPath) ? AssetMoveResult.FailedMove : AssetMoveResult.DidNotMove; + return IsLockedBySomeoneElse(oldPath) || IsLockedBySomeoneElse(newPath) ? AssetMoveResult.FailedMove : AssetMoveResult.DidNotMove; } public static AssetDeleteResult OnWillDeleteAsset(string assetPath, RemoveAssetOptions option) { - return IsLocked(assetPath) ? AssetDeleteResult.FailedDelete : AssetDeleteResult.DidNotDelete; + return IsLockedBySomeoneElse(assetPath) ? AssetDeleteResult.FailedDelete : AssetDeleteResult.DidNotDelete; } + // Returns true if this file can be edited by this user public static bool IsOpenForEdit(string assetPath, out string message) { var lck = GetLock(assetPath); - message = lck.HasValue ? "File is locked for editing by " + lck.Value.Owner : null; - return !lck.HasValue; + var canEdit = !IsLockedBySomeoneElse(lck); + message = !canEdit ? "File is locked for editing by " + lck.Value.Owner : null; + return canEdit; } private static void RepositoryOnLocksChanged(CacheUpdateEvent cacheUpdateEvent) @@ -66,9 +70,14 @@ private static void UserMayHaveChanged() loggedInUser = platform.Keychain.Connections.Select(x => x.Username).FirstOrDefault(); } - private static bool IsLocked(string assetPath) + private static bool IsLockedBySomeoneElse(GitLock? lck) { - return GetLock(assetPath).HasValue; + return lck.HasValue && !lck.Value.Owner.Name.Equals(loggedInUser); + } + + private static bool IsLockedBySomeoneElse(string assetPath) + { + return IsLockedBySomeoneElse(GetLock(assetPath)); } private static GitLock? GetLock(string assetPath) @@ -78,9 +87,9 @@ private static bool IsLocked(string assetPath) GitLock lck; var repositoryPath = environment.GetRepositoryPath(assetPath.ToNPath()); - if (!locks.TryGetValue(repositoryPath, out lck) || lck.Owner.Name.Equals(loggedInUser)) - return null; - return lck; + if (locks.TryGetValue(repositoryPath, out lck)) + return lck; + return null; } } } From b9b2751a8fedd96a87da6036ee7b79321314979e Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Mon, 19 Nov 2018 15:44:58 +0100 Subject: [PATCH 479/567] Avoid throwing when the first ongui pass is not a layout pass --- src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs index 68208cc74..0092d94f2 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/UI/LocksView.cs @@ -119,7 +119,7 @@ public bool Render(Rect containingRect, Action singleClick = null, visibleItems[entry.GitLock.ID] = shouldRenderEntry; } - if (visibleItems[entry.GitLock.ID]) + if (visibleItems.ContainsKey(entry.GitLock.ID) && visibleItems[entry.GitLock.ID]) { entryRect = RenderEntry(entryRect, entry); } From 84c543f7a47896228d6c9d5e98b546df90f2751f Mon Sep 17 00:00:00 2001 From: Andreia Gaita Date: Fri, 16 Nov 2018 21:41:47 +0100 Subject: [PATCH 480/567] Show a message in the inspector when a file is locked. --- GitHub.Unity.sln | 10 +++ .../ExtensionLoader/ExtensionLoader.asmdef | 2 +- .../ExtensionLoader/ExtensionLoader.cs | 5 +- .../ExtensionLoader/ExtensionLoader.csproj | 5 ++ .../ExtensionLoader/UnityAPIWrapper.cs | 20 +++++ .../GitHub.Unity/GitHub.Unity.45.csproj | 4 + .../Editor/GitHub.Unity/GitHub.Unity.asmdef | 7 +- .../Editor/GitHub.Unity/GitHub.Unity.csproj | 4 + .../UI/LfsLocksModificationProcessor.cs | 46 +++++++++++- .../GitHub.Unity/UI/ProjectWindowInterface.cs | 5 +- src/UnityShim/UnityShim.cs | 14 ++++ src/UnityShim/UnityShim.csproj | 74 +++++++++++++++++++ .../CopyLibrariesToPackageProject.csproj | 4 + 13 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/UnityAPIWrapper.cs create mode 100644 src/UnityShim/UnityShim.cs create mode 100644 src/UnityShim/UnityShim.csproj diff --git a/GitHub.Unity.sln b/GitHub.Unity.sln index c9da69500..0707a70fa 100644 --- a/GitHub.Unity.sln +++ b/GitHub.Unity.sln @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityTests", "src\UnityExte EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtensionLoader", "src\UnityExtension\Assets\Editor\GitHub.Unity\ExtensionLoader\ExtensionLoader.csproj", "{6B0EAB30-511A-44C1-87FE-D9AB7E34D115}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityShim", "src\UnityShim\UnityShim.csproj", "{F94F8AE1-C171-4A83-89E8-6557CA91A188}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,6 +163,14 @@ Global {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.dev|Any CPU.Build.0 = dev|Any CPU {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B0EAB30-511A-44C1-87FE-D9AB7E34D115}.Release|Any CPU.Build.0 = Release|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.DebugNoUnity|Any CPU.ActiveCfg = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.DebugNoUnity|Any CPU.Build.0 = Debug|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.dev|Any CPU.ActiveCfg = dev|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.dev|Any CPU.Build.0 = dev|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F94F8AE1-C171-4A83-89E8-6557CA91A188}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef index c400f84eb..a5ed02083 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.asmdef @@ -1,6 +1,6 @@ { "name": "ExtensionLoader", - "references": [], + "references": ["../../build/GitHub.UnityShim.dll"], "includePlatforms": [ "Editor" ], diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs index a04601fa9..f3a7e9eef 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.cs @@ -30,9 +30,12 @@ public bool Initialized private static string[] assemblies20 = { "System.Threading.dll", "AsyncBridge.Net35.dll", "ReadOnlyCollectionsInterfaces.dll", "GitHub.Api.dll", "GitHub.Unity.dll" }; private static string[] assemblies45 = { "GitHub.Api.45.dll", "GitHub.Unity.45.dll" }; + private const string GITHUB_UNITY_DISABLE = "GITHUB_UNITY_DISABLE"; + private static bool IsDisabled { get { return Environment.GetEnvironmentVariable(GITHUB_UNITY_DISABLE) == "1"; } } + static ExtensionLoader() { - if (Environment.GetEnvironmentVariable("GITHUB_UNITY_DISABLE") == "1") + if (IsDisabled) { return; } diff --git a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj index 50ebe9826..29dd7e770 100644 --- a/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj +++ b/src/UnityExtension/Assets/Editor/GitHub.Unity/ExtensionLoader/ExtensionLoader.csproj @@ -54,6 +54,10 @@ + + {F94F8AE1-C171-4A83-89E8-6557CA91A188} + UnityShim + $(UnityDir)Managed\UnityEditor.dll False @@ -65,6 +69,7 @@ +