Implement VSPackage with basic login dialog

Includes a menu that launches a login dialog
for testing purposes.
This commit is contained in:
Haacked 2014-12-15 15:48:06 -08:00
Родитель 2f00f72032
Коммит 7034a030ac
118 изменённых файлов: 6925 добавлений и 133 удалений

6
.gitmodules поставляемый Normal file
Просмотреть файл

@ -0,0 +1,6 @@
[submodule "Rothko"]
path = Rothko
url = https://github.com/Haacked/Rothko.git
[submodule "submodules/rothko"]
path = submodules/rothko
url = https://github.com/Haacked/Rothko.git

Просмотреть файл

@ -3,39 +3,90 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.31101.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.VisualStudio", "src\GitHub.VisualStudio\GitHub.VisualStudio.csproj", "{11569514-5AE5-4B5B-92A2-F10B0967DE5F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6A6B0910-ECED-4FD6-8563-359FFDC23373}"
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
build\CodeAnalysisDictionary.xml = build\CodeAnalysisDictionary.xml
build\GitHubVS.ruleset = build\GitHubVS.ruleset
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{880CC82E-4419-45F7-8C32-4687D0E1A175}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{72036B62-2FA6-4A22-8B33-69F698A18CF1}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{F8947717-5582-48D1-BDFA-3CFD5BC3C31B}"
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
build\Bootstrap.ps1 = build\Bootstrap.ps1
build.cmd = build.cmd
build\cibuild.ps1 = build\cibuild.ps1
build\SolutionInfo.cs = build\SolutionInfo.cs
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "src\UnitTests\UnitTests.csproj", "{596595A6-2A3C-469E-9386-9E3767D863A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\GitHub.UI\GitHub.UI.csproj", "{346384DD-2445-4A28-AF22-B45F3957BD89}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Reactive", "src\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj", "{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Extensions", "src\GitHub.Extensions\GitHub.Extensions.csproj", "{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Extensions.Reactive", "src\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj", "{6559E128-8B40-49A5-85A8-05565ED0C7E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.App", "src\GitHub.App\GitHub.App.csproj", "{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Submodules", "Submodules", "{1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rothko", "submodules\Rothko\src\Rothko.csproj", "{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{8E1F1B4E-AEA2-4AB1-8F73-423A903550A1}"
ProjectSection(SolutionItems) = preProject
build\Modules\BuildUtils.psm1 = build\Modules\BuildUtils.psm1
build\Modules\Debugging.psm1 = build\Modules\Debugging.psm1
build\Modules\Vsix.psm1 = build\Modules\Vsix.psm1
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}.Release|Any CPU.Build.0 = Release|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11569514-5AE5-4B5B-92A2-F10B0967DE5F}.Release|Any CPU.Build.0 = Release|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{596595A6-2A3C-469E-9386-9E3767D863A5}.Release|Any CPU.Build.0 = Release|Any CPU
{346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{346384DD-2445-4A28-AF22-B45F3957BD89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{346384DD-2445-4A28-AF22-B45F3957BD89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{346384DD-2445-4A28-AF22-B45F3957BD89}.Release|Any CPU.Build.0 = Release|Any CPU
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}.Release|Any CPU.Build.0 = Release|Any CPU
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}.Release|Any CPU.Build.0 = Release|Any CPU
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6559E128-8B40-49A5-85A8-05565ED0C7E3}.Release|Any CPU.Build.0 = Release|Any CPU
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}.Release|Any CPU.Build.0 = Release|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4A84E568-CA86-4510-8CD0-90D3EF9B65F9} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}
{8E1F1B4E-AEA2-4AB1-8F73-423A903550A1} = {F8947717-5582-48D1-BDFA-3CFD5BC3C31B}
EndGlobalSection
EndGlobal

1
build.cmd Normal file
Просмотреть файл

@ -0,0 +1 @@
powershell.exe .\build\cibuild.ps1

1
nunit-UnitTests.xml Normal file
Просмотреть файл

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><test-results name="C:\dev\VisualStudio\src\UnitTests\bin\Release\UnitTests.dll" date="2014-12-15" time="15:43:47" total="4" failures="0" not-run="0"><test-suite name="C:\dev\VisualStudio\src\UnitTests\bin\Release\UnitTests.dll" success="True" time="0.697"><results><test-suite name="TodoTaggerTests+TheGetTagsMethod" success="True" time="0.697"><results><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: &quot;// TODO: foo bar&quot;)" executed="True" success="True" time="0.683" /><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: &quot;//TODO foo bar&quot;)" executed="True" success="True" time="0.003" /><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: &quot;// TODO foo bar&quot;)" executed="True" success="True" time="0.003" /><test-case name="TodoTaggerTests+TheGetTagsMethod.ParsesTodoTags(spanText: &quot;//TODO: foo bar&quot;)" executed="True" success="True" time="0.008" /></results></test-suite></results></test-suite></test-results>

Просмотреть файл

@ -0,0 +1,22 @@
namespace GitHub.Authentication
{
public enum AuthenticationResult
{
CredentialFailure,
VerificationFailure,
Success
}
public static class AuthenticationResultExtensions
{
public static bool IsFailure(this AuthenticationResult result)
{
return result != AuthenticationResult.Success;
}
public static bool IsSuccess(this AuthenticationResult result)
{
return result == AuthenticationResult.Success;
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
using GitHub.Models;
using Octokit;
using ReactiveUI;
namespace GitHub
{
public interface IAccount : IReactiveObject
{
string Email { get; }
int Id { get; }
bool IsEnterprise { get; }
bool IsGitHub { get; }
bool IsLocal { get; }
bool IsOnFreePlan { get; }
bool HasMaximumPrivateRepositories { get; }
bool IsUser { get; }
/// <summary>
/// True if the user is an admin on the host (GitHub or Enterprise).
/// </summary>
/// <remarks>
/// Do not confuse this with "IsStaff". This is true if the user is an admin
/// on the site. IsStaff is true if that site is github.com.
/// </remarks>
bool IsSiteAdmin { get; }
/// <summary>
/// Returns true if the user is a member of the GitHub staff.
/// </summary>
bool IsGitHubStaff { get; }
IRepositoryHost Host { get; }
string Login { get; }
string Name { get; }
int OwnedPrivateRepos { get; }
long PrivateReposInPlan { get; }
void Update(User ghUser);
void Update(Organization org);
}
}

Просмотреть файл

@ -0,0 +1,10 @@
using System;
using Octokit;
namespace GitHub.Authentication
{
public interface ITwoFactorChallengeHandler
{
IObservable<TwoFactorChallengeResult> HandleTwoFactorException(TwoFactorRequiredException exception);
}
}

Просмотреть файл

@ -0,0 +1,44 @@
using System;
using System.ComponentModel.Composition;
using System.Reactive.Linq;
using GitHub.ViewModels;
using Microsoft.VisualStudio.PlatformUI;
using Octokit;
using ReactiveUI;
namespace GitHub.Authentication
{
[Export(typeof(ITwoFactorChallengeHandler))]
public class TwoFactorChallengeHandler : ITwoFactorChallengeHandler
{
readonly IServiceProvider serviceProvider;
[ImportingConstructor]
public TwoFactorChallengeHandler(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public IObservable<TwoFactorChallengeResult> HandleTwoFactorException(TwoFactorRequiredException exception)
{
var twoFactorDialog = (TwoFactorDialogViewModel)serviceProvider.GetService(typeof(TwoFactorDialogViewModel));
var twoFactorView = (IViewFor<TwoFactorDialogViewModel>)serviceProvider.GetService(typeof(IViewFor<TwoFactorDialogViewModel>));
return Observable.Start(() =>
{
twoFactorView.ViewModel = twoFactorDialog;
((DialogWindow)twoFactorView).Show();
var userError = new TwoFactorRequiredUserError(exception);
return twoFactorDialog.Show(userError)
.SelectMany(x =>
x == RecoveryOptionResult.RetryOperation
? Observable.Return(userError.ChallengeResult)
: Observable.Throw<TwoFactorChallengeResult>(exception));
}, RxApp.MainThreadScheduler)
.SelectMany(x => x)
.Finally(() =>
((DialogWindow)twoFactorView).Hide());
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using System;
using NullGuard;
using Octokit;
using ReactiveUI;
namespace GitHub.Authentication
{
public class TwoFactorRequiredUserError : UserError
{
public TwoFactorRequiredUserError(TwoFactorRequiredException exception)
: base(exception.Message, innerException: exception)
{
TwoFactorType = exception.TwoFactorType;
}
public TwoFactorType TwoFactorType { get; private set; }
[AllowNull]
public TwoFactorChallengeResult ChallengeResult
{
[return: AllowNull]
get;
set;
}
public IObservable<RecoveryOptionResult> Throw()
{
return Throw(this);
}
}
}

Просмотреть файл

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Linq;
using Akavache;
using GitHub.Extensions;
using GitHub.Extensions.Reactive;
using Octokit;
namespace GitHub
{
public class HostCache : IHostCache
{
readonly IBlobCache localMachineCache;
readonly IBlobCache userAccountCache;
public HostCache(IBlobCache localMachineCache, IBlobCache userAccountCache)
{
this.localMachineCache = localMachineCache;
this.userAccountCache = userAccountCache;
}
public IObservable<User> GetUser()
{
return userAccountCache.GetObject<User>("user");
}
public IObservable<Unit> InsertUser(User user)
{
return userAccountCache.InsertObject("user", user);
}
public IObservable<IEnumerable<Organization>> GetAllOrganizations()
{
// NOTE: Akavache with the SQLite storage stores the type name with the cached objects
// to support the GetAllObjects method call. This is the only usage of that method
// which is why we have to keep GitHubOrganization around.
return userAccountCache.GetAllObjects<Organization>();
}
[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
Justification = "We store the user differently")]
public IObservable<Unit> InsertOrganization(Organization organization)
{
return userAccountCache.InsertObject(organization.Login, organization);
}
public IObservable<Unit> InvalidateOrganization(Organization organization)
{
return InvalidateOrganization(organization.Login);
}
public IObservable<Unit> InvalidateOrganization(IAccount organization)
{
return InvalidateOrganization(organization.Login);
}
IObservable<Unit> InvalidateOrganization(string login)
{
Guard.ArgumentNotEmptyString(login, "login");
return userAccountCache.InvalidateObject<Organization>(login);
}
public IObservable<Unit> InvalidateAll()
{
return Observable.Merge
(
localMachineCache.InvalidateAll(),
userAccountCache.InvalidateAll(),
Invalidate()
).AsCompletion();
}
protected virtual IObservable<Unit> Invalidate()
{
return Observable.Return(Unit.Default);
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
localMachineCache.Dispose();
localMachineCache.Shutdown.Wait();
userAccountCache.Dispose();
userAccountCache.Shutdown.Wait();
}
}
}
}

Просмотреть файл

@ -0,0 +1,46 @@
using System;
using System.ComponentModel.Composition;
using System.IO;
using Rothko;
namespace GitHub
{
[Export(typeof(IHostCacheFactory))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class HostCacheFactory : IHostCacheFactory
{
readonly Lazy<IBlobCacheFactory> blobCacheFactory;
readonly Lazy<IOperatingSystemFacade> operatingSystem;
[ImportingConstructor]
public HostCacheFactory(Lazy<IBlobCacheFactory> blobCacheFactory, Lazy<IOperatingSystemFacade> operatingSystem)
{
this.blobCacheFactory = blobCacheFactory;
this.operatingSystem = operatingSystem;
}
public IHostCache Create(HostAddress hostAddress)
{
var environment = OperatingSystem.Environment;
// For GitHub.com, the cache file name should be "api.github.com.cache.db"
// This is why we use ApiUrl and not CredentialCacheHostKey
string host = hostAddress.ApiUri.Host;
string cacheFileName = host + ".cache.db";
var localMachinePath = Path.Combine(environment.GetLocalGitHubApplicationDataPath(), cacheFileName);
var userAccountPath = Path.Combine(environment.GetApplicationDataPath(), cacheFileName);
// CreateDirectory is a noop if the directory already exists.
new[] { localMachinePath, userAccountPath }
.ForEach(x => OperatingSystem.Directory.CreateDirectory(Path.GetDirectoryName(x)));
var localMachineCache = BlobCacheFactory.CreateBlobCache(localMachinePath);
var userAccountCache = BlobCacheFactory.CreateBlobCache(userAccountPath);
return new HostCache(localMachineCache, userAccountCache);
}
IOperatingSystemFacade OperatingSystem { get { return operatingSystem.Value; } }
IBlobCacheFactory BlobCacheFactory { get { return blobCacheFactory.Value; } }
}
}

Просмотреть файл

@ -0,0 +1,9 @@
using Akavache;
namespace GitHub
{
public interface IBlobCacheFactory
{
IBlobCache CreateBlobCache(string path);
}
}

Просмотреть файл

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using Octokit;
namespace GitHub
{
/// <summary>
/// Per host cache data.
/// </summary>
public interface IHostCache : IDisposable
{
IObservable<User> GetUser();
IObservable<Unit> InsertUser(User user);
IObservable<IEnumerable<Organization>> GetAllOrganizations();
IObservable<Unit> InsertOrganization(Organization organization);
IObservable<Unit> InvalidateOrganization(Organization organization);
IObservable<Unit> InvalidateOrganization(IAccount organization);
IObservable<Unit> InvalidateAll();
}
}

Просмотреть файл

@ -0,0 +1,9 @@
using System;
namespace GitHub
{
public interface IHostCacheFactory
{
IHostCache Create(HostAddress hostAddress);
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using System;
using System.Reactive;
using Akavache;
namespace GitHub
{
public interface ILoginCache : IDisposable
{
IObservable<LoginInfo> GetLoginAsync(HostAddress hostAddress);
IObservable<Unit> SaveLogin(string user, string password, HostAddress hostAddress);
IObservable<Unit> EraseLogin(HostAddress hostAddress);
IObservable<Unit> Flush();
}
}

Просмотреть файл

@ -0,0 +1,65 @@
using System;
using System.Linq.Expressions;
using System.Reactive;
using Akavache;
namespace GitHub
{
/// <summary>
/// A cache for data that's not host specific
/// </summary>
public interface ISharedCache : IDisposable
{
IBlobCache UserAccount { get; }
IBlobCache LocalMachine { get; }
ISecureBlobCache Secure { get; }
/// <summary>
/// Retrieves the Enterpise Host Uri from cache if present.
/// </summary>
/// <returns></returns>
IObservable<Uri> GetEnterpriseHostUri();
/// <summary>
/// Inserts the Enterprise Host Uri.
/// </summary>
/// <returns></returns>
IObservable<Unit> InsertEnterpriseHostUri(Uri enterpriseHostUri);
/// <summary>
/// Removes the Enterprise Host Uri from the cache.
/// </summary>
/// <returns></returns>
IObservable<Unit> InvalidateEnterpriseHostUri();
/// <summary>
/// Retrieves whether staff mode is enabled.
/// </summary>
/// <returns></returns>
IObservable<bool> GetStaffMode();
/// <summary>
/// Helper method used to bind a property of a view model to a value in the cache.
/// </summary>
IDisposable BindPropertyToCache<TSource, TProperty>(TSource source,
Expression<Func<TSource, TProperty>> property,
TProperty defaultValue);
/// <summary>
/// Helper method used to bind a property of a view model to a value in the cache with a mapping function to
/// and from the cache.
/// </summary>
IDisposable BindPropertyToCache<TSource, TProperty, TCacheValue>(TSource source,
Expression<Func<TSource, TProperty>> property,
Func<TProperty, TCacheValue> mapToCache,
Func<TCacheValue, TProperty> mapFromCache,
TCacheValue defaultValue);
/// <summary>
/// Sets whether staff mode is enabled;
/// </summary>
/// <param name="staffMode"></param>
/// <returns></returns>
IObservable<Unit> SetStaffMode(bool staffMode);
}
}

Просмотреть файл

@ -0,0 +1,63 @@
using System;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Reactive;
using System.Reactive.Linq;
using Akavache;
using GitHub.Extensions;
using NLog;
namespace GitHub
{
[Export(typeof(ILoginCache))]
public sealed class LoginCache : ILoginCache
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
readonly ISharedCache cache;
static readonly LoginInfo empty = new LoginInfo("", "");
[ImportingConstructor]
public LoginCache(ISharedCache cache)
{
this.cache = cache;
}
public static LoginInfo EmptyLoginInfo
{
get { return empty; }
}
public IObservable<LoginInfo> GetLoginAsync(HostAddress hostAddress)
{
return cache.Secure.GetLoginAsync(hostAddress.CredentialCacheKeyHost).Catch(Observable.Return(empty));
}
public IObservable<Unit> SaveLogin(string user, string password, HostAddress hostAddress)
{
Guard.ArgumentNotEmptyString(user, "user");
Guard.ArgumentNotEmptyString(password, "password");
return cache.Secure.SaveLogin(user, password, hostAddress.CredentialCacheKeyHost);
}
public IObservable<Unit> EraseLogin(HostAddress hostAddress)
{
log.Info(CultureInfo.CurrentCulture, "Erasing the git credential cache for host '{0}'",
hostAddress.CredentialCacheKeyHost);
return cache.Secure.EraseLogin(hostAddress.CredentialCacheKeyHost);
}
public IObservable<Unit> Flush()
{
log.Info("Flushing the login cache");
return cache.Secure.Flush();
}
public void Dispose()
{
cache.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,138 @@
using System;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using Akavache;
using ReactiveUI;
namespace GitHub
{
/// <summary>
/// A cache for data that's not host specific
/// </summary>
[Export(typeof(ISharedCache))]
public class SharedCache : ISharedCache
{
const string enterpriseHostApiBaseUriCacheKey = "enterprise-host-api-base-uri";
const string staffModeKey = "__StaffOnlySettings__:IsStaffMode";
// TODO Use this instead.
//public SharedCache()
// : this(BlobCache.UserAccount, BlobCache.LocalMachine, BlobCache.Secure)
//{
//}
public SharedCache() : this(new InMemoryBlobCache(), new InMemoryBlobCache(), new InMemoryBlobCache())
{
}
protected SharedCache(IBlobCache userAccountCache, IBlobCache localMachineCache, ISecureBlobCache secureCache)
{
UserAccount = userAccountCache;
LocalMachine = localMachineCache;
Secure = secureCache;
}
public IBlobCache UserAccount { get; private set; }
public IBlobCache LocalMachine { get; private set; }
public ISecureBlobCache Secure { get; private set; }
public IObservable<Uri> GetEnterpriseHostUri()
{
return UserAccount.GetObject<Uri>(enterpriseHostApiBaseUriCacheKey);
}
public IObservable<Unit> InsertEnterpriseHostUri(Uri enterpriseHostUri)
{
return UserAccount.InsertObject(enterpriseHostApiBaseUriCacheKey, enterpriseHostUri);
}
public IObservable<Unit> InvalidateEnterpriseHostUri()
{
return UserAccount.InvalidateObject<Uri>(enterpriseHostApiBaseUriCacheKey);
}
public IObservable<bool> GetStaffMode()
{
return UserAccount.GetOrFetchObject(staffModeKey, () => Observable.Return(false));
}
public IObservable<Unit> SetStaffMode(bool staffMode)
{
return UserAccount.InsertObject(staffModeKey, staffMode);
}
public IDisposable BindPropertyToCache<TSource, TProperty>(TSource source,
Expression<Func<TSource, TProperty>> property,
TProperty defaultValue)
{
return BindPropertyToCache(source, property, x => x, x => x, defaultValue);
}
public IDisposable BindPropertyToCache<TSource, TProperty, TCacheValue>(TSource source,
Expression<Func<TSource, TProperty>> property,
Func<TProperty, TCacheValue> mapToCache,
Func<TCacheValue, TProperty> mapFromCache,
TCacheValue defaultValue)
{
var propertyInfo = GetPropertyNameAndSetAction(property, source);
var propertySetter = propertyInfo.Item2;
propertySetter(mapFromCache(defaultValue));
var restoreCachedValue = new Action<TCacheValue>(value => propertySetter(mapFromCache(value)));
// TODO: We may want a way to preserve the cache key should we ever rename the property or type.
// We could look for specific attributes to do this. But for now, let's not worry about it.
string cacheKey = "Application:" + typeof(TSource).Name + ":" + propertyInfo.Item1;
var propertyObservable = source.WhenAny(property, prop => prop.Value).Select(mapToCache);
return LocalMachine.GetObject<TCacheValue>(cacheKey)
.Catch(Observable.Return(defaultValue))
.SingleAsync()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(cachedValue =>
{
restoreCachedValue(cachedValue);
propertyObservable
.Throttle(TimeSpan.FromSeconds(2), RxApp.TaskpoolScheduler)
.SelectMany(width => LocalMachine.InsertObject(cacheKey, width))
.Subscribe();
});
}
static Tuple<string, Action<TProperty>> GetPropertyNameAndSetAction<TSource, TProperty>(
Expression<Func<TSource, TProperty>> expression,
TSource source)
{
var member = expression.Body as MemberExpression;
Debug.Assert(member != null, "Expression should be a property and not method or some other shit.");
var property = member.Member as PropertyInfo;
Debug.Assert(property != null, "Expression should be a property and not field or some other shit.");
var propertySetterAction = new Action<TProperty>(value => property.SetValue(source, value));
return Tuple.Create(property.Name, propertySetterAction);
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
UserAccount.Dispose();
UserAccount.Shutdown.Wait();
LocalMachine.Dispose();
LocalMachine.Shutdown.Wait();
Secure.Dispose();
Secure.Shutdown.Wait();
}
}
}
}

Просмотреть файл

@ -0,0 +1,20 @@
using System.ComponentModel.Composition;
using Akavache;
using Akavache.Sqlite3;
using GitHub.Extensions;
namespace GitHub
{
[Export(typeof(IBlobCacheFactory))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class SqlitePersistentBlobCacheFactory : IBlobCacheFactory
{
public IBlobCache CreateBlobCache(string path)
{
Guard.ArgumentNotEmptyString(path, "path");
return new InMemoryBlobCache();
//return new SQLitePersistentBlobCache(path);
}
}
}

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<NullGuard />
</Weavers>

Просмотреть файл

@ -0,0 +1,192 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1A1DA411-8D1F-4578-80A6-04576BEA2DC5}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GitHub</RootNamespace>
<AssemblyName>GitHub.App</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>ee7f8637</NuGetPackageImportStamp>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="Akavache">
<HintPath>..\..\packages\akavache.core.4.0.3.2\lib\net45\Akavache.dll</HintPath>
</Reference>
<Reference Include="Akavache.Sqlite3">
<HintPath>..\..\packages\akavache.sqlite3.4.0.3.2\lib\Portable-Net45+Win8+WP8+Wpa81\Akavache.Sqlite3.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog">
<HintPath>..\..\packages\NLog.3.1.0.0\lib\net45\NLog.dll</HintPath>
</Reference>
<Reference Include="NullGuard">
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Octokit">
<HintPath>..\..\packages\Octokit.0.4.1\lib\net45\Octokit.dll</HintPath>
</Reference>
<Reference Include="Octokit.Reactive">
<HintPath>..\..\packages\Octokit.Reactive.0.4.1\lib\net45\Octokit.Reactive.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="ReactiveUI">
<HintPath>..\..\packages\reactiveui-core.6.0.6\lib\Net45\ReactiveUI.dll</HintPath>
</Reference>
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Reactive.Core">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Interfaces">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Linq">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.PlatformServices">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Windows.Threading">
<HintPath>..\..\packages\Rx-XAML.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
</Reference>
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Authentication\AuthenticationResult.cs" />
<Compile Include="Authentication\IAccount.cs" />
<Compile Include="Authentication\ITwoFactorChallengeHandler.cs" />
<Compile Include="Authentication\TwoFactorChallengeHandler.cs" />
<Compile Include="Authentication\TwoFactorRequiredUserError.cs" />
<Compile Include="Caches\HostCache.cs" />
<Compile Include="Caches\HostCacheFactory.cs" />
<Compile Include="Caches\IBlobCacheFactory.cs" />
<Compile Include="Caches\IHostCache.cs" />
<Compile Include="Caches\IHostCacheFactory.cs" />
<Compile Include="Caches\ILoginCache.cs" />
<Compile Include="Caches\ISharedCache.cs" />
<Compile Include="Caches\LoginCache.cs" />
<Compile Include="Caches\SharedCache.cs" />
<Compile Include="Caches\SqlitePersistentBlobCacheFactory.cs" />
<Compile Include="Info\EnvironmentExtensions.cs" />
<Compile Include="Info\GitHubUrls.cs" />
<Compile Include="Infrastructure\AppModeDetector.cs" />
<Compile Include="Infrastructure\ExportWrappers.cs" />
<Compile Include="Models\Account.cs" />
<Compile Include="Models\DisconnectedRepositoryHosts.cs" />
<Compile Include="Models\LocalRepositoriesAccount.cs" />
<Compile Include="Models\LocalRepositoriesHost.cs" />
<Compile Include="Models\Program.cs" />
<Compile Include="Models\RepositoryHost.cs" />
<Compile Include="Models\RepositoryHosts.cs" />
<Compile Include="Primitives\HostAddress.cs" />
<Compile Include="Models\IProgram.cs" />
<Compile Include="Models\IRepositoryHost.cs" />
<Compile Include="Models\IRepositoryHosts.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\AccountFactory.cs" />
<Compile Include="Services\ApiClient.cs" />
<Compile Include="Services\ApiClientFactory.cs" />
<Compile Include="Services\Browser.cs" />
<Compile Include="Services\EnterpriseProbe.cs" />
<Compile Include="Services\GitHubCredentialStore.cs" />
<Compile Include="Services\IAccountFactory.cs" />
<Compile Include="Services\IApiClient.cs" />
<Compile Include="Services\IApiClientFactory.cs" />
<Compile Include="Services\IBrowser.cs" />
<Compile Include="Services\IEnterpriseProbe.cs" />
<Compile Include="Services\IRepositoryHostFactory.cs" />
<Compile Include="Services\RepositoryHostFactory.cs" />
<Compile Include="Services\StandardUserErrors.cs" />
<Compile Include="ViewModels\LoginControlViewModel.cs" />
<Compile Include="ViewModels\TwoFactorDialogViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj">
<Project>{6559e128-8b40-49a5-85a8-05565ed0c7e3}</Project>
<Name>GitHub.Extensions.Reactive</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
<Name>GitHub.Extensions</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj">
<Project>{158b05e8-fdbc-4d71-b871-c96e28d5adf5}</Project>
<Name>GitHub.UI.Reactive</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets'))" />
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
</Target>
<Import Project="..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets" Condition="Exists('..\..\packages\SQLitePCL.raw_basic.0.5.0\build\net45\SQLitePCL.raw_basic.targets')" />
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Просмотреть файл

@ -0,0 +1,27 @@
using System.IO;
using Rothko;
using Environment = System.Environment;
namespace GitHub
{
public static class EnvironmentExtensions
{
const string applicationName = "GitHub";
public static string GetLocalGitHubApplicationDataPath(this IEnvironment environment)
{
return Path.Combine(environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
applicationName);
}
public static string GetApplicationDataPath(this IEnvironment environment)
{
return Path.Combine(environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), applicationName);
}
public static string GetProgramFilesPath(this IEnvironment environment)
{
return environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
}
}
}

Просмотреть файл

@ -0,0 +1,82 @@
using System;
using System.Globalization;
namespace GitHub.Info
{
public static class GitHubUrls
{
/// <summary>
/// https://github.com/
/// </summary>
public const string GitHub = "https://github.com/";
/// <summary>
/// The url for a user's dashboard on github.com.
/// </summary>
public const string Dashboard = GitHub + "/dashboard";
/// <summary>
/// The url for contacting support
/// </summary>
public const string ContactSupport = GitHub + "/contact";
/// <summary>
/// The GitHub for Windows download and promo site
/// </summary>
public const string GitHubForWindows = "http://windows.github.com";
/// <summary>
/// The url for a user's billing information.
/// </summary>
public const string UserBilling = GitHub + "/account/billing";
/// <summary>
/// The url for viewing signup information and plans on github.com.
/// This includes a specific referral_code so we can track people
/// coming to the site from the Windows App.
/// </summary>
public static readonly Uri Plans = new Uri(GitHub + "/plans?referral_code=GitHubWindows");
/// <summary>
/// The url for resetting your password on github
/// </summary>
public static readonly Uri ForgotPasswordPath = new Uri("/sessions/forgot_password", UriKind.Relative);
/// <summary>
/// The url to learn about GitHub:Enterprise
/// </summary>
/// <remarks>
/// This doesn't change per enterprise repo.
/// </remarks>
public static readonly Uri GitHubEnterpriseWeb = new Uri("https://enterprise.github.com");
/// <summary>
/// The GitHub maintained repo of common .gitignores.
/// </summary>
/// <remarks>
/// This doesn't change per enterprise repo.
/// </remarks>
public const string GitIgnoreRepo = "https://github.com/github/gitignore";
/// <summary>
/// GitHub Help link about .gitattributes and line endings.
/// </summary>
public const string GitAttributesHelp = "https://help.github.com/articles/dealing-with-line-endings";
/// <summary>
/// The release notes page on the GitHub for Windows marketing site.
/// </summary>
public const string WindowsChangeLog = "https://windows.github.com/release-notes.html";
/// <summary>
/// The url for viewing billing information associated with a GitHub account.
/// </summary>
public static string Billing(this IAccount account)
{
return account.IsUser
? UserBilling
: string.Format(CultureInfo.InvariantCulture,
GitHub + "/organizations/{0}/settings/billing", account.Login);
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
using Splat;
namespace GitHub.Infrastructure
{
public class AppModeDetector : IModeDetector
{
public bool? InUnitTestRunner()
{
return false;
}
public bool? InDesignMode()
{
return false;
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using System.ComponentModel.Composition;
using Octokit.Internal;
namespace GitHub.Infrastructure
{
/// <summary>
/// Since VS doesn't support dynamic component registration, we have to implement wrappers
/// for types we don't control in order to export them.
/// </summary>
[Export(typeof(IHttpClient))]
public class ExportedHttpClient : HttpClientAdapter
{
}
}

Просмотреть файл

@ -0,0 +1,164 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Reactive.Linq;
using Octokit;
using ReactiveUI;
namespace GitHub.Models
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Account : ReactiveObject, IAccount
{
string email;
readonly ObservableAsPropertyHelper<bool> isOnFreePlan;
readonly ObservableAsPropertyHelper<bool> hasMaximumPrivateRepositoriesLeft;
readonly ObservableAsPropertyHelper<bool> isGitHubStaff;
string login;
string name;
int ownedPrivateRepos;
long privateReposInPlan;
bool isSiteAdmin;
public Account(IRepositoryHost host, User user)
: this(host)
{
Id = user.Id;
IsUser = true;
Update(user);
}
public Account(IRepositoryHost host, Organization organization)
: this(host)
{
Id = organization.Id;
IsUser = false;
Update(organization);
}
private Account(IRepositoryHost host)
{
Host = host;
isOnFreePlan = this.WhenAny(x => x.PrivateReposInPlan, x => x.Value == 0)
.ToProperty(this, x => x.IsOnFreePlan);
hasMaximumPrivateRepositoriesLeft = this.WhenAny(
x => x.OwnedPrivateRepos,
x => x.PrivateReposInPlan,
(owned, avalible) => owned.Value >= avalible.Value)
.ToProperty(this, x => x.HasMaximumPrivateRepositories);
isGitHubStaff = this.WhenAny(x => x.IsSiteAdmin, x => x.Value)
.Select(admin => admin && host.IsGitHub)
.ToProperty(this, x => x.IsGitHubStaff);
}
public string Email
{
get { return email; }
private set { this.RaiseAndSetIfChanged(ref email, value); }
}
/// <summary>
/// Returns true if the user is a member of the GitHub staff.
/// </summary>
public bool IsGitHubStaff
{
get { return isGitHubStaff.Value; }
}
public IRepositoryHost Host { get; private set; }
public int Id { get; private set; }
public bool IsEnterprise { get { return Host.IsEnterprise; } }
public bool IsGitHub { get { return Host.IsGitHub; } }
public bool IsLocal { get { return false; } }
public bool IsOnFreePlan
{
get { return isOnFreePlan.Value; }
}
public bool HasMaximumPrivateRepositories
{
get { return hasMaximumPrivateRepositoriesLeft.Value; }
}
public bool IsUser { get; private set; }
/// <summary>
/// True if the user is an admin on the host (GitHub or Enterprise).
/// </summary>
/// <remarks>
/// Do not confuse this with "IsStaff". This is true if the user is an admin
/// on the site. IsStaff is true if that site is github.com.
/// </remarks>
public bool IsSiteAdmin
{
get { return isSiteAdmin; }
private set { this.RaiseAndSetIfChanged(ref isSiteAdmin, value); }
}
public string Login
{
get { return login; }
private set { this.RaiseAndSetIfChanged(ref login, value); }
}
public string Name
{
get { return name; }
private set { this.RaiseAndSetIfChanged(ref name, value); }
}
public int OwnedPrivateRepos
{
get { return ownedPrivateRepos; }
private set { this.RaiseAndSetIfChanged(ref ownedPrivateRepos, value); }
}
public long PrivateReposInPlan
{
get { return privateReposInPlan; }
private set { this.RaiseAndSetIfChanged(ref privateReposInPlan, value); }
}
public void Update(User user)
{
IsSiteAdmin = user.SiteAdmin;
UpdateAccountInfo(user);
}
public void Update(Organization organization)
{
UpdateAccountInfo(organization);
}
void UpdateAccountInfo(Octokit.Account githubAccount)
{
if (Id != githubAccount.Id) return;
Email = githubAccount.Email;
Login = githubAccount.Login;
Name = githubAccount.Name ?? githubAccount.Login;
OwnedPrivateRepos = githubAccount.OwnedPrivateRepos;
PrivateReposInPlan = (githubAccount.Plan == null ? 0 : githubAccount.Plan.PrivateRepos);
}
internal string DebuggerDisplay
{
get
{
return String.Format(CultureInfo.InvariantCulture,
"Account: Login: {0} Name: {1}, Id: {2} Id: ", Login, Name, Id);
}
}
}
}

Просмотреть файл

@ -0,0 +1,66 @@

using System;
using System.Diagnostics;
using System.Reactive;
using System.Reactive.Linq;
using Akavache;
using GitHub.Authentication;
using NullGuard;
using ReactiveUI;
namespace GitHub.Models
{
public class DisconnectedRepositoryHost : ReactiveObject, IRepositoryHost
{
public DisconnectedRepositoryHost()
{
Address = HostAddress.Create(new Uri("https://null/"));
Organizations = new ReactiveList<IAccount>();
Accounts = new ReactiveList<IAccount>();
}
public HostAddress Address { get; private set; }
public IApiClient ApiClient { get; private set; }
[AllowNull]
public IHostCache Cache
{
[return: AllowNull]
get;
private set;
}
public bool IsGitHub { get; private set; }
public bool IsLoggedIn { get; private set; }
public bool IsLoggingIn { get; private set; }
public bool IsEnterprise { get; private set; }
public bool IsLocal { get; private set; }
public ReactiveList<IAccount> Organizations { get; private set; }
public ReactiveList<IAccount> Accounts { get; private set; }
public string Title { get; private set; }
public IAccount User { get; private set; }
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)
{
return Observable.Return(AuthenticationResult.CredentialFailure);
}
public IObservable<AuthenticationResult> LogInFromCache()
{
return Observable.Return(AuthenticationResult.CredentialFailure);
}
public IObservable<Unit> LogOut()
{
return Observable.Return(Unit.Default);
}
public IObservable<Unit> Refresh()
{
return Observable.Return(Unit.Default);
}
public IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc)
{
return Observable.Return(Unit.Default);
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
using System.Reflection;
using Octokit;
namespace GitHub.Models
{
// Represents the currently executing program.
public interface IProgram
{
AssemblyName AssemblyName { get; }
string ExecutingAssemblyDirectory { get; }
ProductHeaderValue ProductHeader { get; }
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using System.Reactive;
using GitHub.Authentication;
using GitHub.Helpers;
using ReactiveUI;
namespace GitHub.Models
{
public interface IRepositoryHost : IReactiveObject
{
HostAddress Address { get; }
IApiClient ApiClient { get; }
IHostCache Cache { get; }
bool IsGitHub { get; }
bool IsLoggedIn { get; }
bool IsLoggingIn { get; }
bool IsEnterprise { get; }
bool IsLocal { get; }
ReactiveList<IAccount> Organizations { get; }
ReactiveList<IAccount> Accounts { get; }
string Title { get; }
IAccount User { get; }
IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password);
IObservable<AuthenticationResult> LogInFromCache();
IObservable<Unit> LogOut();
IObservable<Unit> Refresh();
IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc);
}
}

Просмотреть файл

@ -0,0 +1,20 @@
using System;
using GitHub.Authentication;
using ReactiveUI;
namespace GitHub.Models
{
public interface IRepositoryHosts : IReactiveObject
{
IRepositoryHost EnterpriseHost { get; set; }
IRepositoryHost GitHubHost { get; }
IRepositoryHost LocalRepositoriesHost { get; }
IObservable<AuthenticationResult> LogInEnterpriseHost(
HostAddress enterpriseHostAddress,
string usernameOrEmail,
string password);
IObservable<AuthenticationResult> LogInGitHubHost(string usernameOrEmail, string password);
IRepositoryHostFactory RepositoryHostFactory { get; }
bool IsLoggedInToAnyHost { get; }
}
}

Просмотреть файл

@ -0,0 +1,49 @@
using Octokit;
using ReactiveUI;
namespace GitHub.Models
{
public class LocalRepositoriesAccount : ReactiveObject, IAccount
{
public LocalRepositoriesAccount(IRepositoryHost host)
{
Avatar = "pack://application:,,,/GitHub;component/Images/computer.png";
Email = string.Empty;
Id = 0;
IsOnFreePlan = true;
IsUser = true;
Host = host;
Login = "repositories";
Name = "repositories";
OwnedPrivateRepos = 0;
PrivateReposInPlan = 0;
IsSiteAdmin = false;
IsGitHubStaff = false;
}
public object Avatar { get; private set; }
public string Email { get; private set; }
public int Id { get; private set; }
public bool IsEnterprise { get { return false; } }
public bool IsGitHub { get { return false; } }
public bool IsLocal { get { return true; } }
public bool IsOnFreePlan { get; private set; }
public bool HasMaximumPrivateRepositories { get; private set; }
public bool IsUser { get; private set; }
public bool IsSiteAdmin { get; private set; }
public bool IsGitHubStaff { get; private set; }
public IRepositoryHost Host { get; private set; }
public string Login { get; private set; }
public string Name { get; private set; }
public int OwnedPrivateRepos { get; private set; }
public long PrivateReposInPlan { get; private set; }
public void Update(User ghUser)
{
}
public void Update(Organization org)
{
}
}
}

Просмотреть файл

@ -0,0 +1,86 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using GitHub.Authentication;
using ReactiveUI;
namespace GitHub.Models
{
public class LocalRepositoriesHost : ReactiveObject, IRepositoryHost
{
readonly Lazy<IAccount> userThunk;
public LocalRepositoriesHost()
{
userThunk = new Lazy<IAccount>(() => new LocalRepositoriesAccount(this));
Address = null;
ApiClient = null;
Cache = null;
IsGitHub = false;
IsLoggedIn = true;
IsLoggingIn = false;
Organizations = new ReactiveList<IAccount>();
Accounts = new ReactiveList<IAccount>();
Title = "local";
}
public HostAddress Address { get; private set; }
public IApiClient ApiClient { get; private set; }
public IHostCache Cache { get; private set; }
public bool IsGitHub { get; private set; }
public bool IsLoggedIn { get; private set; }
public bool IsLoggingIn { get; private set; }
public ReactiveList<IAccount> Organizations { get; private set; }
public ReactiveList<IAccount> Accounts { get; private set; }
public string Title { get; private set; }
public IAccount User
{
get { return userThunk.Value; }
}
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)
{
return Observable.Return(AuthenticationResult.Success);
}
public IObservable<AuthenticationResult> LogInFromCache()
{
return Observable.Return(AuthenticationResult.Success);
}
public IObservable<Unit> LogOut()
{
return Observable.Return(Unit.Default);
}
public IObservable<Unit> Refresh()
{
return Observable.Return(Unit.Default);
}
public IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc)
{
return Observable.Return(Unit.Default);
}
bool isSelected;
public bool IsSelected
{
get { return isSelected; }
set { this.RaiseAndSetIfChanged(ref isSelected, value); }
}
public bool IsEnterprise
{
get { return false; }
}
public bool IsLocal
{
get { return true; }
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
using System.ComponentModel.Composition;
using System.IO;
using System.Reflection;
using GitHub.Models;
using Octokit;
namespace GitHub
{
// Represents the currently executing program.
[Export(typeof(IProgram))]
public class Program : IProgram
{
public Program()
{
var executingAssembly = typeof(Program).Assembly;
AssemblyName = executingAssembly.GetName();
ExecutingAssemblyDirectory = Path.GetDirectoryName(executingAssembly.Location);
ProductHeader = new ProductHeaderValue("GitHubVS", AssemblyName.Version.ToString());
}
public AssemblyName AssemblyName { get; private set; }
public string ExecutingAssemblyDirectory { get; private set; }
public ProductHeaderValue ProductHeader { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,520 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Reactive;
using System.Reactive.Linq;
using GitHub.Authentication;
using GitHub.Extensions;
using GitHub.Extensions.Reactive;
using GitHub.Services;
using NLog;
using Octokit;
using ReactiveUI;
using Authorization = Octokit.Authorization;
namespace GitHub.Models
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class RepositoryHost : ReactiveObject, IRepositoryHost
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
bool isLoggedIn;
bool isLoggingIn;
bool isSelected;
IAccount userAccount;
readonly IAccountFactory accountFactory;
public RepositoryHost(
IApiClient apiClient,
IHostCache hostCache,
ILoginCache loginCache,
IAccountFactory accountFactory)
{
Debug.Assert(apiClient.HostAddress != null, "HostAddress of an api client shouldn't be null");
Address = apiClient.HostAddress;
ApiBaseUri = apiClient.HostAddress.ApiUri;
ApiClient = apiClient;
Debug.Assert(ApiBaseUri != null, "Mistakes were made. ApiClient must have non-null ApiBaseUri");
IsGitHub = ApiBaseUri.Equals(Api.ApiClient.GitHubDotComApiBaseUri);
Cache = hostCache;
LoginCache = loginCache;
this.accountFactory = accountFactory;
IsEnterprise = !IsGitHub;
Organizations = new ReactiveList<IAccount>();
Title = MakeTitle(ApiBaseUri);
Accounts = new ReactiveList<IAccount>();
this.WhenAny(x => x.IsLoggedIn, x => x.Value)
.Where(loggedIn => loggedIn)
.Subscribe(_ => OnUserLoggedIn(User));
this.WhenAny(x => x.IsLoggedIn, x => x.Value)
.Skip(1) // so we don't log the account out on the initial evaluation of the WhenAny
.Where(loggedIn => !loggedIn)
.Subscribe(_ => OnUserLoggedOut());
this.WhenAny(x => x.Organizations.ItemsAdded, x => x.Value)
.SelectMany(x => x)
.Subscribe(OnOrgAdded);
this.WhenAny(x => x.Organizations.ItemsRemoved, x => x.Value)
.SelectMany(x => x)
.Subscribe(OnOrgRemoved);
}
Uri ApiBaseUri { get; set; }
public HostAddress Address { get; private set; }
public IApiClient ApiClient { get; private set; }
public IHostCache Cache { get; private set; }
public bool IsGitHub { get; private set; }
public bool IsEnterprise { get; private set; }
public bool IsLocal
{
get { return false; }
}
public bool IsLoggedIn
{
get { return isLoggedIn; }
private set { this.RaiseAndSetIfChanged(ref isLoggedIn, value); }
}
public bool IsLoggingIn
{
get { return isLoggingIn; }
private set { this.RaiseAndSetIfChanged(ref isLoggingIn, value); }
}
public bool IsSelected
{
get { return isSelected; }
set { this.RaiseAndSetIfChanged(ref isSelected, value); }
}
public ReactiveList<IAccount> Organizations { get; private set; }
public string Title { get; private set; }
public IAccount User
{
get { return userAccount; }
private set { this.RaiseAndSetIfChanged(ref userAccount, value); }
}
public ReactiveList<IAccount> Accounts { get; private set; }
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
public IObservable<AuthenticationResult> LogInFromCache()
{
IsLoggingIn = true;
return ApiClient.GetUser()
.ObserveOn(RxApp.MainThreadScheduler)
.Catch<User, Exception>(ex =>
{
if (ex is AuthorizationException)
{
IsLoggingIn = false;
log.Warn("Got an authorization exception", ex);
return Observable.Return<User>(null);
}
return Cache.GetUser()
.Catch<User, Exception>(e =>
{
IsLoggingIn = false;
log.Warn("User does not exist in cache", e);
return Observable.Return<User>(null);
})
.ObserveOn(RxApp.MainThreadScheduler);
})
.SelectMany(LoginWithApiUser)
.Do(result =>
{
if (result.IsFailure()) return;
AddCachedOrganizations();
})
.PublishAsync();
}
static readonly User unverifiedUser = new User { Id = int.MinValue };
public IObservable<AuthenticationResult> LogIn(string usernameOrEmail, string password)
{
Guard.ArgumentNotEmptyString(usernameOrEmail, "usernameOrEmail");
Guard.ArgumentNotEmptyString(password, "password");
// If we need to retry on fallback, we'll store the 2FA token
// from the first request to re-use:
string authenticationCode = null;
// We need to intercept the 2FA handler to get the token:
var interceptingTwoFactorChallengeHandler =
new Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>>(ex =>
ApiClient.TwoFactorChallengeHandler.HandleTwoFactorException(ex)
.Do(twoFactorChallengeResult =>
authenticationCode = twoFactorChallengeResult.AuthenticationCode));
// Keep the function to save the authorization token here because it's used
// in multiple places in the chain below:
var saveAuthorizationToken = new Func<Authorization, IObservable<Unit>>(authorization =>
{
if (authorization == null || String.IsNullOrWhiteSpace(authorization.Token))
return Observable.Return(Unit.Default);
return LoginCache.SaveLogin(authorization.Token, "x-oauth-basic", Address)
.ObserveOn(RxApp.MainThreadScheduler);
});
// Start be saving the username and password, as they will be used for older versions of Enterprise
// that don't support authorization tokens, and for the API client to use until an authorization
// token has been created and acquired:
return LoginCache.SaveLogin(usernameOrEmail, password, Address)
.Do(_ => IsLoggingIn = true)
.ObserveOn(RxApp.MainThreadScheduler)
// Try to get an authorization token, save it, then get the user to log in:
.SelectMany(_ => ApiClient.GetOrCreateApplicationAuthenticationCode(interceptingTwoFactorChallengeHandler))
.SelectMany(saveAuthorizationToken)
.SelectMany(_ => ApiClient.GetUser())
.Catch<User, ApiException>(firstTryEx =>
{
var exception = firstTryEx as AuthorizationException;
if (IsEnterprise
&& exception != null
&& exception.Message == "Bad credentials")
{
return Observable.Throw<User>(exception);
}
// If the Enterprise host doesn't support the write:public_key scope, it'll return a 422.
// EXCEPT, there's a bug where it doesn't, and instead creates a bad token, and in
// that case we'd get a 401 here from the GetUser invocation. So to be safe (and consistent
// with the Mac app), we'll just retry after any API error for Enterprise hosts:
if (IsEnterprise && !(firstTryEx is TwoFactorChallengeFailedException))
{
// Because we potentially have a bad authorization token due to the Enterprise bug,
// we need to reset to using username and password authentication:
return LoginCache.SaveLogin(usernameOrEmail, password, Address)
.ObserveOn(RxApp.MainThreadScheduler)
.SelectMany(_ =>
{
// Retry with the old scopes. If we have a stashed 2FA token, we use it:
if (authenticationCode != null)
return ApiClient.GetOrCreateApplicationAuthenticationCode(
authenticationCode,
true);
// Otherwise, we use the default handler:
return ApiClient.GetOrCreateApplicationAuthenticationCode(useOldScopes: true);
})
// Then save the authorization token (if there is one) and get the user:
.SelectMany(saveAuthorizationToken)
.SelectMany(_ => ApiClient.GetUser());
}
return Observable.Throw<User>(firstTryEx);
})
.Catch<User, ApiException>(retryEx =>
{
// Older Enterprise hosts either don't have the API end-point to PUT an authorization, or they
// return 422 because they haven't white-listed our client ID. In that case, we just ignore
// the failure, using basic authentication (with username and password) instead of trying
// to get an authorization token.
if (IsEnterprise && (retryEx is NotFoundException || retryEx.StatusCode == (HttpStatusCode)422))
return ApiClient.GetUser();
// Other errors are "real" so we pass them along:
return Observable.Throw<User>(retryEx);
})
.ObserveOn(RxApp.MainThreadScheduler)
.Catch<User, Exception>(ex =>
{
// If we get here, we have an actual login failure:
IsLoggingIn = false;
if (ex is TwoFactorChallengeFailedException)
{
return Observable.Return(unverifiedUser);
}
if (ex is AuthorizationException)
{
return Observable.Return(default(User));
}
return Observable.Throw<User>(ex);
})
.SelectMany(LoginWithApiUser)
.Do(result =>
{
if (result.IsFailure()) return;
RefreshOrgs().Subscribe(
_ => { },
ex => log.Warn("Failed to refresh orgs.", ex));
})
.PublishAsync();
}
IObservable<AuthenticationResult> LoginWithApiUser(User user)
{
return Observable.Start(() =>
{
if (user == null)
{
IsLoggingIn = false;
return AuthenticationResult.CredentialFailure;
}
if (user == unverifiedUser)
{
IsLoggingIn = false;
LoginCache.EraseLogin(Address);
return AuthenticationResult.VerificationFailure;
}
Cache.InsertUser(user);
User = accountFactory.CreateAccount(this, user);
IsLoggedIn = true;
IsLoggingIn = false;
return AuthenticationResult.Success;
})
.Do(result => log.Info("Log in from cache for login '{0}' to host '{1}' {2}",
user != null ? user.Login : "(null)",
ApiBaseUri,
result.IsSuccess() ? "SUCCEEDED" : "FAILED"))
.PublishAsync();
}
public IObservable<Unit> LogOut()
{
if (!IsLoggedIn) return Observable.Return(Unit.Default);
log.Info(CultureInfo.InvariantCulture, "Logged user {0} off of host '{1}'", User.Login, ApiBaseUri);
return LoginCache.EraseLogin(Address)
.Catch<Unit, Exception>(e =>
{
log.Warn("ASSERT! Failed to erase login. Going to invalidate cache anyways.", e);
return Observable.Return(Unit.Default);
})
.SelectMany(_ => Cache.InvalidateAll())
.Catch<Unit, Exception>(e =>
{
log.Warn("ASSERT! Failed to invaldiate caches", e);
return Observable.Return(Unit.Default);
})
.ObserveOn(RxApp.MainThreadScheduler)
.Finally(() =>
{
IsLoggedIn = false;
Organizations.Clear();
User = null;
});
}
public IObservable<Unit> Refresh()
{
return Refresh(h => Observable.Return(Unit.Default));
}
public IObservable<Unit> Refresh(Func<IRepositoryHost, IObservable<Unit>> refreshTrackedRepositoriesFunc)
{
if (!IsLoggedIn) return Observable.Return(Unit.Default);
try
{
return Observable.Merge(
RefreshUser(),
RefreshOrgs())
.AsCompletion()
.SelectMany(_ => refreshTrackedRepositoriesFunc(this))
.Catch<Unit, Exception>(ex =>
{
log.Warn("Refresh failed.", ex);
return ex.ShowUserErrorMessage(ErrorType.RefreshFailed)
.AsCompletion();
});
}
catch (Exception ex)
{
log.Warn("Repository host refresh failed.", ex);
return ex.ShowUserErrorMessage(ErrorType.RefreshFailed)
.AsCompletion();
}
}
protected ILoginCache LoginCache { get; private set; }
void AddCachedOrganizations()
{
Cache.GetAllOrganizations()
.Select(orgs => orgs.Where(o => o != null))
.Where(orgs => orgs.Any())
.Select(orgs => orgs.OrderBy(org => org.Login))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(
AddCachedOrganizations,
ex => log.Warn("Failed to get orgs from cache.", ex));
}
void AddCachedOrganizations(IEnumerable<Organization> ghOrgs)
{
if (ghOrgs == null) return;
var orgs = ghOrgs.Select(ghOrg => accountFactory.CreateAccount(this, ghOrg))
.Except(Organizations, (first, second) => first.Id == second.Id); // only add from cache if not already there
// instead of AddRange here, we need to add the
// items one at a time so the ItemAdded and ItemRemoved
// signals are raised for the account list
foreach (var org in orgs)
{
Organizations.Add(org);
}
}
void AddOrUpdateOrg(IAccount org)
{
if (org == null) return;
var existingOrg = Organizations.SingleOrDefault(x => x.Id == org.Id);
if (existingOrg == null)
{
Organizations.Add(org);
return;
}
Organizations[Organizations.IndexOf(existingOrg)] = org;
}
static string MakeTitle(Uri apiBaseUri)
{
return apiBaseUri.Equals(Api.ApiClient.GitHubDotComApiBaseUri) ?
"github" :
apiBaseUri.Host;
}
void RefreshOrgs(ICollection<IAccount> orgs)
{
orgs.ForEach(org =>
{
AddOrUpdateOrg(org);
ApiClient.GetOrganization(org.Login)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(
ghOrg =>
{
Cache.InsertOrganization(ghOrg);
UpdateOrg(ghOrg);
},
ex => log.Warn("Failed to get organization.", ex));
});
var orgsToRemove = Organizations.Except(orgs, (x, y) => x.Id == y.Id).ToArray();
orgsToRemove.ForEach(orgToRemove => Cache.InvalidateOrganization(orgToRemove));
RemoveOrgs(orgsToRemove);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
IObservable<Unit> RefreshOrgs()
{
return ApiClient.GetOrganizations()
.WhereNotNull()
.ToArray()
.ObserveOn(RxApp.MainThreadScheduler)
.Select(ghOrgs => ghOrgs
.OrderBy(ghOrg => ghOrg.Login)
.Select(org => accountFactory.CreateAccount(this, org))
.ToArray())
.Select(ghOrgs =>
{
RefreshOrgs(ghOrgs);
return Unit.Default;
})
.PublishAsync();
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
IObservable<Unit> RefreshUser()
{
return ApiClient.GetUser()
.WhereNotNull()
.ObserveOn(RxApp.MainThreadScheduler)
.Select(ghUser =>
{
Cache.InsertUser(ghUser);
if (User != null)
User.Update(ghUser);
return Unit.Default;
})
.PublishAsync();
}
void RemoveOrgs(IEnumerable<IAccount> orgs)
{
orgs.ForEach(org =>
{
var orgToRemove = Organizations.SingleOrDefault(o => org.Id == o.Id);
if (orgToRemove != null)
Organizations.Remove(orgToRemove);
});
}
void UpdateOrg(Organization ghOrg)
{
if (ghOrg == null) return;
var existingOrg = Organizations.SingleOrDefault(x => x.Id == ghOrg.Id);
if (existingOrg != null)
existingOrg.Update(ghOrg);
}
void OnOrgAdded(IAccount account)
{
var accountTile = Accounts.SingleOrDefault(x => x != null && x.Id == account.Id);
if (accountTile == null)
{
Accounts.Add(account);
}
}
void OnOrgRemoved(IAccount account)
{
var accountTile = Accounts.SingleOrDefault(x => x != null && x.Id == account.Id);
if (accountTile == null) return;
Accounts.Remove(accountTile);
}
void OnUserLoggedIn(IAccount account)
{
Accounts.Insert(0, account);
}
void OnUserLoggedOut()
{
Accounts.Clear();
}
internal string DebuggerDisplay
{
get
{
return String.Format(CultureInfo.InvariantCulture, "RepositoryHost: {0} {1}", Title, ApiBaseUri);
}
}
}
}

Просмотреть файл

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Reactive;
using System.Reactive.Linq;
using Akavache;
using GitHub.Authentication;
using GitHub.Extensions.Reactive;
using ReactiveUI;
namespace GitHub.Models
{
[Export(typeof(IRepositoryHosts))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class RepositoryHosts : ReactiveObject, IRepositoryHosts, IDisposable
{
static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
public static DisconnectedRepositoryHost DisconnectedRepositoryHost = new DisconnectedRepositoryHost();
public const string EnterpriseHostApiBaseUriCacheKey = "enterprise-host-api-base-uri";
readonly ObservableAsPropertyHelper<bool> isLoggedInToAnyHost;
[ImportingConstructor]
public RepositoryHosts(
IRepositoryHostFactory repositoryHostFactory,
ISharedCache sharedCache)
{
RepositoryHostFactory = repositoryHostFactory;
LocalRepositoriesHost = new LocalRepositoriesHost();
GitHubHost = repositoryHostFactory.Create(HostAddress.GitHubDotComHostAddress);
EnterpriseHost = DisconnectedRepositoryHost;
var initialCacheLoadObs = sharedCache.UserAccount.GetObject<Uri>(EnterpriseHostApiBaseUriCacheKey)
.Catch<Uri, KeyNotFoundException>(_ => Observable.Return<Uri>(null))
.Catch<Uri, Exception>(ex =>
{
log.Warn("Failed to get Enterprise host URI from cache.", ex);
return Observable.Return<Uri>(null);
})
.WhereNotNull()
.Select(HostAddress.Create)
.Select(repositoryHostFactory.Create)
.Do(x => EnterpriseHost = x)
.SelectUnit();
var persistEntepriseHostObs = this.WhenAny(x => x.EnterpriseHost, x => x.Value)
.Skip(1) // The first value will be null or something already in the db
.SelectMany(enterpriseHost =>
{
if (!enterpriseHost.IsLoggedIn)
{
return sharedCache.UserAccount
.InvalidateObject<Uri>(EnterpriseHostApiBaseUriCacheKey)
.Catch<Unit, Exception>(ex =>
{
log.Warn("Failed to invalidate enterprise host uri", ex);
return Observable.Return(Unit.Default);
});
}
return sharedCache.UserAccount
.InsertObject(EnterpriseHostApiBaseUriCacheKey, enterpriseHost.Address.ApiUri)
.Catch<Unit, Exception>(ex =>
{
log.Warn("Failed to persist enterprise host uri", ex);
return Observable.Return(Unit.Default);
});
});
isLoggedInToAnyHost = this.WhenAny(
x => x.GitHubHost.IsLoggedIn,
x => x.EnterpriseHost.IsLoggedIn,
(githubLoggedIn, enterpriseLoggedIn) => githubLoggedIn.Value || enterpriseLoggedIn.Value)
.ToProperty(this, x => x.IsLoggedInToAnyHost);
// Wait until we've loaded (or failed to load) an enterprise uri from the db and then
// start tracking changes to the EntepriseHost property and persist every change to the db
Observable.Concat(initialCacheLoadObs, persistEntepriseHostObs).Subscribe();
}
IRepositoryHost githubHost;
public IRepositoryHost GitHubHost
{
get { return githubHost; }
private set { this.RaiseAndSetIfChanged(ref githubHost, value); }
}
IRepositoryHost enterpriseHost;
public IRepositoryHost EnterpriseHost
{
get { return enterpriseHost; }
set
{
var newHost = value ?? DisconnectedRepositoryHost;
this.RaiseAndSetIfChanged(ref enterpriseHost, newHost);
}
}
IRepositoryHost localRepositoriesHost;
public IRepositoryHost LocalRepositoriesHost
{
get { return localRepositoriesHost; }
set { this.RaiseAndSetIfChanged(ref localRepositoriesHost, value); }
}
public IObservable<AuthenticationResult> LogInEnterpriseHost(
HostAddress enterpriseHostAddress,
string usernameOrEmail,
string password)
{
var host = RepositoryHostFactory.Create(enterpriseHostAddress);
return host.LogIn(usernameOrEmail, password)
.Catch<AuthenticationResult, Exception>(Observable.Throw<AuthenticationResult>)
.Do(result =>
{
bool successful = result.IsSuccess();
log.Info("Log in to Enterprise host '{0}' with username '{1}' {2}",
enterpriseHostAddress.ApiUri,
usernameOrEmail,
successful ? "SUCCEEDED" : "FAILED");
if (successful)
{
EnterpriseHost = host;
}
});
}
public IObservable<AuthenticationResult> LogInGitHubHost(string usernameOrEmail, string password)
{
return GitHubHost.LogIn(usernameOrEmail, password)
.Catch<AuthenticationResult, Exception>(Observable.Throw<AuthenticationResult>)
.Do(result => log.Info("Log in to GitHub.com with username '{0}' {1}",
usernameOrEmail,
result.IsSuccess() ? "SUCCEEDED" : "FAILED"));
}
public IRepositoryHostFactory RepositoryHostFactory { get; private set; }
public bool IsLoggedInToAnyHost { get { return isLoggedInToAnyHost.Value; } }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing && EnterpriseHost.Cache != null)
{
EnterpriseHost.Cache.Dispose();
}
}
}
}

Просмотреть файл

@ -0,0 +1,79 @@
using System;
using System.Globalization;
using GitHub.Extensions;
namespace GitHub
{
public class HostAddress
{
public static HostAddress GitHubDotComHostAddress = new HostAddress();
static readonly Uri gistUri = new Uri("https://gist.github.com");
/// <summary>
/// Creates a host address based on the hostUri based on the expected patterns for GitHub.com and
/// GitHub Enterprise instances. The passed in URI can be any URL to the instance.
/// </summary>
/// <param name="hostUri">The URI to a GitHub or GitHub Enterprise instance.</param>
/// <returns></returns>
public static HostAddress Create(Uri hostUri)
{
return IsGitHubDotComUri(hostUri)
? GitHubDotComHostAddress
: new HostAddress(hostUri);
}
/// <summary>
/// Creates a host address from a host name or URL as a string.
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
public static HostAddress Create(string host)
{
Uri uri;
if (Uri.TryCreate(host, UriKind.Absolute, out uri)
|| Uri.TryCreate("https://" + host, UriKind.Absolute, out uri))
{
return Create(uri);
}
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture,
"The host '{0}' is not a valid host",
host));
}
private HostAddress(Uri enterpriseUri)
{
WebUri = new Uri(enterpriseUri, new Uri("/", UriKind.Relative));
ApiUri = new Uri(enterpriseUri, new Uri("/api/v3/", UriKind.Relative));
CredentialCacheKeyHost = ApiUri.Host;
}
private HostAddress()
{
WebUri = new Uri("https://github.com");
ApiUri = new Uri("https://api.github.com");
CredentialCacheKeyHost = "github.com";
}
/// <summary>
/// The Base URL to the host. For example, "https://github.com" or "https://ghe.io"
/// </summary>
public Uri WebUri { get; set; }
/// <summary>
/// The Base Url to the host's API endpoint. For example, "https://api.github.com" or
/// "https://ghe.io/api/v3"
/// </summary>
public Uri ApiUri { get; set; }
// If the host name is "api.github.com" or "gist.github.com", we really only want "github.com",
// since that's the same cache key for all the other github.com operations.
public string CredentialCacheKeyHost { get; private set; }
static bool IsGitHubDotComUri(Uri hostUri)
{
return hostUri.IsSameHost(GitHubDotComHostAddress.WebUri)
|| hostUri.IsSameHost(GitHubDotComHostAddress.ApiUri)
|| hostUri.IsSameHost(gistUri);
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.App")]
[assembly: AssemblyDescription("Provides the view models for the GitHub for Visual Studio extension")]
[assembly: Guid("a8b9a236-d238-4733-b116-716872a1e8e0")]

Просмотреть файл

@ -0,0 +1,24 @@
using System;
using System.ComponentModel.Composition;
using GitHub.Models;
namespace GitHub
{
[Export(typeof(IAccountFactory))]
public class AccountFactory : IAccountFactory
{
public IAccount CreateAccount(
IRepositoryHost repositoryHost,
Octokit.User user)
{
return new Account(repositoryHost, user);
}
public IAccount CreateAccount(
IRepositoryHost repositoryHost,
Octokit.Organization organization)
{
return new Account(repositoryHost, organization);
}
}
}

Просмотреть файл

@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net;
using System.Reactive.Linq;
using GitHub.Authentication;
using GitHub.Extensions;
using NLog;
using Octokit;
using Octokit.Reactive;
using ReactiveUI;
using Authorization = Octokit.Authorization;
using LogManager = NLog.LogManager;
namespace GitHub.Api
{
public class ApiClient : IApiClient
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
/// <summary>
/// https://github.com/
/// </summary>
public const string GitHubUrl = "https://" + GitHubDotComHostName;
public const string GitHubDotComHostName = "github.com";
public const string GitHubGistHostName = "gist.github.com";
const string clientId = "";
const string clientSecret = "";
public static readonly Uri GitHubDotComUri = new Uri(GitHubUrl);
public static readonly Uri GitHubDotComApiBaseUri = new Uri("https://api." + GitHubDotComHostName);
readonly IObservableGitHubClient gitHubClient;
// There are two sets of authorization scopes, old and new:
// The old scops must be used by older versions of Enterprise that don't support the new scopes:
readonly string[] oldAuthorizationScopes = { "user", "repo" };
// These new scopes include write:public_key, which allows us to add public SSH keys to an account:
readonly string[] newAuthorizationScopes = { "user", "repo", "write:public_key" };
public ApiClient(
HostAddress hostAddress,
IObservableGitHubClient gitHubClient,
ITwoFactorChallengeHandler twoFactorChallengeHandler)
{
HostAddress = hostAddress;
this.gitHubClient = gitHubClient;
TwoFactorChallengeHandler = twoFactorChallengeHandler;
}
public HostAddress HostAddress { get; private set; }
public IObservable<Repository> CreateRepository(Repository repo, string login, bool isUser)
{
Guard.ArgumentNotEmptyString(login, "login");
var newRepository = ToNewRepository(repo);
var client = gitHubClient.Repository;
return (isUser ? client.Create(newRepository) : client.Create(login, newRepository));
}
public IObservable<SshKey> GetSshKeys()
{
return gitHubClient.SshKey.GetAllForCurrent();
}
public IObservable<SshKey> AddSshKey(SshKey newKey)
{
log.Info("About to add SSH Key: {0} - {1}", newKey.Title, newKey.Key);
return gitHubClient.SshKey.Create(new SshKeyUpdate { Title = newKey.Title, Key = newKey.Key });
}
public IObservable<User> GetUser()
{
return gitHubClient.User.Current();
}
public IObservable<User> GetAllUsersForAllOrganizations()
{
return GetOrganizations().SelectMany(org => gitHubClient.Organization.Member.GetAll(org.Login));
}
public IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHander = null, bool useOldScopes = false)
{
var newAuthorization = new NewAuthorization
{
Scopes = useOldScopes
? oldAuthorizationScopes
: newAuthorizationScopes,
Note = "GitHub for Windows on " + GetMachineNameSafe()
};
var handler = twoFactorChallengeHander ?? TwoFactorChallengeHandler.HandleTwoFactorException;
Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> dispatchedHandler =
ex => Observable.Start(() => handler(ex), RxApp.MainThreadScheduler).SelectMany(result => result);
return gitHubClient.Authorization.GetOrCreateApplicationAuthentication(
clientId,
clientSecret,
newAuthorization,
dispatchedHandler);
}
public IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(
string authenticationCode,
bool useOldScopes = false)
{
Guard.ArgumentNotEmptyString(authenticationCode, "authenticationCode");
var newAuthorization = new NewAuthorization
{
Scopes = useOldScopes
? oldAuthorizationScopes
: newAuthorizationScopes,
Note = "GitHub for Windows on " + GetMachineNameSafe()
};
return gitHubClient.Authorization.GetOrCreateApplicationAuthentication(
clientId,
clientSecret,
newAuthorization,
authenticationCode);
}
public IObservable<IReadOnlyList<EmailAddress>> GetEmails()
{
return gitHubClient.User.Email.GetAll()
.ToArray()
.Select(emails => new ReadOnlyCollection<EmailAddress>(emails));
}
public IObservable<Organization> GetOrganization(string login)
{
Guard.ArgumentNotEmptyString(login, "login");
return gitHubClient.Organization.Get(login);
}
public IObservable<Organization> GetOrganizations()
{
return gitHubClient.Organization.GetAllForCurrent();
}
public IObservable<User> GetMembersOfOrganization(string organizationName)
{
return gitHubClient.Organization.Member.GetAll(organizationName);
}
IObservable<IEnumerable<Repository>> GetAllRepositoriesForOrganization(Organization org)
{
return gitHubClient.Repository.GetAllForOrg(org.Login)
.ToList();
}
IObservable<IEnumerable<Repository>> GetAllRepositoriesForCurrentUser()
{
return gitHubClient.Repository.GetAllForCurrent()
.ToList();
}
public IObservable<Repository> GetCurrentUserRepositoriesStreamed()
{
return gitHubClient.Repository.GetAllForCurrent();
}
public IObservable<Repository> GetOrganizationRepositoriesStreamed(string login)
{
return gitHubClient.Repository.GetAllForOrg(login);
}
public IObservable<Repository> GetRepository(string owner, string name)
{
return gitHubClient.Repository.Get(owner, name);
}
public IObservable<IEnumerable<Repository>> GetUserRepositories(int currentUserId)
{
return Observable.Merge(
GetAllRepositoriesForCurrentUser(),
GetOrganizations().SelectMany(GetAllRepositoriesForOrganization)
);
}
static NewRepository ToNewRepository(Repository repository)
{
return new NewRepository
{
Name = repository.Name,
Description = repository.Description,
Homepage = repository.Homepage,
Private = repository.Private,
HasIssues = repository.HasIssues,
HasWiki = repository.HasWiki,
HasDownloads= repository.HasDownloads
};
}
static string GetMachineNameSafe()
{
try
{
return Dns.GetHostName();
}
catch (Exception)
{
try
{
return Environment.MachineName;
}
catch (Exception)
{
return "(unknown)";
}
}
}
public ITwoFactorChallengeHandler TwoFactorChallengeHandler { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,41 @@
using System;
using System.ComponentModel.Composition;
using GitHub.Authentication;
using GitHub.Models;
using Octokit;
using Octokit.Reactive;
namespace GitHub.Api
{
[Export(typeof(IApiClientFactory))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class ApiClientFactory : IApiClientFactory
{
readonly ITwoFactorChallengeHandler twoFactorChallengeHandler;
readonly ProductHeaderValue productHeader;
[ImportingConstructor]
public ApiClientFactory(
ILoginCache loginCache,
ITwoFactorChallengeHandler twoFactorChallengeHandler,
IProgram program)
{
LoginCache = loginCache;
this.twoFactorChallengeHandler = twoFactorChallengeHandler;
productHeader = program.ProductHeader;
}
public IApiClient Create(HostAddress hostAddress)
{
var apiBaseUri = hostAddress.ApiUri;
return new ApiClient(
hostAddress,
new ObservableGitHubClient(
new GitHubClient(productHeader, new GitHubCredentialStore(hostAddress, LoginCache), apiBaseUri)),
twoFactorChallengeHandler);
}
protected ILoginCache LoginCache { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,55 @@
using System;
using System.ComponentModel.Composition;
using System.IO;
using GitHub.Services;
using NLog;
using Rothko;
namespace GitHub
{
[Export(typeof(IBrowser))]
public class Browser : IBrowser
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
readonly IProcessStarter processManager;
readonly IEnvironment environment;
[ImportingConstructor]
public Browser(IProcessStarter processManager, IEnvironment environment)
{
this.processManager = processManager;
this.environment = environment;
}
public void OpenUrl(Uri url)
{
if (url == null)
{
log.Warn("Attempted to open a null URL");
return;
}
try
{
processManager.Start(url.ToString(), string.Empty);
return;
}
catch (Exception ex)
{
log.Warn("Opening URL in default browser failed", ex);
}
try
{
processManager.Start(
Path.Combine(environment.GetProgramFilesPath(), @"Internet Explorer", "iexplore.exe"),
url.ToString());
}
catch (Exception ex)
{
log.Error("Really can't open the URL, even in IE", ex);
}
}
}
}

Просмотреть файл

@ -0,0 +1,70 @@
using System;
using System.ComponentModel.Composition;
using System.Net;
using System.Net.Http;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using GitHub.Models;
using GitHub.Services;
using Octokit;
using Octokit.Internal;
namespace GitHub.Helpers
{
[Export(typeof(IEnterpriseProbe))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class EnterpriseProbe : IEnterpriseProbe
{
static readonly Uri endPoint = new Uri("/site/sha", UriKind.Relative);
readonly ProductHeaderValue productHeader;
readonly IHttpClient httpClient;
[ImportingConstructor]
public EnterpriseProbe(IProgram program, IHttpClient httpClient)
{
productHeader = program.ProductHeader;
this.httpClient = httpClient;
}
public IObservable<EnterpriseProbeResult> Probe(Uri enterpriseBaseUrl)
{
var request = new Request
{
Method = HttpMethod.Get,
BaseAddress = enterpriseBaseUrl,
Endpoint = endPoint,
Timeout = TimeSpan.FromSeconds(3),
AllowAutoRedirect = false,
};
request.Headers.Add("User-Agent", productHeader.ToString());
return httpClient.Send<object>(request)
.ToObservable()
.Catch(Observable.Return<IResponse<object>>(null))
.Select(resp => resp == null
? EnterpriseProbeResult.Failed
: (resp.StatusCode == HttpStatusCode.OK
? EnterpriseProbeResult.Ok
: EnterpriseProbeResult.NotFound));
}
}
public enum EnterpriseProbeResult
{
/// <summary>
/// Yep! It's an Enterprise server
/// </summary>
Ok,
/// <summary>
/// Got a response from a server, but it wasn't an Enterprise server
/// </summary>
NotFound,
/// <summary>
/// Request timed out or DNS failed. So it's probably the case it's not an enterprise server but
/// we can't know for sure.
/// </summary>
Failed
}
}

Просмотреть файл

@ -0,0 +1,43 @@
using System.Globalization;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Akavache;
using GitHub.Extensions.Reactive;
using NLog;
using Octokit;
using LogManager = NLog.LogManager;
namespace GitHub
{
public class GitHubCredentialStore : ICredentialStore
{
static readonly Logger log = LogManager.GetCurrentClassLogger();
readonly HostAddress hostAddress;
readonly ILoginCache loginCache;
public GitHubCredentialStore(HostAddress hostAddress, ILoginCache loginCache)
{
this.hostAddress = hostAddress;
this.loginCache = loginCache;
}
public async Task<Credentials> GetCredentials()
{
return await loginCache.GetLoginAsync(hostAddress)
.CatchNonCritical(Observable.Return(LoginCache.EmptyLoginInfo))
.Select(CreateFromLoginInfo);
}
Credentials CreateFromLoginInfo(LoginInfo loginInfo)
{
if (loginInfo == LoginCache.EmptyLoginInfo)
{
log.Debug(CultureInfo.InvariantCulture, "Could not retrieve login info from the secure cache '{0}'",
hostAddress.CredentialCacheKeyHost);
return Credentials.Anonymous;
}
return new Credentials(loginInfo.UserName, loginInfo.Password);
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
using GitHub.Models;
namespace GitHub
{
public interface IAccountFactory
{
IAccount CreateAccount(
IRepositoryHost repositoryHost,
Octokit.User user);
IAccount CreateAccount(
IRepositoryHost repositoryHost,
Octokit.Organization organization);
}
}

Просмотреть файл

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using GitHub.Authentication;
using Octokit;
namespace GitHub
{
public interface IApiClient
{
HostAddress HostAddress { get; }
IObservable<SshKey> AddSshKey(SshKey newKey);
IObservable<Repository> CreateRepository(Repository repo, string login, bool isUser);
IObservable<SshKey> GetSshKeys();
IObservable<User> GetUser();
IObservable<User> GetAllUsersForAllOrganizations();
IObservable<Organization> GetOrganization(string login);
IObservable<Organization> GetOrganizations();
IObservable<User> GetMembersOfOrganization(string organizationName);
IObservable<Repository> GetRepository(string owner, string name);
IObservable<IEnumerable<Repository>> GetUserRepositories(int currentUserId);
IObservable<Repository> GetCurrentUserRepositoriesStreamed();
IObservable<Repository> GetOrganizationRepositoriesStreamed(string login);
IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(
Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHander = null,
bool useOldScopes = false);
IObservable<Authorization> GetOrCreateApplicationAuthenticationCode(
string authenticationCode,
bool useOldScopes = false);
IObservable<IReadOnlyList<EmailAddress>> GetEmails();
ITwoFactorChallengeHandler TwoFactorChallengeHandler { get; }
}
}

Просмотреть файл

@ -0,0 +1,7 @@
namespace GitHub
{
public interface IApiClientFactory
{
IApiClient Create(HostAddress hostAddress);
}
}

Просмотреть файл

@ -0,0 +1,9 @@
using System;
namespace GitHub.Services
{
public interface IBrowser
{
void OpenUrl(Uri url);
}
}

Просмотреть файл

@ -0,0 +1,10 @@
using System;
using GitHub.Helpers;
namespace GitHub.Services
{
public interface IEnterpriseProbe
{
IObservable<EnterpriseProbeResult> Probe(Uri enterpriseBaseUrl);
}
}

Просмотреть файл

@ -0,0 +1,9 @@
using GitHub.Models;
namespace GitHub
{
public interface IRepositoryHostFactory
{
IRepositoryHost Create(HostAddress hostAddress);
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System.ComponentModel.Composition;
using GitHub.Models;
namespace GitHub
{
[Export(typeof(IRepositoryHostFactory))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class RepositoryHostFactory : IRepositoryHostFactory
{
readonly IApiClientFactory apiClientFactory;
readonly IHostCacheFactory hostCacheFactory;
readonly ILoginCache loginCache;
readonly IAccountFactory accountFactory;
[ImportingConstructor]
public RepositoryHostFactory(
IApiClientFactory apiClientFactory,
IHostCacheFactory hostCacheFactory,
ILoginCache loginCache,
IAccountFactory accountFactory)
{
this.apiClientFactory = apiClientFactory;
this.hostCacheFactory = hostCacheFactory;
this.loginCache = loginCache;
this.accountFactory = accountFactory;
}
public IRepositoryHost Create(HostAddress hostAddress)
{
var apiClient = apiClientFactory.Create(hostAddress);
var hostCache = hostCacheFactory.Create(hostAddress);
return new RepositoryHost(apiClient, hostCache, loginCache, accountFactory);
}
}
}

Просмотреть файл

@ -0,0 +1,69 @@
using System;
using System.Reactive.Linq;
using System.Windows;
using ReactiveUI;
namespace GitHub.Services
{
public enum ErrorType
{
BranchCreateFailed,
BranchDeleteFailed,
BranchCheckoutFailed,
BranchPublishFailed,
BranchUnpublishFailed,
BranchListFailed,
CannotDropFolder,
ClipboardFailed,
ClonedFailed,
CloneFailedNotLoggedIn,
CommitCreateFailed,
CommitRevertFailed,
CommitUndoFailed,
CommitFilesLoadFailed,
DeleteEnterpriseServerFailed,
DiscardFileChangesFailed,
DiscardAllChangesFailed,
IgnoreFileFailed,
EnterpriseConnectFailed,
GitExtractionFailed,
GettingHeadFailed,
LaunchEnterpriseConnectionFailed,
LoginFailed,
LogFileError,
RepoCorrupted,
RepoDirectoryAlreadyExists,
RepoCreationFailed,
RepoCreationOnGitHubFailed,
RepoCreationAsPrivateNotAvailableForFreePlan,
RepoExistsOnDisk,
RepoExistsForUser,
RepoExistsInOrganization,
RepositoryNotFoundOnDisk,
SyncFailed,
ShellFailed,
CustomShellFailed,
WorkingDirectoryDoesNotExist,
MergeFailed,
PowerShellNotFound,
LoadingCommitsFailed,
LoadingWorkingDirectoryFailed,
SaveRepositorySettingsFailed,
MenuActionFailed,
Global,
RefreshFailed
}
public static class StandardUserErrors
{
public static IObservable<RecoveryOptionResult> ShowUserErrorMessage(
this Exception ex, ErrorType errorType, params object[] messageArgs)
{
// TODO: Fix this. This is just placeholder logic. -@haacked
Console.WriteLine(errorType);
Console.WriteLine(messageArgs);
MessageBox.Show(ex.Message);
return Observable.Return(new RecoveryOptionResult());
}
}
}

Просмотреть файл

@ -0,0 +1,440 @@
using System;
using System.ComponentModel.Composition;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using GitHub.Authentication;
using GitHub.Extensions;
using GitHub.Helpers;
using GitHub.Info;
using GitHub.Models;
using GitHub.Services;
using GitHub.Validation;
using NullGuard;
using ReactiveUI;
namespace GitHub.ViewModels
{
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
[Export(typeof(LoginControlViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class LoginControlViewModel : ReactiveValidatableObject, IDisposable
{
string enterpriseUrl;
readonly ObservableAsPropertyHelper<bool> isLoggingInToEnterprise;
readonly ObservableAsPropertyHelper<bool> isLoginInProgress;
readonly Subject<AuthenticationResult> authenticationResults;
readonly ObservableAsPropertyHelper<string> loginButtonText;
bool loginFailed;
string loginFailedText;
readonly ObservableAsPropertyHelper<LoginMode> loginMode;
readonly ObservableAsPropertyHelper<LoginTarget> loginTarget;
readonly ObservableAsPropertyHelper<VisualState> visualState;
string password;
string usernameOrEmail;
Uri enterpriseHostBaseUrl;
readonly Lazy<IEnterpriseProbe> lazyEnterpriseProbe;
const string notEnterpriseServerError = "Not an Enterprise server. Please enter an Enterprise URL";
readonly ObservableAsPropertyHelper<Uri> forgotPasswordUrl;
[SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "It's Rx baby")]
[ImportingConstructor]
public LoginControlViewModel(
IServiceProvider serviceProvider,
IRepositoryHosts repositoryHosts,
IBrowser browser,
Lazy<IEnterpriseProbe> enterpriseProbe) : base(serviceProvider)
{
RepositoryHosts = repositoryHosts;
lazyEnterpriseProbe = enterpriseProbe;
var canLogin = this.WhenAny(x => x.IsValid, x => x.Value);
this.WhenAny(x => x.LoginFailed, x => x.Value ? "Try again" : "Log in")
.ToProperty(this, x => x.LoginButtonText, out loginButtonText);
LoginPrefix = "Log in";
LoginFailedText = "Log in failed";
LoginCommand = ReactiveCommand.CreateAsyncObservable(canLogin,
x => Validate()
? LogIn()
: Observable.Return(AuthenticationResult.CredentialFailure));
LoginCommand.IsExecuting
.ToProperty(this, vm => vm.IsLoginInProgress, out isLoginInProgress);
CancelCommand = ReactiveCommand.Create(Observable.Return(true));
SignupCommand = ReactiveCommand.Create(Observable.Return(true));
SignupCommand.Subscribe(_ => browser.OpenUrl(GitHubUrls.Plans));
LearnMoreCommand = ReactiveCommand.Create(Observable.Return(true));
LearnMoreCommand.Subscribe(_ => browser.OpenUrl(GitHubUrls.GitHubEnterpriseWeb));
// Whenever a host logs on or off we re-evaluate this. If there are no logged on hosts (local excluded)
// then the user may log on to either .com or an enterprise instance. If there's already a logged on host
// for .com then the user may only log on to an enterprise instance and vice versa.
loginMode = Observable.CombineLatest(
this.WhenAny(x => x.RepositoryHosts.GitHubHost.IsLoggedIn, x => x.Value),
this.WhenAny(x => x.RepositoryHosts.EnterpriseHost.IsLoggedIn, x => x.Value),
GetLoginModeFromLoggedInRemoteHosts)
.ToProperty(this, x => x.LoginMode);
var canSwitchTargets = this.WhenAny(
x => x.IsLoginInProgress,
y => y.LoginMode,
(x, y) => new { IsLoginInProgress = x.Value, LoginMode = y.Value }
)
.Select(x => !x.IsLoginInProgress && x.LoginMode == LoginMode.DotComOrEnterprise);
ShowDotComLoginCommand = ReactiveCommand.Create(canSwitchTargets);
ShowEnterpriseLoginCommand = ReactiveCommand.Create(canSwitchTargets);
Observable.Merge(
this.WhenAny(x => x.LoginMode, x => x.Value),
ShowEnterpriseLoginCommand.Select(_ => LoginMode.EnterpriseOnly),
ShowDotComLoginCommand.Select(_ => LoginMode.DotComOnly),
CancelCommand.Select(_ => LoginMode))
.Where(x =>
x == LoginMode.DotComOnly
|| x == LoginMode.EnterpriseOnly
|| x == LoginMode.DotComOrEnterprise)
.Select(x => x == LoginMode.EnterpriseOnly ? LoginTarget.Enterprise : LoginTarget.DotCom)
.ToProperty(this, x => x.LoginTarget, out loginTarget);
Observable.Merge(
ShowDotComLoginCommand,
ShowEnterpriseLoginCommand)
.Subscribe(_ =>
{
UsernameOrEmail = "";
Password = "";
ResetValidation();
});
this.WhenAny(
x => x.LoginMode,
x => x.LoginTarget,
(x, y) => new {Mode = x.Value, Target = y.Value})
.Select(x => GetStateNameFromModeAndTarget(x.Mode, x.Target))
.ToProperty(this, x => x.VisualState, out visualState);
this.WhenAny(
x => x.LoginTarget,
x => x.Value == LoginTarget.Enterprise)
.ToProperty(this, x => x.IsLoggingInToEnterprise, out isLoggingInToEnterprise);
authenticationResults = new Subject<AuthenticationResult>();
this.WhenAny(
x => x.EnterpriseHostBaseUrl,
x => x.LoginTarget,
(x, y) => new { enterpriseBaseUrl = x.Value, loginTarget = y.Value })
.Select(x => GetForgotPasswordUrl(x.enterpriseBaseUrl, x.loginTarget))
.ToProperty(this, x => x.ForgotPasswordUrl, out forgotPasswordUrl);
ForgotPasswordCommand = ReactiveCommand.Create();
ForgotPasswordCommand.Subscribe(_ => browser.OpenUrl(ForgotPasswordUrl));
}
public ReactiveCommand<object> CancelCommand { get; private set; }
[ValidateIf("IsLoggingInToEnterprise")]
[Required(ErrorMessage = "Please enter an Enterprise URL")]
[AllowNull]
public string EnterpriseUrl
{
get { return enterpriseUrl; }
set { this.RaiseAndSetIfChanged(ref enterpriseUrl, value); }
}
public ReactiveCommand<object> ForgotPasswordCommand { get; private set; }
public bool IsLoggingInToEnterprise
{
get { return isLoggingInToEnterprise.Value; }
}
public bool IsLoginInProgress
{
get { return isLoginInProgress.Value; }
}
public IObservable<AuthenticationResult> AuthenticationResults { get { return authenticationResults; } }
public string LoginButtonText
{
get { return loginButtonText.Value; }
}
public ReactiveCommand<AuthenticationResult> LoginCommand { get; private set; }
public bool LoginFailed
{
get { return loginFailed; }
set { this.RaiseAndSetIfChanged(ref loginFailed, value); }
}
public string LoginFailedText
{
get { return loginFailedText; }
private set { this.RaiseAndSetIfChanged(ref loginFailedText, value); }
}
public LoginMode LoginMode
{
get { return loginMode.Value; }
}
public string LoginPrefix { get; set; }
public LoginTarget LoginTarget
{
get { return loginTarget.Value; }
}
public VisualState VisualState
{
get { return visualState.Value; }
}
[AllowNull]
public string Password
{
[return: AllowNull]
get { return password; }
set { this.RaiseAndSetIfChanged(ref password, value); }
}
public Uri ForgotPasswordUrl
{
get { return forgotPasswordUrl.Value; }
}
Uri EnterpriseHostBaseUrl
{
get { return enterpriseHostBaseUrl; }
set { this.RaiseAndSetIfChanged(ref enterpriseHostBaseUrl, value); }
}
// HACKETY HACK!
// Because #Bind() doesn't yet set up validation, we must use XAML bindings for username and password.
// But, because our SecurePasswordBox manipulates base.Text, it doesn't work with XAML binding.
// (It binds the password mask, not the password.)
// So, this property is a "black hole" to point the XAML binding to so validation works.
// And the actual password is bound to #Password via #Bind(). Ugly? Yep.
[Required(ErrorMessage = "Please enter your password")]
public string PasswordNoOp { get; set; }
protected IRepositoryHosts RepositoryHosts { get; private set; }
public ReactiveCommand<object> ShowDotComLoginCommand { get; set; }
public ReactiveCommand<object> ShowEnterpriseLoginCommand { get; set; }
public ReactiveCommand<object> SignupCommand { get; private set; }
public ReactiveCommand<object> LearnMoreCommand { get; private set; }
[Required(ErrorMessage = "Please enter your username or email address")]
[AllowNull]
public string UsernameOrEmail
{
[return: AllowNull]
get { return usernameOrEmail; }
set { this.RaiseAndSetIfChanged(ref usernameOrEmail, value); }
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
authenticationResults.OnCompleted();
authenticationResults.Dispose();
}
protected IObservable<AuthenticationResult> DoEnterpriseLogIn()
{
Uri uri;
if (EnterpriseUrl.IsNullOrEmpty() || !Uri.TryCreate(EnterpriseUrl, UriKind.Absolute, out uri))
{
LoginFailed = true;
SetErrorMessage("EnterpriseUrl", "Please enter a valid Enterprise URL");
return Observable.Return(AuthenticationResult.CredentialFailure);
}
var enterpriseHostAddress = HostAddress.Create(uri);
if (enterpriseHostAddress == HostAddress.GitHubDotComHostAddress)
{
LoginFailed = true;
SetErrorMessage("EnterpriseUrl", notEnterpriseServerError);
return Observable.Return(AuthenticationResult.CredentialFailure);
}
var enterpriseProbe = lazyEnterpriseProbe.Value;
// Make a test request to /site/sha to make sure the provided URL points to a GitHub site:
return enterpriseProbe.Probe(enterpriseHostAddress.WebUri)
.SelectMany(result => HandleEnterpriseProbeResult(result, enterpriseHostAddress));
}
IObservable<AuthenticationResult> HandleEnterpriseProbeResult(
EnterpriseProbeResult result,
HostAddress enterpriseHostAddress)
{
LoginFailed = result != EnterpriseProbeResult.Ok;
// Something went wrong with the test request, like it doesn't exist or timed out:
if (result == EnterpriseProbeResult.Failed)
{
SetErrorMessage("EnterpriseUrl", "Failed to connect to the URL.");
return Observable.Return(AuthenticationResult.CredentialFailure);
}
// The test request didn't fail, but it didn't return 200, so it's not a GitHub site:
if (result == EnterpriseProbeResult.NotFound)
{
SetErrorMessage("EnterpriseUrl", notEnterpriseServerError);
return Observable.Return(AuthenticationResult.CredentialFailure);
}
EnterpriseHostBaseUrl = enterpriseHostAddress.WebUri;
var host = RepositoryHosts.RepositoryHostFactory.Create(enterpriseHostAddress);
return DoLogIn(host)
.Do(authenticationResult =>
{
if (authenticationResult.IsSuccess())
RepositoryHosts.EnterpriseHost = host;
});
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
protected IObservable<AuthenticationResult> DoLogIn(IRepositoryHost host)
{
LoginFailed = false;
if (usernameOrEmail.IsNullOrEmpty() || password.IsNullOrEmpty())
{
LoginFailed = true;
Password = "";
return Observable.Return(AuthenticationResult.CredentialFailure);
}
return host.LogIn(usernameOrEmail, password)
.ObserveOn(RxApp.MainThreadScheduler)
.Catch<AuthenticationResult, Exception>(ex =>
{
LoginFailed = true;
return Observable.Throw<AuthenticationResult>(ex);
})
.Do(result =>
{
authenticationResults.OnNext(result);
if (result.IsSuccess())
return;
LoginFailed = true;
LoginFailedText = String.Format(
CultureInfo.InvariantCulture,
"{0} failed",
(result == AuthenticationResult.CredentialFailure ?
LoginPrefix :
"Two-factor authentication"));
})
.Finally(() =>
{
Password = "";
ResetValidation();
})
.Multicast(new AsyncSubject<AuthenticationResult>()).RefCount();
}
private static LoginMode GetLoginModeFromLoggedInRemoteHosts(
bool gitHubHostLoggedIn,
bool enterpriseHostLoggedIn)
{
return gitHubHostLoggedIn
? enterpriseHostLoggedIn
? LoginMode.None
: LoginMode.EnterpriseOnly
: enterpriseHostLoggedIn // And Not logged into GitHub
? LoginMode.DotComOnly
: LoginMode.DotComOrEnterprise;
}
public virtual IObservable<AuthenticationResult> LogIn()
{
return LoginTarget == LoginTarget.Enterprise
? DoEnterpriseLogIn()
: DoLogIn(RepositoryHosts.GitHubHost);
}
public void Reset()
{
LoginPrefix = "Log in";
LoginFailed = false;
EnterpriseUrl = "";
UsernameOrEmail = "";
Password = "";
ResetValidation();
}
static Uri GetForgotPasswordUrl(Uri validEnterpriseHostBaseUrl, LoginTarget currentLoginTarget)
{
var baseUrl = currentLoginTarget == LoginTarget.DotCom || validEnterpriseHostBaseUrl == null
? HostAddress.GitHubDotComHostAddress.WebUri
: validEnterpriseHostBaseUrl;
return new Uri(baseUrl, GitHubUrls.ForgotPasswordPath);
}
private static VisualState GetStateNameFromModeAndTarget(LoginMode loginMode, LoginTarget loginTarget)
{
if (loginMode == LoginMode.DotComOnly || loginMode == LoginMode.EnterpriseOnly)
return (VisualState)loginMode;
switch (loginTarget)
{
case LoginTarget.DotCom:
case LoginTarget.Enterprise:
return (VisualState)loginTarget;
}
return VisualState.DotCom;
}
}
public enum LoginMode
{
None = 0,
DotComOrEnterprise,
DotComOnly = 3,
EnterpriseOnly = 4,
}
public enum LoginTarget
{
None = 0,
DotCom = 1,
Enterprise = 2,
}
public enum VisualState
{
None = 0,
DotCom = 1,
Enterprise = 2,
DotComOnly = 3,
EnterpriseOnly = 4
}
}

Просмотреть файл

@ -0,0 +1,141 @@
using System;
using System.ComponentModel.Composition;
using System.ComponentModel.DataAnnotations;
using System.Reactive.Linq;
using GitHub.Authentication;
using GitHub.Services;
using GitHub.Validation;
using NullGuard;
using Octokit;
using ReactiveUI;
namespace GitHub.ViewModels
{
[Export(typeof(TwoFactorDialogViewModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class TwoFactorDialogViewModel : ReactiveValidatableObject
{
bool isAuthenticationCodeSent;
string authenticationCode;
TwoFactorType twoFactorType;
readonly ObservableAsPropertyHelper<string> description;
readonly ObservableAsPropertyHelper<bool> isShowing;
readonly ObservableAsPropertyHelper<bool> isSms;
[ImportingConstructor]
public TwoFactorDialogViewModel(IBrowser browser, IServiceProvider serviceProvider) : base(serviceProvider)
{
OkCommand = ReactiveCommand.Create(this.WhenAny(
x => x.IsValid,
x => x.AuthenticationCode,
(valid, y) => valid.Value && (String.IsNullOrEmpty(y.Value) || (y.Value != null && y.Value.Length == 6))));
CancelCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
ShowHelpCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
//ShowHelpCommand.Subscribe(x => browser.OpenUrl(twoFactorHelpUri));
ResendCodeCommand = new ReactiveCommand<RecoveryOptionResult>(Observable.Return(true), _ => null);
description = this.WhenAny(x => x.TwoFactorType, x => x.Value)
.Select(type =>
{
switch (type)
{
case TwoFactorType.Sms:
return "We sent you a message via SMS with your authentication code.";
case TwoFactorType.AuthenticatorApp:
return "Open the two-factor authentication app on your device to view your " +
"authentication code.";
case TwoFactorType.Unknown:
return "Enter a login authentication code here";
default:
return null;
}
})
.ToProperty(this, x => x.Description);
isShowing = this.WhenAny(x => x.TwoFactorType, x => x.Value)
.Select(factorType => factorType != TwoFactorType.None)
.ToProperty(this, x => x.IsShowing);
isSms = this.WhenAny(x => x.TwoFactorType, x => x.Value)
.Select(factorType => factorType == TwoFactorType.Sms)
.ToProperty(this, x => x.IsSms);
}
public TwoFactorType TwoFactorType
{
get { return twoFactorType; }
private set { this.RaiseAndSetIfChanged(ref twoFactorType, value); }
}
public bool IsShowing
{
get { return isShowing.Value; }
}
public bool IsSms
{
get { return isSms.Value; }
}
public bool IsAuthenticationCodeSent
{
get { return isAuthenticationCodeSent; }
private set { this.RaiseAndSetIfChanged(ref isAuthenticationCodeSent, value); }
}
public string Description
{
[return: AllowNull]
get { return description.Value; }
}
[Required(ErrorMessage = "Please enter your authentication code")]
[RegularExpression(@"\d+", ErrorMessage = "Authentication code must only contain numbers")]
[AllowNull]
public string AuthenticationCode
{
[return: AllowNull]
get { return authenticationCode; }
set { this.RaiseAndSetIfChanged(ref authenticationCode, value); }
}
public ReactiveCommand<object> OkCommand { get; private set; }
public ReactiveCommand<RecoveryOptionResult> CancelCommand { get; private set; }
public ReactiveCommand<RecoveryOptionResult> ShowHelpCommand { get; private set; }
public ReactiveCommand<RecoveryOptionResult> ResendCodeCommand { get; private set; }
public IObservable<RecoveryOptionResult> Show(TwoFactorRequiredUserError error)
{
TwoFactorType = error.TwoFactorType;
var ok = OkCommand
.Where(x => Validate())
.Select(_ => AuthenticationCode == null
? RecoveryOptionResult.CancelOperation
: RecoveryOptionResult.RetryOperation)
.Do(_ => error.ChallengeResult = AuthenticationCode != null
? new TwoFactorChallengeResult(AuthenticationCode)
: null);
var cancel = CancelCommand.Select(_ => RecoveryOptionResult.CancelOperation);
var resend = ResendCodeCommand.Select(_ => RecoveryOptionResult.RetryOperation)
.Do(_ => error.ChallengeResult = TwoFactorChallengeResult.RequestResendCode);
return Observable.Merge(ok, cancel, resend)
.Take(1)
.Do(_ =>
{
bool authenticationCodeSent = error.ChallengeResult == TwoFactorChallengeResult.RequestResendCode;
if (!authenticationCodeSent)
{
TwoFactorType = TwoFactorType.None;
}
IsAuthenticationCodeSent = authenticationCodeSent;
})
.Finally(() =>
{
AuthenticationCode = null;
//TODO: ResetValidation();
});
}
}
}

19
src/GitHub.App/app.config Normal file
Просмотреть файл

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Reactive.Linq" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.2.5.0" newVersion="2.2.5.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Просмотреть файл

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="akavache" version="4.0.3.2" targetFramework="net45" />
<package id="akavache.core" version="4.0.3.2" targetFramework="net45" />
<package id="akavache.sqlite3" version="4.0.3.2" targetFramework="net45" />
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
<package id="Newtonsoft.Json" version="6.0.4" targetFramework="net45" />
<package id="NLog" version="3.1.0.0" targetFramework="net45" />
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
<package id="Octokit" version="0.4.1" targetFramework="net45" />
<package id="Octokit.Reactive" version="0.4.1" targetFramework="net45" />
<package id="reactiveui" version="6.0.6" targetFramework="net45" />
<package id="reactiveui-core" version="6.0.6" targetFramework="net45" />
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
<package id="Rx-Main" version="2.2.5" targetFramework="net45" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net45" />
<package id="Rx-XAML" version="2.2.5" targetFramework="net45" />
<package id="Splat" version="1.4.2.1" targetFramework="net45" />
<package id="SQLitePCL.raw_basic" version="0.5.0" targetFramework="net45" />
</packages>

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<NullGuard />
</Weavers>

Просмотреть файл

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6559E128-8B40-49A5-85A8-05565ED0C7E3}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GitHub.Extensions.Reactive</RootNamespace>
<AssemblyName>GitHub.Extensions.Reactive</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>760f38ad</NuGetPackageImportStamp>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="NullGuard">
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Interfaces">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Linq">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="ObservableExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
<Name>GitHub.Extensions</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Просмотреть файл

@ -0,0 +1,68 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace GitHub.Extensions.Reactive
{
public static class ObservableExtensions
{
public static IObservable<T> WhereNotNull<T>(this IObservable<T> observable) where T : class
{
return observable.Where(item => item != null);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
Justification = "Rx has your back.")]
public static IObservable<T> PublishAsync<T>(this IObservable<T> observable)
{
var ret = observable.Multicast(new AsyncSubject<T>());
ret.Connect();
return ret;
}
/// <summary>
/// Used in cases where we only care when an observable completes and not what the observed values are.
/// This is commonly used by some of our "Refresh" methods.
/// </summary>
public static IObservable<Unit> AsCompletion<T>(this IObservable<T> observable)
{
return observable
.SelectMany(_ => Observable.Empty<Unit>())
.Concat(Observable.Return(Unit.Default));
}
/// <summary>
/// Helper method to transform an IObservable{T} to IObservable{Unit}.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="observable"></param>
/// <returns></returns>
public static IObservable<Unit> SelectUnit<T>(this IObservable<T> observable)
{
return observable.Select(_ => Unit.Default);
}
public static IObservable<TSource> CatchNonCritical<TSource>(
this IObservable<TSource> first,
Func<Exception, IObservable<TSource>> second)
{
return first.Catch<TSource, Exception>(e =>
{
if (!e.IsCriticalException())
return second(e);
return Observable.Throw<TSource>(e);
});
}
public static IObservable<TSource> CatchNonCritical<TSource>(
this IObservable<TSource> first,
IObservable<TSource> second)
{
return first.CatchNonCritical(e => second);
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.Extensions.Reactive")]
[assembly: AssemblyDescription("Provides useful Rx based extension and utility methods common to the needs of GitHub applications")]
[assembly: Guid("73e49b11-0bd0-4984-b9a8-3e7edceb071e")]

Просмотреть файл

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
</packages>

Просмотреть файл

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace GitHub
{
public static class EnumerableExtensions
{
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source) action(item);
}
public static IEnumerable<TSource> Except<TSource>(
this IEnumerable<TSource> enumerable,
IEnumerable<TSource> second,
Func<TSource, TSource, bool> comparer)
{
return enumerable.Except(second, new LambdaComparer<TSource>(comparer));
}
class LambdaComparer<T> : IEqualityComparer<T>
{
private readonly Func<T, T, bool> lambdaComparer;
private readonly Func<T, int> lambdaHash;
public LambdaComparer(Func<T, T, bool> lambdaComparer) :
this(lambdaComparer, o => 0)
{
}
LambdaComparer(Func<T, T, bool> lambdaComparer, Func<T, int> lambdaHash)
{
this.lambdaComparer = lambdaComparer;
this.lambdaHash = lambdaHash;
}
public bool Equals(T x, T y)
{
return lambdaComparer(x, y);
}
public int GetHashCode(T obj)
{
return lambdaHash(obj);
}
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
using System;
using System.Threading;
namespace GitHub.Extensions
{
public static class ExceptionExtensions
{
/// <summary>
/// Represents exceptions we should never attempt to catch and ignore.
/// </summary>
/// <param name="exception">The exception being thrown.</param>
/// <returns></returns>
public static bool IsCriticalException(this Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException("exception");
}
return exception.IsFatalException()
|| exception is AppDomainUnloadedException
|| exception is BadImageFormatException
|| exception is CannotUnloadAppDomainException
|| exception is InvalidProgramException
|| exception is NullReferenceException
|| exception is ArgumentException;
}
/// <summary>
/// Represents exceptions we should never attempt to catch and ignore when executing third party plugin code.
/// This is not as extensive as a proposed IsCriticalException method that I want to write for our own code.
/// </summary>
/// <param name="exception">The exception being thrown.</param>
/// <returns></returns>
public static bool IsFatalException(this Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException("exception");
}
return exception is StackOverflowException
|| exception is OutOfMemoryException
|| exception is ThreadAbortException
|| exception is AccessViolationException;
}
}
}

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<NullGuard />
</Weavers>

Просмотреть файл

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6AFE2E2D-6DB0-4430-A2EA-F5F5388D2F78}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GitHub.Extensions</RootNamespace>
<AssemblyName>GitHub.Extensions</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>d4e00d5e</NuGetPackageImportStamp>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="NullGuard">
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="EnumerableExtensions.cs" />
<Compile Include="ExceptionExtensions.cs" />
<Compile Include="Guard.cs" />
<Compile Include="ReflectionExtensions.cs" />
<Compile Include="StringExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UriExtensions.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Просмотреть файл

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
namespace GitHub.Extensions
{
public static class Guard
{
public static void ArgumentNonNegative(int value, string name)
{
if (value > -1) return;
var message = String.Format(CultureInfo.InvariantCulture, "The value for '{0}' must be non-negative", name);
#if DEBUG
if (!InUnitTestRunner())
{
Debug.Fail(message);
}
#endif
throw new ArgumentException(message, name);
}
/// <summary>
/// Checks a string argument to ensure it isn't null or empty.
/// </summary>
/// <param name = "value">The argument value to check.</param>
/// <param name = "name">The name of the argument.</param>
public static void ArgumentNotEmptyString(string value, string name)
{
if (value.Length > 0) return;
string message = String.Format(CultureInfo.InvariantCulture, "The value for '{0}' must not be empty", name);
#if DEBUG
if (!InUnitTestRunner())
{
Debug.Fail(message);
}
#endif
throw new ArgumentException(message, name);
}
public static void ArgumentInRange(int value, int minValue, string name)
{
if (value >= minValue) return;
string message = String.Format(CultureInfo.InvariantCulture,
"The value '{0}' for '{1}' must be greater than or equal to '{2}'",
value,
name,
minValue);
#if DEBUG
if (!InUnitTestRunner())
{
Debug.Fail(message);
}
#endif
throw new ArgumentOutOfRangeException(name, message);
}
public static void ArgumentInRange(int value, int minValue, int maxValue, string name)
{
if (value >= minValue && value <= maxValue) return;
string message = String.Format(CultureInfo.InvariantCulture,
"The value '{0}' for '{1}' must be greater than or equal to '{2}' and less than or equal to '{3}'",
value,
name,
minValue,
maxValue);
#if DEBUG
if (!InUnitTestRunner())
{
Debug.Fail(message);
}
#endif
throw new ArgumentOutOfRangeException(name, message);
}
// Borrowed from Splat.
static bool InUnitTestRunner()
{
var testAssemblies = new[] {
"CSUNIT",
"NUNIT",
"XUNIT",
"MBUNIT",
"PEX.",
"NBEHAVE",
};
try
{
return SearchForAssembly(testAssemblies);
}
catch (Exception)
{
return false;
}
}
static bool SearchForAssembly(IEnumerable<string> assemblyList)
{
return AppDomain.CurrentDomain.GetAssemblies()
.Any(x => assemblyList.Any(name => x.FullName.ToUpperInvariant().Contains(name)));
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.Extensions")]
[assembly: AssemblyDescription("Provides useful extension and utility methods common to the needs of GitHub applications")]
[assembly: Guid("3bf91177-3d16-425d-9c62-50a86cf26298")]

Просмотреть файл

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace GitHub.Extensions
{
public static class ReflectionExtensions
{
public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
return e.Types.Where(t => t != null);
}
}
public static bool HasInterface(this Type type, Type targetInterface)
{
if (targetInterface.IsAssignableFrom(type))
return true;
return type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == targetInterface);
}
}
}

Просмотреть файл

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using NullGuard;
namespace GitHub.Extensions
{
public static class StringExtensions
{
public static bool Contains(this string s, string expectedSubstring, StringComparison comparison)
{
return s.IndexOf(expectedSubstring, comparison) > -1;
}
public static bool ContainsAny(this string s, IEnumerable<char> characters)
{
return s.IndexOfAny(characters.ToArray()) > -1;
}
public static string DebugRepresentation([AllowNull]this string s)
{
s = s ?? "(null)";
return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", s);
}
public static bool IsNotNullOrEmpty(this string s)
{
return !string.IsNullOrEmpty(s);
}
public static bool IsNullOrEmpty([AllowNull]this string s)
{
return string.IsNullOrEmpty(s);
}
public static string ToNullIfEmpty([AllowNull]this string s)
{
return s.IsNullOrEmpty() ? null : s;
}
public static bool StartsWith([AllowNull]this string s, char c)
{
if (String.IsNullOrEmpty(s)) return false;
return s.First() == c;
}
public static string RightAfter([AllowNull]this string s, string search)
{
if (s == null) return null;
int lastIndex = s.IndexOf(search, StringComparison.OrdinalIgnoreCase);
if (lastIndex < 0)
return null;
return s.Substring(lastIndex + search.Length);
}
public static string RightAfterLast(this string s, string search)
{
if (s == null) return null;
int lastIndex = s.LastIndexOf(search, StringComparison.OrdinalIgnoreCase);
if (lastIndex < 0)
return null;
return s.Substring(lastIndex + search.Length);
}
public static string LeftBeforeLast([AllowNull]this string s, string search)
{
if (s == null) return null;
int lastIndex = s.LastIndexOf(search, StringComparison.OrdinalIgnoreCase);
if (lastIndex < 0)
return null;
return s.Substring(0, lastIndex);
}
// Returns a file name even if the path is FUBAR.
public static string ParseFileName([AllowNull]this string path)
{
if (path == null) return null;
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).RightAfterLast(Path.DirectorySeparatorChar + "");
}
// Returns the parent directory even if the path is FUBAR.
public static string ParseParentDirectory([AllowNull]this string path)
{
if (path == null) return null;
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).LeftBeforeLast(Path.DirectorySeparatorChar + "");
}
public static string EnsureStartsWith([AllowNull]this string s, char c)
{
if (s == null) return null;
return c + s.TrimStart(c);
}
// Ensures the string ends with the specified character.
public static string EnsureEndsWith([AllowNull]this string s, char c)
{
if (s == null) return null;
return s.TrimEnd(c) + c;
}
public static string NormalizePath([AllowNull]this string path)
{
if (String.IsNullOrEmpty(path)) return null;
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
public static string TrimEnd([AllowNull]this string s, string suffix)
{
if (s == null) return null;
if (!s.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
return s;
return s.Substring(0, s.Length - suffix.Length);
}
public static string RemoveSurroundingQuotes(this string s)
{
if (s.Length < 2)
return s;
var quoteCharacters = new[] { '"', '\'' };
char firstCharacter = s[0];
if (!quoteCharacters.Contains(firstCharacter))
return s;
if (firstCharacter != s[s.Length - 1])
return s;
return s.Substring(1, s.Length - 2);
}
public static Int32 ToInt32(this string s)
{
Int32 val;
return Int32.TryParse(s, out val) ? val : 0;
}
/// <summary>
/// Wrap a string to the specified length.
/// </summary>
/// <param name="text">The text string to wrap</param>
/// <param name="maxLength">The character length to wrap at</param>
/// <returns>A wrapped string using the platform's default newline character. This string will end in a newline.</returns>
public static string Wrap(this string text, int maxLength = 72)
{
if (text.Length == 0) return string.Empty;
var sb = new StringBuilder();
foreach (var unwrappedLine in text.Split(new[] { Environment.NewLine }, StringSplitOptions.None))
{
var line = new StringBuilder();
foreach (var word in unwrappedLine.Split(' '))
{
var needsLeadingSpace = line.Length > 0;
var extraLength = (needsLeadingSpace ? 1 : 0) + word.Length;
if (line.Length + extraLength > maxLength)
{
sb.AppendLine(line.ToString());
line.Clear();
needsLeadingSpace = false;
}
if (needsLeadingSpace)
line.Append(" ");
line.Append(word);
}
sb.AppendLine(line.ToString());
}
return sb.ToString();
}
public static Uri ToUriSafe(this string url)
{
Uri uri;
Uri.TryCreate(url, UriKind.Absolute, out uri);
return uri;
}
}
}

Просмотреть файл

@ -0,0 +1,44 @@
using System;
namespace GitHub.Extensions
{
public static class UriExtensions
{
public static bool IsHypertextTransferProtocol(this Uri uri)
{
return uri.Scheme == "http" || uri.Scheme == "https";
}
public static bool IsSameHost(this Uri uri, Uri compareUri)
{
return uri.Host.Equals(compareUri.Host, StringComparison.OrdinalIgnoreCase);
}
public static Uri WithAbsolutePath(this Uri uri, string absolutePath)
{
absolutePath = absolutePath.EnsureStartsWith('/');
return new Uri(uri, new Uri(absolutePath, UriKind.Relative));
}
public static Uri ToHttps(this Uri uri)
{
if (uri == null
|| uri.Scheme != Uri.UriSchemeHttp
|| (uri.Port != 80 && uri.Port != -1)
|| uri.IsLoopback)
return uri;
var uriBuilder = new UriBuilder(uri);
uriBuilder.Scheme = Uri.UriSchemeHttps;
// trick to keep uriBuilder from explicitly appending :80 to the HTTPS URI
uriBuilder.Port = -1;
return uriBuilder.Uri;
}
public static string ToUpperInvariantString(this Uri uri)
{
return uri == null ? "" : uri.ToString().ToUpperInvariant();
}
}
}

Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
<package id="Splat" version="1.4.2.1" targetFramework="net45" />
</packages>

Просмотреть файл

@ -0,0 +1,11 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="..\Assets\Controls\Validation\ValidationMessage.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Просмотреть файл

@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using GitHub.Extensions;
using ReactiveUI;
namespace GitHub.UI
{
public class ReactivePropertyValidationResult
{
public bool IsValid { get; private set; }
public ValidationStatus Status { get; private set; }
public string Message { get; private set; }
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "It is immutable")]
public static readonly ReactivePropertyValidationResult Success = new ReactivePropertyValidationResult(ValidationStatus.Valid);
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "It is immutable")]
public static readonly ReactivePropertyValidationResult Unvalidated = new ReactivePropertyValidationResult();
public ReactivePropertyValidationResult()
: this(ValidationStatus.Unvalidated, "")
{
}
public ReactivePropertyValidationResult(ValidationStatus validationStatus)
: this(validationStatus, "")
{
}
public ReactivePropertyValidationResult(ValidationStatus validationStatus, string message)
{
Status = validationStatus;
IsValid = validationStatus != ValidationStatus.Invalid;
Message = message;
}
}
public enum ValidationStatus
{
Unvalidated = 0,
Invalid = 1,
Valid = 2,
}
public abstract class ReactivePropertyValidator : ReactiveObject
{
public static ReactivePropertyValidator<TProp> For<TObj, TProp>(TObj This, Expression<Func<TObj, TProp>> property)
{
return new ReactivePropertyValidator<TObj, TProp>(This, property);
}
public abstract ReactivePropertyValidationResult ValidationResult { get; protected set; }
public abstract bool IsValidating { get; }
protected ReactivePropertyValidator()
{
}
public abstract Task ExecuteAsync();
public abstract Task ResetAsync();
}
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
public class ReactivePropertyValidator<TProp> : ReactivePropertyValidator
{
readonly ReactiveCommand<ReactivePropertyValidationResult> validateCommand;
ReactivePropertyValidationResult validationResult;
public override ReactivePropertyValidationResult ValidationResult
{
get { return validationResult; }
protected set { this.RaiseAndSetIfChanged(ref validationResult, value); }
}
public override Task ExecuteAsync()
{
return validateCommand.ExecuteAsyncTask(new ValidationParameter());
}
public override Task ResetAsync()
{
return validateCommand.ExecuteAsyncTask(new ValidationParameter { RequiresReset = true });
}
readonly List<Func<TProp, IObservable<ReactivePropertyValidationResult>>> validators =
new List<Func<TProp, IObservable<ReactivePropertyValidationResult>>>();
readonly ObservableAsPropertyHelper<bool> isValidating;
public override bool IsValidating
{
get { return isValidating.Value; }
}
public ReactivePropertyValidator(IObservable<TProp> signal)
{
validateCommand = ReactiveCommand.CreateAsyncObservable(param =>
{
var validationParams = (ValidationParameter)param;
if (validationParams.RequiresReset)
{
return Observable.Return(ReactivePropertyValidationResult.Unvalidated);
}
TProp value = validationParams.PropertyValue;
var currentValidators = validators.ToList();
// HEAR YE, HEAR YE
// This .ToList() is here to ignore changes to the validator collection,
// and thus avoid fantastically vague exceptions about
// "Collection was modified, enumeration operation may not execute"
// bubbling up to tear the application down
// Thus, the collection will be correct when the command executes,
// which should be fine until we need to do more complex validation
if (!currentValidators.Any())
return Observable.Return(ReactivePropertyValidationResult.Unvalidated);
return currentValidators.ToObservable()
.SelectMany(v => v(value))
.FirstOrDefaultAsync(x => x.Status == ValidationStatus.Invalid)
.Select(x => x == null ? ReactivePropertyValidationResult.Success : x);
});
isValidating = validateCommand.IsExecuting.ToProperty(this, x => x.IsValidating);
validateCommand.Subscribe(x => ValidationResult = x);
signal.Subscribe(x => validateCommand.Execute(new ValidationParameter { PropertyValue = x, RequiresReset = false }));
}
public ReactivePropertyValidator<TProp> IfTrue(Func<TProp, bool> predicate, string errorMessage)
{
return Add(predicate, errorMessage);
}
public ReactivePropertyValidator<TProp> IfFalse(Func<TProp, bool> predicate, string errorMessage)
{
return Add(x => !predicate(x), errorMessage);
}
ReactivePropertyValidator<TProp> Add(Func<TProp, bool> predicate, string errorMessage)
{
return Add(x => predicate(x) ? errorMessage : null);
}
public ReactivePropertyValidator<TProp> Add(Func<TProp, string> predicateWithMessage)
{
validators.Add(value => Observable.Defer(() => Observable.Return(Validate(value, predicateWithMessage))));
return this;
}
public ReactivePropertyValidator<TProp> IfTrueAsync(Func<TProp, IObservable<bool>> predicate, string errorMessage)
{
AddAsync(x => predicate(x).Select(result => result ? errorMessage : null));
return this;
}
public ReactivePropertyValidator<TProp> IfFalseAsync(Func<TProp, IObservable<bool>> predicate, string errorMessage)
{
AddAsync(x => predicate(x).Select(result => result ? null : errorMessage));
return this;
}
public ReactivePropertyValidator<TProp> AddAsync(Func<TProp, IObservable<string>> predicateWithMessage)
{
validators.Add(value => Observable.Defer(() =>
{
return predicateWithMessage(value)
.Select(result => String.IsNullOrEmpty(result)
? ReactivePropertyValidationResult.Success
: new ReactivePropertyValidationResult(ValidationStatus.Invalid, result));
}));
return this;
}
static ReactivePropertyValidationResult Validate(TProp value, Func<TProp, string> predicateWithMessage)
{
var result = predicateWithMessage(value);
if (String.IsNullOrEmpty(result))
return ReactivePropertyValidationResult.Success;
return new ReactivePropertyValidationResult(ValidationStatus.Invalid, result);
}
class ValidationParameter
{
public TProp PropertyValue { get; set; }
public bool RequiresReset { get; set; }
}
}
public class ReactivePropertyValidator<TObj, TProp> : ReactivePropertyValidator<TProp>
{
protected ReactivePropertyValidator()
: base(Observable.Empty<TProp>())
{
}
public ReactivePropertyValidator(TObj This, Expression<Func<TObj, TProp>> property)
: base(This.WhenAny(property, x => x.Value)) { }
}
public static class ReactivePropertyValidatorExtensions
{
public static ReactivePropertyValidator<string> IfMatch(this ReactivePropertyValidator<string> This, string pattern, string errorMessage)
{
var regex = new Regex(pattern);
return This.IfTrue(regex.IsMatch, errorMessage);
}
public static ReactivePropertyValidator<string> IfNotMatch(this ReactivePropertyValidator<string> This, string pattern, string errorMessage)
{
var regex = new Regex(pattern);
return This.IfFalse(regex.IsMatch, errorMessage);
}
public static ReactivePropertyValidator<string> IfNullOrEmpty(this ReactivePropertyValidator<string> This, string errorMessage)
{
return This.IfTrue(String.IsNullOrEmpty, errorMessage);
}
public static ReactivePropertyValidator<string> IfNotUri(this ReactivePropertyValidator<string> This, string errorMessage)
{
return This.IfFalse(s =>
{
Uri uri;
return Uri.TryCreate(s, UriKind.Absolute, out uri);
}, errorMessage);
}
public static ReactivePropertyValidator<string> IfSameAsHost(this ReactivePropertyValidator<string> This, Uri compareToHost, string errorMessage)
{
return This.IfTrue(s =>
{
Uri uri;
var isUri = Uri.TryCreate(s, UriKind.Absolute, out uri);
return isUri && uri.IsSameHost(compareToHost);
}, errorMessage);
}
public static ReactivePropertyValidator<string> IfContainsInvalidPathChars(this ReactivePropertyValidator<string> This, string errorMessage)
{
return This.IfTrue(str =>
{
// easiest check to make
if (str.ContainsAny(Path.GetInvalidPathChars()))
{
return true;
}
string driveLetter;
try
{
// if for whatever reason you don't have an absolute path
// hopefully you've remembered to use `IfPathNotRooted`
// in your validator
driveLetter = Path.GetPathRoot(str);
}
catch (ArgumentException)
{
// Path.GetPathRoot does some fun things
// around legal combinations of characters that we miss
// by simply checking against an array of legal characters
return true;
}
if (driveLetter == null)
{
return false;
}
// lastly, check each directory name doesn't contain
// any invalid filename characters
var foldersInPath = str.Substring(driveLetter.Length);
return foldersInPath.Split(new[] { '\\', '/' }, StringSplitOptions.None)
.Any(x => x.ContainsAny(Path.GetInvalidFileNameChars()));
}, errorMessage);
}
public static ReactivePropertyValidator<string> IfPathNotRooted(this ReactivePropertyValidator<string> This, string errorMessage)
{
return This.IfFalse(Path.IsPathRooted, errorMessage);
}
public static ReactivePropertyValidator<string> IfUncPath(this ReactivePropertyValidator<string> This, string errorMessage)
{
return This.IfTrue(str => str.StartsWith(@"\\", StringComparison.Ordinal), errorMessage);
}
}
}

Просмотреть файл

@ -0,0 +1,103 @@
using System;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using GitHub.Extensions.Reactive;
using ReactiveUI;
namespace GitHub.UI
{
public class ValidationMessage : UserControl
{
const double defaultTextChangeThrottle = 0.2;
public ValidationMessage()
{
this.WhenAny(x => x.ReactiveValidator.ValidationResult, x => x.Value)
.WhereNotNull()
.Subscribe(result =>
{
ShowError = result.IsValid == false;
Text = result.Message;
});
this.WhenAny(x => x.ValidatesControl, x => x.Value)
.WhereNotNull()
.Select(control =>
Observable.Merge(
control.Events().TextChanged
.Throttle(TimeSpan.FromSeconds(ShowError ? defaultTextChangeThrottle : TextChangeThrottle),
RxApp.MainThreadScheduler)
.Select(_ => ShowError),
control.Events().LostFocus
.Select(_ => ShowError),
control.Events().LostFocus
.Where(__ => string.IsNullOrEmpty(ValidatesControl.Text))
.Select(_ => false)))
.Switch()
.Subscribe(showError =>
{
IsShowingMessage = showError;
});
}
public static readonly DependencyProperty IsShowingMessageProperty = DependencyProperty.Register("IsShowingMessage", typeof(bool), typeof(ValidationMessage));
public bool IsShowingMessage
{
get { return (bool)GetValue(IsShowingMessageProperty); }
private set { SetValue(IsShowingMessageProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ValidationMessage));
public string Text
{
get { return (string)GetValue(TextProperty); }
private set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty ShowErrorProperty = DependencyProperty.Register("ShowError", typeof(bool), typeof(ValidationMessage));
public bool ShowError
{
get { return (bool)GetValue(ShowErrorProperty); }
set { SetValue(ShowErrorProperty, value); }
}
public static readonly DependencyProperty TextChangeThrottleProperty = DependencyProperty.Register("TextChangeThrottle", typeof(double), typeof(ValidationMessage), new PropertyMetadata(defaultTextChangeThrottle));
public double TextChangeThrottle
{
get { return (double)GetValue(TextChangeThrottleProperty); }
set { SetValue(TextChangeThrottleProperty, value); }
}
public static readonly DependencyProperty ValidatesControlProperty = DependencyProperty.Register("ValidatesControl", typeof(TextBox), typeof(ValidationMessage), new PropertyMetadata(default(TextBox)));
public TextBox ValidatesControl
{
get { return (TextBox)GetValue(ValidatesControlProperty); }
set { SetValue(ValidatesControlProperty, value); }
}
public static readonly DependencyProperty ReactiveValidatorProperty = DependencyProperty.Register("ReactiveValidator", typeof(ReactivePropertyValidator), typeof(ValidationMessage));
public ReactivePropertyValidator ReactiveValidator
{
get { return (ReactivePropertyValidator)GetValue(ReactiveValidatorProperty); }
set { SetValue(ReactiveValidatorProperty, value); }
}
public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(Octicon), typeof(ValidationMessage), new PropertyMetadata(Octicon.stop));
public Octicon Icon
{
get { return (Octicon) GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty FillProperty =
DependencyProperty.Register("Fill", typeof(Brush), typeof(ValidationMessage), new PropertyMetadata(new SolidColorBrush(Color.FromRgb(0xe7, 0x4c, 0x3c))));
public Brush Fill
{
get { return (Brush)GetValue(FillProperty); }
set { SetValue(FillProperty, value); }
}
}
}

Просмотреть файл

@ -0,0 +1,81 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GitHub.UI"
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI">
<Style TargetType="{x:Type local:ValidationMessage}">
<Setter Property="Padding" Value="2,6" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ValidationMessage}">
<Grid x:Name="grid" Visibility="Collapsed" Height="0" Opacity="0" Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Viewbox Width="16" Height="16" Grid.Column="0"
Visibility="{TemplateBinding ShowError, Converter={ui:BooleanToVisibilityConverter}}">
<ui:OcticonPath x:Name="icon" Icon="{TemplateBinding Icon}" Height="1024" Fill="{TemplateBinding Fill}"/>
</Viewbox>
<TextBlock Grid.Column="1" Text="{TemplateBinding Text}"
Margin="3,0,0,2"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Resources>
<Storyboard x:Key="Show" Storyboard.TargetName="grid">
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="20"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="Hide" Storyboard.TargetName="grid">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)">
<SplineDoubleKeyFrame KeyTime="0:0:0.2" Value="20"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.4" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="0" Value="1"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.2" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="0:0:0.4" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<ControlTemplate.Triggers>
<Trigger Property="IsShowingMessage" Value="True">
<Trigger.EnterActions>
<BeginStoryboard x:Name="Show_BeginStoryboard" Storyboard="{StaticResource Show}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="Show_BeginStoryboard"/>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsShowingMessage" Value="False">
<Trigger.EnterActions>
<BeginStoryboard x:Name="Hide_BeginStoryboard" Storyboard="{StaticResource Hide}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="Hide_BeginStoryboard"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<NullGuard />
</Weavers>

Просмотреть файл

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{158B05E8-FDBC-4D71-B871-C96E28D5ADF5}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GitHub</RootNamespace>
<AssemblyName>GitHub.UI.Reactive</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>2c878333</NuGetPackageImportStamp>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<Reference Include="NullGuard">
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="ReactiveUI, Version=6.0.6.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\reactiveui-core.6.0.6\lib\Net45\ReactiveUI.dll</HintPath>
</Reference>
<Reference Include="ReactiveUI.Events, Version=6.0.6.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\reactiveui-events.6.0.6\lib\net45\ReactiveUI.Events.dll</HintPath>
</Reference>
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Interactive">
<HintPath>..\..\packages\Ix_Experimental-Main.1.1.10823\lib\Net4\System.Interactive.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Core">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Interfaces">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Linq">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.PlatformServices">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Windows.Threading">
<HintPath>..\..\packages\Rx-XAML.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
</Reference>
<Reference Include="System.Xaml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Assets\Controls\Validation\ReactivePropertyValidator.cs" />
<Compile Include="Assets\Controls\Validation\ValidationMessage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Validation\ReactiveValidatableObject.cs" />
<Compile Include="Validation\ValidateIfAttribute.cs" />
</ItemGroup>
<ItemGroup>
<Page Include="Assets\Controls.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Assets\Controls\Validation\ValidationMessage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj">
<Project>{6559E128-8B40-49A5-85A8-05565ED0C7E3}</Project>
<Name>GitHub.Extensions.Reactive</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
<Name>GitHub.Extensions</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.UI\GitHub.UI.csproj">
<Project>{346384DD-2445-4A28-AF22-B45F3957BD89}</Project>
<Name>GitHub.UI</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Просмотреть файл

@ -0,0 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.UI.Recative")]
[assembly: AssemblyDescription("GitHub flavored WPF styles and controls that require Rx and RxUI")]
[assembly: Guid("885a491c-1d13-49e7-baa6-d61f424befcb")]

Просмотреть файл

@ -0,0 +1,218 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using GitHub.Extensions;
using ReactiveUI;
namespace GitHub.Validation
{
public class ReactiveValidatableObject : ReactiveObject, IDataErrorInfo
{
static readonly ConcurrentDictionary<Type, Dictionary<string, ValidatedProperty>> typeValidatorsMap = new ConcurrentDictionary<Type, Dictionary<string, ValidatedProperty>>();
const string isValidPropertyName = "IsValid";
readonly IServiceProvider serviceProvider;
readonly Dictionary<string, ValidatedProperty> validatedProperties;
readonly Dictionary<string, bool> invalidProperties = new Dictionary<string, bool>();
readonly Dictionary<string, bool> enabledProperties = new Dictionary<string, bool>();
bool validationEnabled = true;
public ReactiveValidatableObject(IServiceProvider serviceProvider)
{
validatedProperties = typeValidatorsMap.GetOrAdd(GetType(), GetValidatedProperties);
this.serviceProvider = serviceProvider; // This is allowed to be null.
// NOTE: Until a property has been changed, we don't want to mark it as invalid
// as far as IDataErrorInfo is concerned.
Changed.Where(x => validatedProperties.ContainsKey(x.PropertyName))
.Subscribe(x => enabledProperties[x.PropertyName] = true);
}
public string this[string propertyName]
{
get
{
if (!validationEnabled || !enabledProperties.ContainsKey(propertyName)) return null;
string errorMessage = GetErrorMessage(propertyName);
bool isValid = errorMessage == null;
if (isValid && invalidProperties.ContainsKey(propertyName))
{
invalidProperties.Remove(propertyName);
this.RaisePropertyChanged(isValidPropertyName); // TODO: See if I can make this more declarative with a reactive dictionary, if there is one.
}
else if (!isValid)
{
invalidProperties[propertyName] = true;
this.RaisePropertyChanged(isValidPropertyName); // TODO: See if I can make this more declarative with a reactive dictionary, if there is one.
}
return errorMessage;
}
}
[SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Justification = "Need to ask the author why this is not implemented and to add a message as such to the exception.")]
public string Error
{
get { throw new NotImplementedException(); }
}
public bool IsValid
{
get { return !invalidProperties.Any(); }
}
public bool Validate()
{
bool wasValid = IsValid;
var unvalidated = EnableValidationForUnvalidatedProperties();
unvalidated.ForEach(TriggerValidationForProperty);
if (IsValid != wasValid)
{
this.RaisePropertyChanged(isValidPropertyName);
}
return IsValid;
}
public void ResetValidation()
{
try
{
validationEnabled = false;
TriggerValidationForAllProperties(); // Tell the UI every property is now valid.
invalidProperties.Clear();
enabledProperties.Clear();
foreach (var v in validatedProperties.Values)
v.Reset();
this.RaisePropertyChanged(isValidPropertyName);
}
finally
{
validationEnabled = true;
}
}
// Enables validation for any properties that have not yet been validated and
// returns that set of properties.
IEnumerable<string> EnableValidationForUnvalidatedProperties()
{
return validatedProperties.Keys.Where(key => !enabledProperties.ContainsKey(key))
.Do(propertyName => enabledProperties[propertyName] = true);
}
// Made this its own method because it's not clear that the way to trigger validation is to
// raise a property changed event.
void TriggerValidationForProperty(string propertyName)
{
this.RaisePropertyChanged(propertyName);
}
void TriggerValidationForAllProperties()
{
validatedProperties.Keys.ForEach(TriggerValidationForProperty);
}
string GetErrorMessage(string propertyName)
{
ValidatedProperty validatedProperty;
if (!validatedProperties.TryGetValue(propertyName, out validatedProperty))
return null; // TODO: This would be a good place to do default data type validation as the need arises.
var validationResult = validatedProperty.GetFirstValidationError(this, serviceProvider);
return validationResult == ValidationResult.Success ? null : validationResult.ErrorMessage;
}
public void SetErrorMessage(string propertyName, string errorMessage)
{
Guard.ArgumentNotEmptyString(propertyName, "propertyName");
Guard.ArgumentNotEmptyString(errorMessage, "errorMessage");
ValidatedProperty validatedProperty;
if (!validatedProperties.TryGetValue(propertyName, out validatedProperty))
return;
validatedProperty.AddValidator(new SetErrorValidator(errorMessage, validatedProperty.Property.GetValue(this, null)));
TriggerValidationForProperty(propertyName);
}
static Dictionary<string, ValidatedProperty> GetValidatedProperties(Type type)
{
return (from property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
let validated = new ValidatedProperty(property)
where validated.Validators.Any()
select validated).ToDictionary(p => p.Property.Name, p => p);
}
class ValidatedProperty
{
readonly IList<ValidationAttribute> validators;
public ValidatedProperty(PropertyInfo property)
{
Property = property;
validators = property.GetCustomAttributes(typeof(ValidationAttribute), true).Cast<ValidationAttribute>().ToList();
Validators = validators.Where(v => !(v is ValidateIfAttribute));
ConditionalValidation = validators.FirstOrDefault(v => v is ValidateIfAttribute) as ValidateIfAttribute;
}
public void AddValidator(ValidationAttribute validator)
{
validators.Add(validator);
}
public ValidationResult GetFirstValidationError(object instance, IServiceProvider serviceProvider)
{
var validationContext = new ValidationContext(instance, serviceProvider, null) { MemberName = Property.Name };
if (ConditionalValidation != null && !ConditionalValidation.IsValidationRequired(validationContext))
{
return ValidationResult.Success;
}
var value = Property.GetValue(instance, null);
return (from validator in Validators
let r = validator.GetValidationResult(value, validationContext)
where r != null
select r).FirstOrDefault();
}
public void Reset()
{
var setErrorValidators = validators.Where(x => x is SetErrorValidator).ToList();
setErrorValidators.ForEach(x => validators.Remove(x));
}
public PropertyInfo Property { get; private set; }
public IEnumerable<ValidationAttribute> Validators { get; private set; }
ValidateIfAttribute ConditionalValidation { get; set; }
}
sealed class SetErrorValidator : ValidationAttribute
{
readonly string errorMessage;
readonly object originalValue;
public SetErrorValidator(string errorMessage, object originalValue)
{
this.errorMessage = errorMessage;
this.originalValue = originalValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (originalValue.Equals(value))
return new ValidationResult(errorMessage);
return ValidationResult.Success;
}
}
}
}

Просмотреть файл

@ -0,0 +1,46 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using GitHub.Extensions;
using NullGuard;
namespace GitHub.Validation
{
/// <summary>
/// Used to conditionally run a set of validators. If this attribute is applied, and
/// the dependent property is false, no other validators run. This special case logic
/// occurs in ReactiveValidatableObject.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class ValidateIfAttribute : ValidationAttribute
{
public ValidateIfAttribute(string dependentPropertyName)
{
Guard.ArgumentNotEmptyString(dependentPropertyName, "dependentPropertyName");
DependentPropertyName = dependentPropertyName;
}
protected override ValidationResult IsValid([AllowNull]object value, [AllowNull]ValidationContext validationContext)
{
return ValidationResult.Success;
}
public bool IsValidationRequired(ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
Debug.Assert(instance != null, "The ValidationContext does not allow null instances.");
var property = instance.GetType().GetProperty(DependentPropertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (property == null || property.PropertyType != typeof(bool))
{
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Could not find a boolean property named '{0}", DependentPropertyName));
}
return (bool)property.GetValue(instance, null);
}
public string DependentPropertyName { get; private set; }
}
}

Просмотреть файл

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
<package id="Ix_Experimental-Main" version="1.1.10823" targetFramework="net45" />
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
<package id="reactiveui" version="6.0.6" targetFramework="net45" />
<package id="reactiveui-core" version="6.0.6" targetFramework="net45" />
<package id="reactiveui-events" version="6.0.6" targetFramework="net45" />
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
<package id="Rx-Main" version="2.2.5" targetFramework="net45" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net45" />
<package id="Rx-XAML" version="2.2.5" targetFramework="net45" />
<package id="Splat" version="1.4.2.1" targetFramework="net45" />
</packages>

Просмотреть файл

@ -1,4 +1,8 @@
using System.Windows;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
namespace GitHub.UI

Просмотреть файл

@ -1,5 +1,14 @@
using System.Windows;
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace GitHub.UI

Просмотреть файл

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<NullGuard />
</Weavers>

Просмотреть файл

@ -11,7 +11,11 @@
<AssemblyName>GitHub.UI</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>6cab0329</NuGetPackageImportStamp>
<NuGetPackageImportStamp>1145ea6a</NuGetPackageImportStamp>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -20,10 +24,6 @@
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>false</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -37,7 +37,7 @@
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>
<AssemblyOriginatorKeyFile>..\..\build\Key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.Shell.12.0, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
@ -58,6 +58,9 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Controls\Octicons\Octicon.cs" />
<Compile Include="Controls\Octicons\OcticonPath.cs" />
<Compile Include="Controls\Octicons\OcticonPaths.Designer.cs" />
@ -128,7 +131,6 @@
</Page>
</ItemGroup>
<ItemGroup>
<None Include="Key.snk" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
@ -140,12 +142,12 @@
<EmbeddedResource Include="Controls\Octicons\OcticonPaths.resx" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets')" />
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.26.3-beta0001\build\Fody.targets'))" />
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

Просмотреть файл

@ -1,7 +1,4 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Media;
namespace GitHub.UI
@ -60,10 +57,7 @@ namespace GitHub.UI
public static SolidColorBrush CreateBrush(string color)
{
var convertedColor = ColorConverter.ConvertFromString(color);
Debug.Assert(convertedColor != null,
String.Format(CultureInfo.InvariantCulture, "The converted Color '{0}' should not be null", color));
return CreateBrush((Color)convertedColor);
return CreateBrush((Color)ColorConverter.ConvertFromString(color));
}
}
}

Просмотреть файл

@ -1,28 +1,6 @@
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("GitHub.UI")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("GitHub.UI")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: AssemblyDescription("GitHub flavored WPF styles and controls")]
[assembly: Guid("f3cec21e-6a86-43ae-97a6-a274fa31efbe")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: AssemblyVersion("0.1.0.0")]
[assembly: AssemblyFileVersion("0.1.0.0")]

Просмотреть файл

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Fody" version="1.26.3-beta0001" targetFramework="net45" developmentDependency="true" />
<package id="Fody" version="1.25.0" targetFramework="net45" developmentDependency="true" />
<package id="NullGuard.Fody" version="1.2.0.0" targetFramework="net45" developmentDependency="true" />
</packages>

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<NullGuard />
</Weavers>

Просмотреть файл

@ -1,47 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
<PropertyGroup>
<MinimumVisualStudioVersion>12.0</MinimumVisualStudioVersion>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>12.0</OldToolsVersion>
<UpgradeBackupLocation />
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<NuGetPackageImportStamp>39465276</NuGetPackageImportStamp>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<SchemaVersion>2.0</SchemaVersion>
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<ProjectGuid>{1D05E67A-6D4A-4176-BFB0-D9F81BFD6E21}</ProjectGuid>
<ProjectGuid>{11569514-5AE5-4B5B-92A2-F10B0967DE5F}</ProjectGuid>
<ProjectTypeGuids>{82b43b9b-a64c-4715-b499-d71e9ca2bd60};{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>GitHub.VisualStudio</RootNamespace>
<AssemblyName>GitHub.VisualStudio</AssemblyName>
<SignAssembly>false</SignAssembly>
<AssemblyOriginatorKeyFile>
</AssemblyOriginatorKeyFile>
<StartAction>Program</StartAction>
<StartProgram>$(DevEnvDir)\devenv.exe</StartProgram>
<StartArguments>/rootsuffix Exp</StartArguments>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<GeneratePkgDefFile>false</GeneratePkgDefFile>
<IncludeAssemblyInVSIXContainer>false</IncludeAssemblyInVSIXContainer>
<IncludeDebugSymbolsInVSIXContainer>false</IncludeDebugSymbolsInVSIXContainer>
<IncludeDebugSymbolsInLocalVSIXDeployment>false</IncludeDebugSymbolsInLocalVSIXDeployment>
<CopyBuildOutputToOutputDirectory>false</CopyBuildOutputToOutputDirectory>
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -51,8 +33,9 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>false</RunCodeAnalysis>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\..\build\GitHubVS.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -61,49 +44,239 @@
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Reference Include="EditorUtils, Version=1.4.0.0, Culture=neutral, PublicKeyToken=3d1514c4742e0252, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\EditorUtils.1.4.0.0\lib\net40\EditorUtils.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.CoreUtility, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Language.Intellisense, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Text.Data, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Text.Logic, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Text.UI, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.Text.UI.Wpf, Version=12.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Microsoft.VisualStudio.OLE.Interop" />
<Reference Include="Microsoft.VisualStudio.Shell.Interop" />
<Reference Include="Microsoft.VisualStudio.Shell.Interop.8.0" />
<Reference Include="Microsoft.VisualStudio.Shell.Interop.9.0" />
<Reference Include="Microsoft.VisualStudio.Shell.Interop.10.0" />
<Reference Include="Microsoft.VisualStudio.Shell.Interop.11.0">
<EmbedInteropTypes>true</EmbedInteropTypes>
</Reference>
<Reference Include="Microsoft.VisualStudio.Shell.Interop.12.0">
<EmbedInteropTypes>true</EmbedInteropTypes>
</Reference>
<Reference Include="Microsoft.VisualStudio.TextManager.Interop" />
<Reference Include="Microsoft.VisualStudio.Shell.12.0" />
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.10.0" />
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.11.0" />
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.12.0" />
<Reference Include="NullGuard">
<HintPath>..\..\packages\NullGuard.Fody.1.2.0.0\Lib\portable-net4+sl4+wp7+win8+MonoAndroid16+MonoTouch40\NullGuard.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ReactiveUI, Version=6.0.6.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\reactiveui-core.6.0.6\lib\Net45\ReactiveUI.dll</HintPath>
</Reference>
<Reference Include="Splat, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Splat.1.4.2.1\lib\Net45\Splat.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Design" />
<Reference Include="System.Drawing" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Linq">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Windows.Threading, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Rx-XAML.2.2.5\lib\net45\System.Reactive.Windows.Threading.dll</HintPath>
</Reference>
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="WindowsBase" />
<Reference Include="System.Xaml" />
</ItemGroup>
<ItemGroup>
<COMReference Include="EnvDTE100">
<Guid>{26AD1324-4B7C-44BC-84F8-B86AED45729F}</Guid>
<VersionMajor>10</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>False</EmbedInteropTypes>
</COMReference>
<COMReference Include="EnvDTE90">
<Guid>{2CE2370E-D744-4936-A090-3FFFE667B0E1}</Guid>
<VersionMajor>9</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>False</EmbedInteropTypes>
</COMReference>
<COMReference Include="Microsoft.VisualStudio.CommandBars">
<Guid>{1CBA492E-7263-47BB-87FE-639000619B15}</Guid>
<VersionMajor>8</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>False</EmbedInteropTypes>
</COMReference>
<COMReference Include="stdole">
<Guid>{00020430-0000-0000-C000-000000000046}</Guid>
<VersionMajor>2</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>primary</WrapperTool>
<Isolated>False</Isolated>
<EmbedInteropTypes>False</EmbedInteropTypes>
</COMReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\build\SolutionInfo.cs">
<Link>Properties\SolutionInfo.cs</Link>
</Compile>
<Compile Include="Guids.cs" />
<Compile Include="Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="GitHubPackage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PkgCmdID.cs" />
<Compile Include="UI\DrawingExtensions.cs" />
<Compile Include="UI\Views\Controls\LoginControl.xaml.cs">
<DependentUpon>LoginControl.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Views\CreateIssueDialog.xaml.cs">
<DependentUpon>CreateIssueDialog.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Views\LoginCommandDialog.xaml.cs">
<DependentUpon>LoginCommandDialog.xaml</DependentUpon>
</Compile>
<Compile Include="UI\Views\TwoFactorView.xaml.cs">
<DependentUpon>TwoFactorView.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="VSPackage.resx">
<MergeWithCTO>true</MergeWithCTO>
<ManifestResourceName>VSPackage</ManifestResourceName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="source.extension.vsixmanifest">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<VSCTCompile Include="GitHub.VisualStudio.vsct">
<ResourceName>Menus.ctmenu</ResourceName>
</VSCTCompile>
</ItemGroup>
<ItemGroup>
<None Include="Resources\Images.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="FodyWeavers.xml" />
<CodeAnalysisDictionary Include="..\..\build\CodeAnalysisDictionary.xml">
<Link>Properties\CodeAnalysisDictionary.xml</Link>
</CodeAnalysisDictionary>
<Content Include="Resources\Package.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<IncludeInVSIX>true</IncludeInVSIX>
</Content>
</ItemGroup>
<ItemGroup>
<Page Include="UI\Views\Controls\LoginControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Views\CreateIssueDialog.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="UI\Views\LoginCommandDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Views\TwoFactorView.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GitHub.App\GitHub.App.csproj">
<Project>{1a1da411-8d1f-4578-80a6-04576bea2dc5}</Project>
<Name>GitHub.App</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Extensions.Reactive\GitHub.Extensions.Reactive.csproj">
<Project>{6559e128-8b40-49a5-85a8-05565ed0c7e3}</Project>
<Name>GitHub.Extensions.Reactive</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.Extensions\GitHub.Extensions.csproj">
<Project>{6afe2e2d-6db0-4430-a2ea-f5f5388d2f78}</Project>
<Name>GitHub.Extensions</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.UI.Reactive\GitHub.UI.Reactive.csproj">
<Project>{158b05e8-fdbc-4d71-b871-c96e28d5adf5}</Project>
<Name>GitHub.UI.Reactive</Name>
</ProjectReference>
<ProjectReference Include="..\GitHub.UI\GitHub.UI.csproj">
<Project>{346384dd-2445-4a28-af22-b45f3957bd89}</Project>
<Name>GitHub.UI</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\Rothko\src\Rothko.csproj">
<Project>{4a84e568-ca86-4510-8cd0-90d3ef9b65f9}</Project>
<Name>Rothko</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<UseCodebase>true</UseCodebase>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="..\..\packages\Fody.1.25.0\build\Fody.targets" Condition="Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\Fody.1.25.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Fody.1.25.0\build\Fody.targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

Просмотреть файл

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<CommandTable
xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This is the file that defines the actual layout and type of the commands.
It is divided in different sections (e.g. command definition, command
placement, ...), with each defining a specific set of properties.
See the comment before each section for more details about how to
use it. -->
<!-- The VSCT compiler (the tool that translates this file into the binary
format that VisualStudio will consume) has the ability to run a preprocessor
on the vsct file; this preprocessor is (usually) the C++ preprocessor, so
it is possible to define includes and macros with the same syntax used
in C++ files. Using this ability of the compiler here, we include some files
defining some of the constants that we will use inside the file. -->
<!--This is the file that defines the IDs for all the commands exposed by VisualStudio. -->
<Extern href="stdidcmd.h"/>
<!--This header contains the command ids for the menus provided by the shell. -->
<Extern href="vsshlids.h"/>
<!--The Commands section is where we the commands, menus and menu groups are defined.
This section uses a Guid to identify the package that provides the command defined inside it. -->
<Commands package="guidGitHubPkg">
<Menus>
<Menu guid="guidGitHubCmdSet" id="GitHubMenu" priority="0x700" type="Menu">
<Parent guid="guidSHLMainMenu" id="IDG_VS_MM_TOOLSADDINS" />
<Strings>
<ButtonText>&amp;GitHub</ButtonText>
</Strings>
</Menu>
</Menus>
<Groups>
<Group guid="guidGitHubCmdSet" id="GitHubMenuGroup" priority="0x0600">
<Parent guid="guidGitHubCmdSet" id="GitHubMenu" />
</Group>
</Groups>
<!--Buttons section. -->
<!--This section defines the elements the user can interact with, like a menu command or a button
or combo box in a toolbar. -->
<Buttons>
<!--To define a menu group you have to specify its ID, the parent menu and its display priority.
The command is visible and enabled by default. If you need to change the visibility, status, etc, you can use
the CommandFlag node.
You can add more than one CommandFlag node e.g.:
<CommandFlag>DefaultInvisible</CommandFlag>
<CommandFlag>DynamicVisibility</CommandFlag>
If you do not want an image next to your command, remove the Icon node /> -->
<Button guid="guidGitHubCmdSet" id="loginCommand" priority="0x0100" type="Button">
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>&amp;Login</ButtonText>
</Strings>
</Button>
<Button guid="guidGitHubCmdSet" id="createIssueCommand" priority="0x0101" type="Button">
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>&amp;Create Issue</ButtonText>
</Strings>
</Button>
<Button guid="guidGitHubCmdSet" id="goToIssueCommand" priority="0x0102" type="Button">
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>&amp;Go to issue</ButtonText>
</Strings>
</Button>
<Button guid="guidGitHubCmdSet" id="viewIssuesCommand" priority="0x0103" type="Button">
<Parent guid="guidGitHubCmdSet" id="GitHubMenuGroup" />
<Icon guid="guidImages" id="bmpPic1" />
<Strings>
<ButtonText>&amp;View Issues</ButtonText>
</Strings>
</Button>
</Buttons>
<!--The bitmaps section is used to define the bitmaps that are used for the commands.-->
<Bitmaps>
<!-- The bitmap id is defined in a way that is a little bit different from the others:
the declaration starts with a guid for the bitmap strip, then there is the resource id of the
bitmap strip containing the bitmaps and then there are the numeric ids of the elements used
inside a button definition. An important aspect of this declaration is that the element id
must be the actual index (1-based) of the bitmap inside the bitmap strip. -->
<Bitmap guid="guidImages" href="Resources\Images.png" usedList="bmpPic1, bmpPic2, bmpPicSearch, bmpPicX, bmpPicArrows"/>
</Bitmaps>
</Commands>
<KeyBindings>
<KeyBinding guid="guidGitHubCmdSet" id="loginCommand" editor="guidVSStd97" key1="U" key2="L" mod2="CONTROL" mod1="CONTROL"/>
<KeyBinding guid="guidGitHubCmdSet" id="createIssueCommand" editor="guidVSStd97" key1="U" key2="C" mod2="CONTROL" mod1="CONTROL"/>
<KeyBinding guid="guidGitHubCmdSet" id="goToIssueCommand" editor="guidVSStd97" key1="U" key2="G" mod2="CONTROL" mod1="CONTROL"/>
<KeyBinding guid="guidGitHubCmdSet" id="viewIssuesCommand" editor="guidVSStd97" key1="U" key2="V" mod2="CONTROL" mod1="CONTROL"/>
</KeyBindings>
<Symbols>
<!-- This is the package guid. -->
<GuidSymbol name="guidGitHubPkg" value="{c3d3dc68-c977-411f-b3e8-03b0dccf7dfc}" />
<!-- This is the guid used to group the menu commands together -->
<GuidSymbol name="guidGitHubCmdSet" value="{c4c91892-8881-4588-a5d9-b41e8f540f5a}">
<IDSymbol name="GitHubMenuGroup" value="0x1020" />
<IDSymbol name="loginCommand" value="0x0110" />
<IDSymbol name="createIssueCommand" value="0x0100" />
<IDSymbol name="goToIssueCommand" value="0x0101" />
<IDSymbol name="viewIssuesCommand" value="0x0102" />
<IDSymbol name="GitHubMenu" value="0x1021"/>
</GuidSymbol>
<GuidSymbol name="guidImages" value="{27841f47-070a-46d6-90be-a5cbbfc724ac}" >
<IDSymbol name="bmpPic1" value="1" />
<IDSymbol name="bmpPic2" value="2" />
<IDSymbol name="bmpPicSearch" value="3" />
<IDSymbol name="bmpPicX" value="4" />
<IDSymbol name="bmpPicArrows" value="5" />
<IDSymbol name="bmpPicStrikethrough" value="6" />
</GuidSymbol>
</Symbols>
</CommandTable>

Просмотреть файл

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
using GitHub.Authentication;
using GitHub.Infrastructure;
using GitHub.ViewModels;
using GitHub.VisualStudio.UI.Views;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Shell;
using ReactiveUI;
using Splat;
namespace GitHub.VisualStudio
{
/// <summary>
/// This is the class that implements the package exposed by this assembly.
///
/// The minimum requirement for a class to be considered a valid package for Visual Studio
/// is to implement the IVsPackage interface and register itself with the shell.
/// This package uses the helper classes defined inside the Managed Package Framework (MPF)
/// to do it: it derives from the Package class that provides the implementation of the
/// IVsPackage interface and uses the registration attributes defined in the framework to
/// register itself and its components with the shell.
/// </summary>
// This attribute tells the PkgDef creation utility (CreatePkgDef.exe) that this class is
// a package.
[PackageRegistration(UseManagedResourcesOnly = true)]
// This attribute is used to register the information needed to show this package
// in the Help/About dialog of Visual Studio.
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
// This attribute is needed to let the shell know that this package exposes some menus.
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(GuidList.guidGitHubPkgString)]
[ProvideBindingPath]
public class GitHubPackage : Package
{
readonly IServiceProvider serviceProvider;
// Set of assemblies we need to load early.
static readonly IEnumerable<string> earlyLoadAssemblies = new[] {
"Rothko.dll",
"GitHub.App.dll",
"GitHub.UI.Reactive.dll",
"GitHub.UI.dll"
};
/// <summary>
/// Default constructor of the package.
/// Inside this method you can place any initialization code that does not require
/// any Visual Studio service because at this point the package object is created but
/// not sited yet inside Visual Studio environment. The place to do all the other
/// initialization is the Initialize method.
/// </summary>
public GitHubPackage()
{
serviceProvider = this;
}
public GitHubPackage(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
/// <summary>
/// Initialization of the package; this method is called right after the package is sited, so this is the place
/// where you can put all the initialization code that rely on services provided by VisualStudio.
/// </summary>
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFile")]
protected override void Initialize()
{
Debug.WriteLine("Entering Initialize() of: {0}", ToString());
base.Initialize();
ModeDetector.OverrideModeDetector(new AppModeDetector());
RxApp.MainThreadScheduler = new DispatcherScheduler(Application.Current.Dispatcher);
var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Debug.Assert(dir != null, "The Assembly location can't be null");
foreach (var v in earlyLoadAssemblies)
{
Assembly.LoadFile(Path.Combine(dir, v));
}
// Set the Export Provider
var mefServiceProvider = GetExportedValue<IServiceProvider>() as MefServiceProvider;
Debug.Assert(mefServiceProvider != null, "Service Provider can't be imported");
var componentModel = (IComponentModel)(serviceProvider.GetService(typeof(SComponentModel)));
mefServiceProvider.ExportProvider = componentModel.DefaultExportProvider;
// Add our command handlers for menu (commands must exist in the .vsct file)
var mcs = serviceProvider.GetService(typeof(IMenuCommandService)) as IMenuCommandService;
if (mcs != null)
{
// Login Command Menu Item
AddTopLevelMenuItem(mcs, PkgCmdIDList.loginCommand, OnLoginCommand);
// Create Issue Command Menu Item
AddTopLevelMenuItem(mcs, PkgCmdIDList.createIssueCommand, OnCreateIssueCommand);
}
}
static void AddTopLevelMenuItem(
IMenuCommandService menuCommandService,
uint packageCommandId,
EventHandler eventHandler)
{
var menuCommandId = new CommandID(GuidList.guidGitHubCmdSet, (int)packageCommandId);
var menuItem = new MenuCommand(eventHandler, menuCommandId);
menuCommandService.AddCommand(menuItem);
}
/// <summary>
/// This function is the callback used to execute a command when the a menu item is clicked.
/// See the Initialize method to see how the menu item is associated to this function using
/// the OleMenuCommandService service and the MenuCommand class.
/// </summary>
static void OnCreateIssueCommand(object sender, EventArgs e)
{
var createIssueDialog = new CreateIssueDialog();
createIssueDialog.ShowModal();
}
void OnLoginCommand(object sender, EventArgs e)
{
var loginControlViewModel = GetExportedValue<LoginControlViewModel>();
var loginIssueDialog = new LoginCommandDialog(loginControlViewModel);
loginIssueDialog.Show();
loginControlViewModel.AuthenticationResults.Subscribe(result =>
{
if (result == AuthenticationResult.Success)
loginIssueDialog.Hide();
});
}
T GetExportedValue<T>()
{
var componentModel = (IComponentModel)(serviceProvider.GetService(typeof(SComponentModel)));
var exportProvider = componentModel.DefaultExportProvider;
return exportProvider.GetExportedValue<T>();
}
}
[Export(typeof(IServiceProvider))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MefServiceProvider : IServiceProvider
{
public ExportProvider ExportProvider { get; set; }
public object GetService(Type serviceType)
{
string contract = AttributedModelServices.GetContractName(serviceType);
var instance = ExportProvider.GetExportedValues<object>(contract).FirstOrDefault();
if (instance != null)
return instance;
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
"Could not locate any instances of contract {0}.", contract));
}
}
}

Просмотреть файл

@ -0,0 +1,11 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project. Project-level
// suppressions either have no target or are given a specific target
// and scoped to a namespace, type, member, etc.
//
// To add a suppression to this file, right-click the message in the
// Error List, point to "Suppress Message(s)", and click "In Project
// Suppression File". You do not need to add suppressions to this
// file manually.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1701:ResourceStringCompoundWordsShouldBeCasedCorrectly", MessageId = "GitHub", Scope = "resource", Target = "VSPackage.resources")]

Просмотреть файл

@ -0,0 +1,14 @@
// Guids.cs
// MUST match guids.h
using System;
namespace GitHub.VisualStudio
{
static class GuidList
{
public const string guidGitHubPkgString = "c3d3dc68-c977-411f-b3e8-03b0dccf7dfc";
public const string guidGitHubCmdSetString = "c4c91892-8881-4588-a5d9-b41e8f540f5a";
public static readonly Guid guidGitHubCmdSet = new Guid(guidGitHubCmdSetString);
};
}

Просмотреть файл

@ -0,0 +1,10 @@
// PkgCmdID.cs
// MUST match PkgCmdID.h
namespace GitHub.VisualStudio
{
static class PkgCmdIDList
{
public const uint loginCommand = 0x110;
public const uint createIssueCommand = 0x100;
};
}

Просмотреть файл

@ -1,4 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.VisualStudio")]
[assembly: AssemblyDescription("The GitHub for Visual Studio VSIX package")]
[assembly: AssemblyDescription("GitHub for Visual Studio VSPackage")]
[assembly: Guid("fad77eaa-3fe1-4c4b-88dc-3753b6263cd7")]

63
src/GitHub.VisualStudio/Resources.Designer.cs сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.34014
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace GitHub.VisualStudio {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GitHub.VisualStudio.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

Просмотреть файл

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

Двоичные данные
src/GitHub.VisualStudio/Resources/Images.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 994 B

Просмотреть файл

@ -0,0 +1,13 @@
using System.Windows;
namespace GitHub.VisualStudio.UI
{
public static class DrawingExtensions
{
public static T FreezeThis<T>(this T freezable) where T : Freezable
{
freezable.Freeze();
return freezable;
}
}
}

Просмотреть файл

@ -0,0 +1,230 @@
<UserControl x:Class="GitHub.VisualStudio.UI.Views.Controls.LoginControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI"
xmlns:helpers="clr-namespace:GitHub.Helpers;assembly=GitHub.UI"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/Styles.xaml" />
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/Buttons.xaml" />
<ResourceDictionary Source="pack://application:,,,/GitHub.UI;component/Assets/TextBlocks.xaml" />
<ResourceDictionary Source="pack://application:,,,/GitHub.UI.Reactive;component/Assets/Controls.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid
FocusManager.IsFocusScope="True"
x:Name="loginStackPanel"
FocusVisualStyle="{x:Null}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="0" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Normal">
<VisualState x:Name="Enterprise">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="dotComOnlyLoginLabel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseOnlyLoginLabel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="dotComTabButton">
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
</BooleanAnimationUsingKeyFrames>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="enterpriseTabButton">
<DiscreteBooleanKeyFrame KeyTime="0" Value="False" />
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="dotComTabButton">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<FontWeight>Normal</FontWeight>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="enterpriseTabButton">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<FontWeight>Bold</FontWeight>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="gitHubSignupStackPanel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.MinHeight)" Storyboard.TargetName="fields">
<EasingDoubleKeyFrame KeyTime="0" Value="300"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="DotCom">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="dotComOnlyLoginLabel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseOnlyLoginLabel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="dotComTabButton">
<DiscreteBooleanKeyFrame KeyTime="0" Value="False" />
</BooleanAnimationUsingKeyFrames>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.IsEnabled)" Storyboard.TargetName="enterpriseTabButton">
<DiscreteBooleanKeyFrame KeyTime="0" Value="True" />
</BooleanAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="dotComTabButton">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<FontWeight>Bold</FontWeight>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(TextElement.FontWeight)" Storyboard.TargetName="enterpriseTabButton">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<FontWeight>Normal</FontWeight>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseLearnStackPanel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseUrlTextBox">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.MinHeight)" Storyboard.TargetName="fields">
<EasingDoubleKeyFrame KeyTime="0" Value="300"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="EnterpriseOnly">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="tabs">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="gitHubSignupStackPanel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="dotComOnlyLoginLabel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="DotComOnly">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="tabs">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseLearnStackPanel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseUrlTextBox">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="enterpriseOnlyLoginLabel">
<DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Design" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel x:Name="fields" Margin="24,0,0,12" Background="{x:Null}" FocusManager.IsFocusScope="True" FocusVisualStyle="{x:Null}" helpers:AccessKeysManagerScoping.IsEnabled="True">
<StackPanel
x:Name="loginLabel"
Visibility="{Binding LoginFailed, Mode=OneWay, Converter={ui:BooleanToInverseVisibilityConverter}}"
Height="32"
Orientation="Horizontal"
HorizontalAlignment="Left"
Margin="40,0,0,12">
<TextBlock
x:Name="loginLabelPrefix"
Margin="0"
Style="{DynamicResource GitHubH1TextBlock}"
IsHitTestVisible="False"
Text="Log in" />
<TextBlock
x:Name="dotComOnlyLoginLabel"
Margin="0"
Style="{DynamicResource GitHubH1TextBlock}"
IsHitTestVisible="False"
Text=" to GitHub" />
<TextBlock
x:Name="enterpriseOnlyLoginLabel"
Margin="0"
Style="{DynamicResource GitHubH1TextBlock}"
IsHitTestVisible="False"
Text=" to GitHub Enterprise" />
</StackPanel>
<StackPanel
x:Name="loginFailed"
Margin="40,0,0,12"
Orientation="Horizontal"
Visibility="{Binding LoginFailed, Converter={ui:BooleanToVisibilityConverter}}"
Height="32">
<TextBlock x:Name="loginFailedLabel" Margin="0" Style="{DynamicResource GitHubH1TextBlock}" Foreground="#ea2828" Text="{Binding LoginFailedText}" IsHitTestVisible="False" Visibility="{Binding LoginFailed, Converter={ui:BooleanToVisibilityConverter}}" />
<ui:OcticonLinkButton x:Name="forgotPasswordButton" Content="Forgot password?" Icon="link_external" Margin="6,3,0,0" IsTabStop="False" Command="{Binding ForgotPasswordCommand}" ToolTip="{Binding ForgotPasswordUrl}" />
</StackPanel>
<StackPanel x:Name="tabs" Orientation="Horizontal" HorizontalAlignment="Left">
<Button
x:Name="dotComTabButton"
Content="GitHub"
IsEnabled="True"
Style="{StaticResource GitHubTabButton}" />
<Button
x:Name="enterpriseTabButton"
Content="GitHub Enterprise"
IsEnabled="False"
Style="{StaticResource GitHubTabButton}" />
</StackPanel>
<ui:PromptTextBox
Margin="0,0,0,12"
x:Name="usernameOrEmailTextBox"
PromptText="Username or Email"
Style="{DynamicResource RoundedPromptTextBox}"
IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}"
Text="{Binding UsernameOrEmail, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}" />
<ui:SecurePasswordBox
x:Name="passwordTextBox"
PromptText="Password"
Margin="0,0,0,12"
Style="{DynamicResource RoundedPromptTextBox}"
VerticalAlignment="Stretch"
IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}"
Text="{Binding PasswordNoOp, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}" />
<ui:PromptTextBox
Margin="0"
x:Name="enterpriseUrlTextBox"
PromptText="GitHub Enterprise URL"
Style="{DynamicResource RoundedPromptTextBox}"
IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}"
Text="{Binding EnterpriseUrl, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
Validation.ErrorTemplate="{DynamicResource ValidationAdorner}" />
<StackPanel Orientation="Horizontal" IsEnabled="{Binding IsLoginInProgress, Converter={ui:InverseBooleanConverter}}" Margin="0,12,0,0">
<ui:OcticonCircleButton x:Name="loginButton" Icon="check" Content="{Binding LoginButtonText, FallbackValue=Log In}" IsDefault="True" />
<ui:OcticonCircleButton x:Name="cancelButton" Icon="x" Margin="12,0,0,0" Content="Cancel" />
</StackPanel>
<StackPanel x:Name="gitHubSignupStackPanel">
<TextBlock x:Name="signUpLabel" Style="{DynamicResource GitHubH1TextBlock}" Text="Sign up" Margin="0,10,0,0" />
<TextBlock x:Name="description" Style="{DynamicResource GitHubDescriptionTextBlock}" Margin="0"><Run Text="Powerful collaboration, review, and code management for open source and private development projects." /><LineBreak /><Run Text="Build software better, together." FontWeight="Bold"/></TextBlock>
<ui:OcticonLinkButton x:Name="signUpLink" HorizontalContentAlignment="Right" HorizontalAlignment="Left" Icon="link_external" Content="Sign up" ToolTip="go to github.com to sign up for an account" IsTabStop="False" />
</StackPanel>
<StackPanel x:Name="enterpriseLearnStackPanel">
<TextBlock x:Name="learnMoreLabel" Style="{DynamicResource GitHubH1TextBlock}" Text="Learn more" Margin="0,10,0,0" />
<TextBlock Style="{DynamicResource GitHubDescriptionTextBlock}" Margin="0"><Run Text="Powerful collaboration, review, and code management for private development projects." /><LineBreak /><Run Text="The best way to build and ship software, on your servers." FontWeight="Bold" /></TextBlock>
<ui:OcticonLinkButton x:Name="learnMoreButton" Icon="link_external" HorizontalContentAlignment="Right" HorizontalAlignment="Left" Content="Learn more" ToolTip="go to enterprise.github.com to learn more about running github on your servers" IsTabStop="False" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше