зеркало из https://github.com/github/VisualStudio.git
Merge pull request #93 from github/shana/cloning
Implementing the cloning action for the Clone dialog and friends
This commit is contained in:
Коммит
fdf9f5068d
Двоичный файл не отображается.
|
@ -23,6 +23,7 @@ namespace GitHub.Api
|
|||
public const string GitHubUrl = "https://" + GitHubDotComHostName;
|
||||
public const string GitHubDotComHostName = "github.com";
|
||||
public const string GitHubGistHostName = "gist.github.com";
|
||||
const string ProductName = "GitHub Extension for Visual Studio";
|
||||
const string clientId = "";
|
||||
const string clientSecret = "";
|
||||
public static readonly Uri GitHubDotComUri = new Uri(GitHubUrl);
|
||||
|
@ -86,7 +87,7 @@ namespace GitHub.Api
|
|||
Scopes = useOldScopes
|
||||
? oldAuthorizationScopes
|
||||
: newAuthorizationScopes,
|
||||
Note = "GitHub for Windows on " + GetMachineNameSafe()
|
||||
Note = ProductName + " on " + GetMachineNameSafe()
|
||||
};
|
||||
|
||||
var handler = twoFactorChallengeHander ?? TwoFactorChallengeHandler.HandleTwoFactorException;
|
||||
|
@ -112,7 +113,7 @@ namespace GitHub.Api
|
|||
Scopes = useOldScopes
|
||||
? oldAuthorizationScopes
|
||||
: newAuthorizationScopes,
|
||||
Note = "GitHub for Windows on " + GetMachineNameSafe()
|
||||
Note = ProductName + " on " + GetMachineNameSafe()
|
||||
};
|
||||
|
||||
return gitHubClient.Authorization.GetOrCreateApplicationAuthentication(
|
||||
|
@ -191,7 +192,7 @@ namespace GitHub.Api
|
|||
HasIssues = repository.HasIssues,
|
||||
HasWiki = repository.HasWiki,
|
||||
HasDownloads= repository.HasDownloads
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
static string GetMachineNameSafe()
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Security.Permissions;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Authentication.CredentialManagement
|
||||
{
|
||||
public class Credential : IDisposable
|
||||
{
|
||||
const int maxPasswordLengthInBytes = NativeMethods.CREDUI_MAX_PASSWORD_LENGTH * 2;
|
||||
|
||||
static readonly object _lockObject = new object();
|
||||
bool _disposed;
|
||||
|
||||
static readonly SecurityPermission _unmanagedCodePermission;
|
||||
|
||||
CredentialType _type;
|
||||
string _target;
|
||||
SecureString _password;
|
||||
string _username;
|
||||
string _description;
|
||||
DateTime _lastWriteTime;
|
||||
PersistenceType _persistanceType;
|
||||
|
||||
static Credential()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_unmanagedCodePermission = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
|
||||
}
|
||||
}
|
||||
|
||||
public Credential()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public Credential(string username)
|
||||
: this(username, null)
|
||||
{
|
||||
}
|
||||
|
||||
public Credential(string username, string password)
|
||||
: this(username, password, null)
|
||||
{
|
||||
}
|
||||
|
||||
public Credential(string username, string password, string target)
|
||||
: this(username, password, target, CredentialType.Generic)
|
||||
{
|
||||
}
|
||||
|
||||
public Credential(string username, string password, string target, CredentialType type)
|
||||
{
|
||||
Username = username;
|
||||
Password = password;
|
||||
Target = target;
|
||||
Type = type;
|
||||
PersistenceType = PersistenceType.Session;
|
||||
_lastWriteTime = DateTime.MinValue;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
// Prevent GC Collection since we have already disposed of this object
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
~Credential()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
SecurePassword.Clear();
|
||||
SecurePassword.Dispose();
|
||||
}
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void CheckNotDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("Credential object is already disposed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Username
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
return _username;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_username = value;
|
||||
}
|
||||
}
|
||||
public string Password
|
||||
{
|
||||
get
|
||||
{
|
||||
return SecureStringHelper.CreateString(SecurePassword);
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
SecurePassword = SecureStringHelper.CreateSecureString(string.IsNullOrEmpty(value) ? string.Empty : value);
|
||||
}
|
||||
}
|
||||
public SecureString SecurePassword
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_unmanagedCodePermission.Demand();
|
||||
return _password == null ? new SecureString() : _password.Copy();
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
if (_password != null)
|
||||
{
|
||||
_password.Clear();
|
||||
_password.Dispose();
|
||||
}
|
||||
_password = null == value ? new SecureString() : value.Copy();
|
||||
}
|
||||
}
|
||||
public string Target
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
return _target;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_target = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
return _description;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_description = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime LastWriteTime
|
||||
{
|
||||
get
|
||||
{
|
||||
return LastWriteTimeUtc.ToLocalTime();
|
||||
}
|
||||
}
|
||||
public DateTime LastWriteTimeUtc
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
return _lastWriteTime;
|
||||
}
|
||||
private set { _lastWriteTime = value; }
|
||||
}
|
||||
|
||||
public CredentialType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
return _type;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_type = value;
|
||||
}
|
||||
}
|
||||
|
||||
public PersistenceType PersistenceType
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckNotDisposed();
|
||||
return _persistanceType;
|
||||
}
|
||||
set
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_persistanceType = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Save()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_unmanagedCodePermission.Demand();
|
||||
|
||||
byte[] passwordBytes = Encoding.Unicode.GetBytes(Password);
|
||||
ValidatePasswordLength(passwordBytes);
|
||||
|
||||
NativeMethods.CREDENTIAL credential = new NativeMethods.CREDENTIAL();
|
||||
credential.TargetName = Target;
|
||||
credential.UserName = Username;
|
||||
credential.CredentialBlob = Marshal.StringToCoTaskMemUni(Password);
|
||||
credential.CredentialBlobSize = passwordBytes.Length;
|
||||
credential.Comment = Description;
|
||||
credential.Type = (int)Type;
|
||||
credential.Persist = (int)PersistenceType;
|
||||
|
||||
bool result = NativeMethods.CredWrite(ref credential, 0);
|
||||
if (!result)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
LastWriteTimeUtc = DateTime.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Save(byte[] passwordBytes)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_unmanagedCodePermission.Demand();
|
||||
|
||||
ValidatePasswordLength(passwordBytes);
|
||||
|
||||
NativeMethods.CREDENTIAL credential = new NativeMethods.CREDENTIAL();
|
||||
credential.TargetName = Target;
|
||||
credential.UserName = Username;
|
||||
IntPtr blob = Marshal.AllocCoTaskMem(passwordBytes.Length);
|
||||
Marshal.Copy(passwordBytes, 0, blob, passwordBytes.Length);
|
||||
credential.CredentialBlob = blob;
|
||||
credential.CredentialBlobSize = passwordBytes.Length;
|
||||
credential.Comment = Description;
|
||||
credential.Type = (int)Type;
|
||||
credential.Persist = (int)PersistenceType;
|
||||
|
||||
bool result = NativeMethods.CredWrite(ref credential, 0);
|
||||
Marshal.FreeCoTaskMem(blob);
|
||||
if (!result)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
LastWriteTimeUtc = DateTime.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Delete()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_unmanagedCodePermission.Demand();
|
||||
|
||||
if (string.IsNullOrEmpty(Target))
|
||||
{
|
||||
throw new InvalidOperationException("Target must be specified to delete a credential.");
|
||||
}
|
||||
|
||||
StringBuilder target = string.IsNullOrEmpty(Target) ? new StringBuilder() : new StringBuilder(Target);
|
||||
bool result = NativeMethods.CredDelete(target, Type, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Load()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_unmanagedCodePermission.Demand();
|
||||
|
||||
IntPtr credPointer;
|
||||
|
||||
bool result = NativeMethods.CredRead(Target, Type, 0, out credPointer);
|
||||
if (!result)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
using (NativeMethods.CriticalCredentialHandle credentialHandle = new NativeMethods.CriticalCredentialHandle(credPointer))
|
||||
{
|
||||
LoadInternal(credentialHandle.GetCredential());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Exists()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
_unmanagedCodePermission.Demand();
|
||||
|
||||
if (string.IsNullOrEmpty(Target))
|
||||
{
|
||||
throw new InvalidOperationException("Target must be specified to check existance of a credential.");
|
||||
}
|
||||
|
||||
using (Credential existing = new Credential { Target = Target, Type = Type })
|
||||
{
|
||||
return existing.Load();
|
||||
}
|
||||
}
|
||||
|
||||
internal void LoadInternal(NativeMethods.CREDENTIAL credential)
|
||||
{
|
||||
Username = credential.UserName;
|
||||
if (credential.CredentialBlobSize > 0)
|
||||
{
|
||||
Password = Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / 2);
|
||||
}
|
||||
Target = credential.TargetName;
|
||||
Type = (CredentialType)credential.Type;
|
||||
PersistenceType = (PersistenceType)credential.Persist;
|
||||
Description = credential.Comment;
|
||||
LastWriteTimeUtc = DateTime.FromFileTimeUtc(credential.LastWritten);
|
||||
}
|
||||
|
||||
static void ValidatePasswordLength(byte[] passwordBytes)
|
||||
{
|
||||
if (passwordBytes.Length > maxPasswordLengthInBytes)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("The password has exceeded " + maxPasswordLengthInBytes + " bytes.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace GitHub.Authentication.CredentialManagement
|
||||
{
|
||||
public class CredentialSet : List<Credential>, IDisposable
|
||||
{
|
||||
bool _disposed;
|
||||
|
||||
public CredentialSet()
|
||||
{
|
||||
}
|
||||
|
||||
public CredentialSet(string target)
|
||||
: this()
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(target, "target");
|
||||
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public string Target { get; set; }
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
// Prevent GC Collection since we have already disposed of this object
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~CredentialSet()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (Count > 0)
|
||||
{
|
||||
ForEach(cred => cred.Dispose());
|
||||
}
|
||||
}
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public CredentialSet Load()
|
||||
{
|
||||
LoadInternal();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void LoadInternal()
|
||||
{
|
||||
uint count;
|
||||
|
||||
var pCredentials = IntPtr.Zero;
|
||||
bool result = NativeMethods.CredEnumerateW(Target, 0, out count, out pCredentials);
|
||||
if (!result)
|
||||
{
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
Trace.WriteLine(string.Format(CultureInfo.InvariantCulture, "Win32Exception: {0}", new Win32Exception(lastError).ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read in all of the pointers first
|
||||
var ptrCredList = new IntPtr[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ptrCredList[i] = Marshal.ReadIntPtr(pCredentials, IntPtr.Size * i);
|
||||
}
|
||||
|
||||
// Now let's go through all of the pointers in the list
|
||||
// and create our Credential object(s)
|
||||
var credentialHandles =
|
||||
ptrCredList.Select(ptrCred => new NativeMethods.CriticalCredentialHandle(ptrCred)).ToList();
|
||||
|
||||
var existingCredentials = credentialHandles
|
||||
.Select(handle => handle.GetCredential())
|
||||
.Select(nativeCredential =>
|
||||
{
|
||||
Credential credential = new Credential();
|
||||
credential.LoadInternal(nativeCredential);
|
||||
return credential;
|
||||
});
|
||||
AddRange(existingCredentials);
|
||||
|
||||
// The individual credentials should not be free'd
|
||||
credentialHandles.ForEach(handle => handle.SetHandleAsInvalid());
|
||||
|
||||
// Clean up memory to the Enumeration pointer
|
||||
NativeMethods.CredFree(pCredentials);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GitHub.Authentication.CredentialManagement
|
||||
{
|
||||
[SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32",
|
||||
Justification = "This is a uint as required by the unmanaged API")]
|
||||
public enum CredentialType : uint
|
||||
{
|
||||
None = 0,
|
||||
Generic = 1,
|
||||
DomainPassword = 2,
|
||||
DomainCertificate = 3,
|
||||
DomainVisiblePassword = 4
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace GitHub.Authentication.CredentialManagement
|
||||
{
|
||||
public static class NativeMethods
|
||||
{
|
||||
public const int CREDUI_MAX_USERNAME_LENGTH = 513;
|
||||
public const int CREDUI_MAX_PASSWORD_LENGTH = 256;
|
||||
public const int CREDUI_MAX_MESSAGE_LENGTH = 32767;
|
||||
public const int CREDUI_MAX_CAPTION_LENGTH = 128;
|
||||
|
||||
[SuppressMessage("Microsoft.Design", "CA1049:TypesThatOwnNativeResourcesShouldBeDisposable"
|
||||
, Justification = "This type needs to be this way for interop")]
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CREDENTIAL
|
||||
{
|
||||
public int Flags;
|
||||
public int Type;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string TargetName;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string Comment;
|
||||
public long LastWritten;
|
||||
public int CredentialBlobSize;
|
||||
[SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources"
|
||||
, Justification = "Need to validate that SafeHandle works properly with native interop")]
|
||||
public IntPtr CredentialBlob;
|
||||
public int Persist;
|
||||
public int AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string TargetAlias;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string UserName;
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes",
|
||||
Justification = "This type is soley for native interop")]
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct CREDUI_INFO
|
||||
{
|
||||
public int cbSize;
|
||||
[SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible"
|
||||
, Justification = "This is needed for native interop")]
|
||||
public IntPtr hwndParent;
|
||||
public string pszMessageText;
|
||||
public string pszCaptionText;
|
||||
[SuppressMessage("Microsoft.Security", "CA2111:PointersShouldNotBeVisible"
|
||||
, Justification = "This is needed for native interop")]
|
||||
public IntPtr hbmBanner;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum WINXP_CREDUI_FLAGS
|
||||
{
|
||||
INCORRECT_PASSWORD = 0x00001,
|
||||
DO_NOT_PERSIST = 0x00002,
|
||||
REQUEST_ADMINISTRATOR = 0x00004,
|
||||
EXCLUDE_CERTIFICATES = 0x00008,
|
||||
REQUIRE_CERTIFICATE = 0x00010,
|
||||
SHOW_SAVE_CHECK_BOX = 0x00040,
|
||||
ALWAYS_SHOW_UI = 0x00080,
|
||||
REQUIRE_SMARTCARD = 0x00100,
|
||||
PASSWORD_ONLY_OK = 0x00200,
|
||||
VALIDATE_USERNAME = 0x00400,
|
||||
COMPLETE_USERNAME = 0x00800,
|
||||
PERSIST = 0x01000,
|
||||
SERVER_CREDENTIAL = 0x04000,
|
||||
EXPECT_CONFIRMATION = 0x20000,
|
||||
GENERIC_CREDENTIALS = 0x40000,
|
||||
USERNAME_TARGET_CREDENTIALS = 0x80000,
|
||||
KEEP_USERNAME = 0x100000,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum WINVISTA_CREDUI_FLAGS
|
||||
{
|
||||
/// <summary>
|
||||
/// The caller is requesting that the credential provider return the user name and password in plain text.
|
||||
/// This value cannot be combined with SECURE_PROMPT.
|
||||
/// </summary>
|
||||
CREDUIWIN_GENERIC = 0x1,
|
||||
/// <summary>
|
||||
/// The Save check box is displayed in the dialog box.
|
||||
/// </summary>
|
||||
CREDUIWIN_CHECKBOX = 0x2,
|
||||
/// <summary>
|
||||
/// Only credential providers that support the authentication package specified by the authPackage parameter should be enumerated.
|
||||
/// This value cannot be combined with CREDUIWIN_IN_CRED_ONLY.
|
||||
/// </summary>
|
||||
CREDUIWIN_AUTHPACKAGE_ONLY = 0x10,
|
||||
/// <summary>
|
||||
/// Only the credentials specified by the InAuthBuffer parameter for the authentication package specified by the authPackage parameter should be enumerated.
|
||||
/// If this flag is set, and the InAuthBuffer parameter is NULL, the function fails.
|
||||
/// This value cannot be combined with CREDUIWIN_AUTHPACKAGE_ONLY.
|
||||
/// </summary>
|
||||
CREDUIWIN_IN_CRED_ONLY = 0x20,
|
||||
/// <summary>
|
||||
/// Credential providers should enumerate only administrators. This value is intended for User Account Control (UAC) purposes only. We recommend that external callers not set this flag.
|
||||
/// </summary>
|
||||
CREDUIWIN_ENUMERATE_ADMINS = 0x100,
|
||||
/// <summary>
|
||||
/// Only the incoming credentials for the authentication package specified by the authPackage parameter should be enumerated.
|
||||
/// </summary>
|
||||
CREDUIWIN_ENUMERATE_CURRENT_USER = 0x200,
|
||||
/// <summary>
|
||||
/// The credential dialog box should be displayed on the secure desktop. This value cannot be combined with CREDUIWIN_GENERIC.
|
||||
/// Windows Vista: This value is not supported until Windows Vista with SP1.
|
||||
/// </summary>
|
||||
CREDUIWIN_SECURE_PROMPT = 0x1000,
|
||||
/// <summary>
|
||||
/// The credential provider should align the credential BLOB pointed to by the refOutAuthBuffer parameter to a 32-bit boundary, even if the provider is running on a 64-bit system.
|
||||
/// </summary>
|
||||
CREDUIWIN_PACK_32_WOW = 0x10000000,
|
||||
}
|
||||
|
||||
internal enum CredUIReturnCodes
|
||||
{
|
||||
NO_ERROR = 0,
|
||||
ERROR_CANCELLED = 1223,
|
||||
ERROR_NO_SUCH_LOGON_SESSION = 1312,
|
||||
ERROR_NOT_FOUND = 1168,
|
||||
ERROR_INVALID_ACCOUNT_NAME = 1315,
|
||||
ERROR_INSUFFICIENT_BUFFER = 122,
|
||||
ERROR_BAD_ARGUMENTS = 160,
|
||||
ERROR_INVALID_PARAMETER = 87,
|
||||
ERROR_INVALID_FLAGS = 1004,
|
||||
}
|
||||
|
||||
internal enum CREDErrorCodes
|
||||
{
|
||||
NO_ERROR = 0,
|
||||
ERROR_NOT_FOUND = 1168,
|
||||
ERROR_NO_SUCH_LOGON_SESSION = 1312,
|
||||
ERROR_INVALID_PARAMETER = 87,
|
||||
ERROR_INVALID_FLAGS = 1004,
|
||||
ERROR_BAD_USERNAME = 2202,
|
||||
SCARD_E_NO_READERS_AVAILABLE = (int)(0x8010002E - 0x100000000),
|
||||
SCARD_E_NO_SMARTCARD = (int)(0x8010000C - 0x100000000),
|
||||
SCARD_W_REMOVED_CARD = (int)(0x80100069 - 0x100000000),
|
||||
SCARD_W_WRONG_CHV = (int)(0x8010006B - 0x100000000)
|
||||
}
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] UInt32 flags);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredFree([In] IntPtr cred);
|
||||
|
||||
[DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredDelete(StringBuilder target, CredentialType type, int flags);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredEnumerateW(string filter, int flag, out uint count, out IntPtr pCredentials);
|
||||
|
||||
[DllImport("credui.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern CredUIReturnCodes CredUIPromptForCredentials(ref CREDUI_INFO creditUR, string targetName, IntPtr reserved1, int iError, StringBuilder userName, int maxUserName, StringBuilder password, int maxPassword, [MarshalAs(UnmanagedType.Bool)] ref bool pfSave, int flags);
|
||||
|
||||
[DllImport("credui.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern CredUIReturnCodes CredUIPromptForWindowsCredentials(ref CREDUI_INFO notUsedHere, int authError, ref uint authPackage, IntPtr InAuthBuffer, uint InAuthBufferSize, out IntPtr refOutAuthBuffer, out uint refOutAuthBufferSize, [MarshalAs(UnmanagedType.Bool)]ref bool fSave, int flags);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
internal static extern void CoTaskMemFree(IntPtr ptr);
|
||||
|
||||
[DllImport("credui.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredPackAuthenticationBuffer(int dwFlags, StringBuilder pszUserName, StringBuilder pszPassword, IntPtr pPackedCredentials, ref int pcbPackedCredentials);
|
||||
|
||||
[DllImport("credui.dll", CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CredUnPackAuthenticationBuffer(int dwFlags, IntPtr pAuthBuffer, uint cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword);
|
||||
|
||||
internal sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
// Set the handle.
|
||||
internal CriticalCredentialHandle(IntPtr preexistingHandle)
|
||||
{
|
||||
SetHandle(preexistingHandle);
|
||||
}
|
||||
|
||||
internal CREDENTIAL GetCredential()
|
||||
{
|
||||
if (!IsInvalid)
|
||||
{
|
||||
// Get the Credential from the mem location
|
||||
return (CREDENTIAL)Marshal.PtrToStructure(handle, typeof(CREDENTIAL));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Invalid CriticalHandle!");
|
||||
}
|
||||
}
|
||||
|
||||
// Perform any specific actions to release the handle in the ReleaseHandle method.
|
||||
// Often, you need to use Pinvoke to make a call into the Win32 API to release the
|
||||
// handle. In this case, however, we can use the Marshal class to release the unmanaged memory.
|
||||
|
||||
override protected bool ReleaseHandle()
|
||||
{
|
||||
// If the handle was set, free it. Return success.
|
||||
if (!IsInvalid)
|
||||
{
|
||||
// NOTE: We should also ZERO out the memory allocated to the handle, before free'ing it
|
||||
// so there are no traces of the sensitive data left in memory.
|
||||
CredFree(handle);
|
||||
// Mark the handle as invalid for future users.
|
||||
SetHandleAsInvalid();
|
||||
return true;
|
||||
}
|
||||
// Return false.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace GitHub.Authentication.CredentialManagement
|
||||
{
|
||||
[SuppressMessage("Microsoft.Design", "CA1028:EnumStorageShouldBeInt32"
|
||||
, Justification = "This is this way for interop")]
|
||||
[SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue"
|
||||
, Justification = "I assume this is defined this way for Native interop")]
|
||||
public enum PersistenceType : uint
|
||||
{
|
||||
Session = 1,
|
||||
LocalComputer = 2,
|
||||
Enterprise = 3
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace GitHub.Authentication.CredentialManagement
|
||||
{
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
internal static class SecureStringHelper
|
||||
{
|
||||
// Methods
|
||||
internal static SecureString CreateSecureString(string plainString)
|
||||
{
|
||||
var str = new SecureString();
|
||||
if (!string.IsNullOrEmpty(plainString))
|
||||
{
|
||||
foreach (char c in plainString)
|
||||
{
|
||||
str.AppendChar(c);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
internal static string CreateString(SecureString secureString)
|
||||
{
|
||||
string str;
|
||||
var zero = IntPtr.Zero;
|
||||
if ((secureString == null) || (secureString.Length == 0))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
try
|
||||
{
|
||||
zero = Marshal.SecureStringToBSTR(secureString);
|
||||
str = Marshal.PtrToStringBSTR(zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (zero != IntPtr.Zero)
|
||||
{
|
||||
Marshal.ZeroFreeBSTR(zero);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
using Akavache;
|
||||
using GitHub.Authentication.CredentialManagement;
|
||||
using GitHub.Helpers;
|
||||
|
||||
namespace GitHub.Caches
|
||||
{
|
||||
public class CredentialCache : ISecureBlobCache, IObjectBlobCache
|
||||
{
|
||||
public IScheduler Scheduler { get; protected set; }
|
||||
|
||||
readonly AsyncSubject<Unit> shutdown = new AsyncSubject<Unit>();
|
||||
public IObservable<Unit> Shutdown { get { return shutdown; } }
|
||||
|
||||
bool disposed = false;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Scheduler = null;
|
||||
shutdown.OnNext(Unit.Default);
|
||||
shutdown.OnCompleted();
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
public IObservable<Unit> Flush()
|
||||
{
|
||||
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<byte[]> Get(string key)
|
||||
{
|
||||
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<byte[]>("CredentialCache");
|
||||
|
||||
var keyHost = GetKeyHost(key);
|
||||
using (var credential = new Credential())
|
||||
{
|
||||
credential.Target = keyHost;
|
||||
credential.Type = CredentialType.Generic;
|
||||
if (credential.Load())
|
||||
return Observable.Return(Encoding.Unicode.GetBytes(credential.Password));
|
||||
}
|
||||
return ExceptionHelper.ObservableThrowKeyNotFoundException<byte[]>(key);
|
||||
}
|
||||
|
||||
public IObservable<IEnumerable<string>> GetAllKeys()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IObservable<DateTimeOffset?> GetCreatedAt(string key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IObservable<Unit> Insert(string key, byte[] data, DateTimeOffset? absoluteExpiration = default(DateTimeOffset?))
|
||||
{
|
||||
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(key);
|
||||
}
|
||||
|
||||
public IObservable<Unit> Invalidate(string key)
|
||||
{
|
||||
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
|
||||
|
||||
var keyGit = GetKeyGit(key);
|
||||
if (!DeleteKey(keyGit))
|
||||
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(keyGit);
|
||||
|
||||
var keyHost = GetKeyHost(key);
|
||||
if (!DeleteKey(keyHost))
|
||||
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(keyHost);
|
||||
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateAll()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IObservable<Unit> Vacuum()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// TODO: Use SecureString
|
||||
public IObservable<Unit> InsertObject<T>(string key, T value, DateTimeOffset? absoluteExpiration = default(DateTimeOffset?))
|
||||
{
|
||||
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
|
||||
|
||||
Tuple<string, string> values = value as Tuple<string, string>;
|
||||
if (values == null)
|
||||
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(key);
|
||||
|
||||
var keyGit = GetKeyGit(key);
|
||||
if (!SaveKey(keyGit, values.Item1, values.Item2))
|
||||
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(keyGit);
|
||||
|
||||
var keyHost = GetKeyHost(key);
|
||||
if (!SaveKey(keyHost, values.Item1, values.Item2))
|
||||
return ExceptionHelper.ObservableThrowInvalidOperationException<Unit>(keyGit);
|
||||
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<T> GetObject<T>(string key)
|
||||
{
|
||||
if (typeof(T) == typeof(Tuple<string, string>))
|
||||
return (IObservable<T>) GetTuple(key);
|
||||
return ExceptionHelper.ObservableThrowInvalidOperationException<T>(key);
|
||||
}
|
||||
|
||||
IObservable<Tuple<string, string>> GetTuple(string key)
|
||||
{
|
||||
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Tuple<string, string>>("CredentialCache");
|
||||
|
||||
var keyHost = GetKeyHost(key);
|
||||
var ret = GetKey(keyHost);
|
||||
if (ret != null)
|
||||
return Observable.Return(ret);
|
||||
return ExceptionHelper.ObservableThrowKeyNotFoundException<Tuple<string, string>>(keyHost);
|
||||
}
|
||||
|
||||
public IObservable<IEnumerable<T>> GetAllObjects<T>()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IObservable<DateTimeOffset?> GetObjectCreatedAt<T>(string key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateObject<T>(string key)
|
||||
{
|
||||
if (disposed) return ExceptionHelper.ObservableThrowObjectDisposedException<Unit>("CredentialCache");
|
||||
|
||||
var keyGit = GetKeyGit(key);
|
||||
if (!DeleteKey(keyGit))
|
||||
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(keyGit);
|
||||
|
||||
var keyHost = GetKeyHost(key);
|
||||
if (!DeleteKey(keyHost))
|
||||
return ExceptionHelper.ObservableThrowKeyNotFoundException<Unit>(key);
|
||||
|
||||
return Observable.Return(Unit.Default);
|
||||
}
|
||||
|
||||
public IObservable<Unit> InvalidateAllObjects<T>()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
static string FormatKey(string key)
|
||||
{
|
||||
if (key.StartsWith("login:", StringComparison.Ordinal))
|
||||
key = key.Substring("login:".Length);
|
||||
return key;
|
||||
}
|
||||
|
||||
static string GetKeyGit(string key)
|
||||
{
|
||||
key = FormatKey(key);
|
||||
// it appears this is how MS expects the host key
|
||||
if (!key.StartsWith("git:", StringComparison.Ordinal))
|
||||
key = "git:" + key;
|
||||
if (key.EndsWith("/", StringComparison.Ordinal))
|
||||
key = key.Substring(0, key.Length - 1);
|
||||
return key;
|
||||
}
|
||||
|
||||
static string GetKeyHost(string key)
|
||||
{
|
||||
key = FormatKey(key);
|
||||
if (key.StartsWith("git:", StringComparison.Ordinal))
|
||||
key = key.Substring("git:".Length);
|
||||
if (!key.EndsWith("/", StringComparison.Ordinal))
|
||||
key += '/';
|
||||
return key;
|
||||
}
|
||||
|
||||
static bool DeleteKey(string key)
|
||||
{
|
||||
using (var credential = new Credential())
|
||||
{
|
||||
credential.Target = key;
|
||||
if (!credential.Load())
|
||||
return false;
|
||||
return credential.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
static bool SaveKey(string key, string user, string pwd)
|
||||
{
|
||||
using (var credential = new Credential(user, pwd, key))
|
||||
{
|
||||
credential.Type = CredentialType.Generic;
|
||||
credential.PersistenceType = PersistenceType.LocalComputer;
|
||||
return credential.Save();
|
||||
}
|
||||
}
|
||||
|
||||
static Tuple<string, string> GetKey(string key)
|
||||
{
|
||||
using (var credential = new Credential())
|
||||
{
|
||||
credential.Target = key;
|
||||
credential.Type = CredentialType.Generic;
|
||||
if (credential.Load())
|
||||
return new Tuple<string, string>(credential.Username, credential.Password);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ namespace GitHub.Caches
|
|||
//{
|
||||
//}
|
||||
|
||||
public SharedCache() : this(new InMemoryBlobCache(), new InMemoryBlobCache(), new InMemoryBlobCache())
|
||||
public SharedCache() : this(new InMemoryBlobCache(), new InMemoryBlobCache(), new CredentialCache())
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,7 @@ namespace GitHub.Controllers
|
|||
var twofa = uiProvider.GetService<ITwoFactorViewModel>();
|
||||
twofa.WhenAny(x => x.IsShowing, x => x.Value)
|
||||
.Where(x => x)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
Fire(Trigger.Next);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Weavers>
|
||||
<NullGuard/>
|
||||
<NullGuard ExcludeRegex="^GitHub.Authentication.CredentialManagement.*$"/>
|
||||
</Weavers>
|
|
@ -49,6 +49,9 @@
|
|||
<DelaySign>false</DelaySign>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.TeamFoundation.Git.Controls, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\lib\Microsoft.TeamFoundation.Git.Controls.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.Immutable.10.0" />
|
||||
|
@ -105,6 +108,13 @@
|
|||
<Link>Key.snk</Link>
|
||||
</None>
|
||||
<Compile Include="Extensions\FileExtensions.cs" />
|
||||
<Compile Include="Authentication\CredentialManagement\Credential.cs" />
|
||||
<Compile Include="Authentication\CredentialManagement\CredentialSet.cs" />
|
||||
<Compile Include="Authentication\CredentialManagement\CredentialType.cs" />
|
||||
<Compile Include="Authentication\CredentialManagement\NativeMethods.cs" />
|
||||
<Compile Include="Authentication\CredentialManagement\PersistenceType.cs" />
|
||||
<Compile Include="Authentication\CredentialManagement\SecureStringHelper.cs" />
|
||||
<Compile Include="Caches\CredentialCache.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="..\..\script\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
|
@ -167,7 +177,9 @@
|
|||
<Compile Include="ViewModels\TwoFactorDialogViewModel.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="FodyWeavers.xml" />
|
||||
<Content Include="FodyWeavers.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Resource Include="Images\default_org_avatar.png" />
|
||||
<Resource Include="Images\default_user_avatar.png" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -177,7 +177,7 @@ namespace GitHub.Models
|
|||
if (authorization == null || String.IsNullOrWhiteSpace(authorization.Token))
|
||||
return Observable.Return(Unit.Default);
|
||||
|
||||
return LoginCache.SaveLogin(authorization.Token, "x-oauth-basic", Address)
|
||||
return LoginCache.SaveLogin(usernameOrEmail, authorization.Token, Address)
|
||||
.ObserveOn(RxApp.MainThreadScheduler);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,56 +1,103 @@
|
|||
using System;
|
||||
using GitHub.Primitives;
|
||||
using NullGuard;
|
||||
using ReactiveUI;
|
||||
using Octokit;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Globalization;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class RepositoryModel : IRepositoryModel
|
||||
public class RepositoryModel : ReactiveObject, IRepositoryModel
|
||||
{
|
||||
int? id;
|
||||
string name;
|
||||
string description;
|
||||
bool isPrivate = true; // All repos are assumed to be private until proven otherwise.
|
||||
readonly ObservableAsPropertyHelper<string> nameWithOwner;
|
||||
string owner;
|
||||
Uri hostUri;
|
||||
UriString cloneUrl;
|
||||
|
||||
[AllowNull]
|
||||
public string Description
|
||||
{
|
||||
get;
|
||||
set;
|
||||
[return: AllowNull]
|
||||
get { return description; }
|
||||
set { this.RaiseAndSetIfChanged(ref description, value); }
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public HostAddress HostAddress
|
||||
{
|
||||
[return: AllowNull]
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public Uri HostUri
|
||||
{
|
||||
get;
|
||||
set;
|
||||
[return: AllowNull]
|
||||
get { return hostUri; }
|
||||
set { this.RaiseAndSetIfChanged(ref hostUri, value); }
|
||||
}
|
||||
|
||||
public int? Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get { return id; }
|
||||
set { this.RaiseAndSetIfChanged(ref id, value); }
|
||||
}
|
||||
|
||||
public bool IsPrivate
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get { return isPrivate; }
|
||||
set { this.RaiseAndSetIfChanged(ref isPrivate, value); }
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get { return name; }
|
||||
set { this.RaiseAndSetIfChanged(ref name, value); }
|
||||
}
|
||||
|
||||
public string NameWithOwner
|
||||
{
|
||||
get;
|
||||
set;
|
||||
get { return nameWithOwner.Value; }
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public string Owner
|
||||
{
|
||||
get;
|
||||
set;
|
||||
[return: AllowNull]
|
||||
get { return owner; }
|
||||
set { this.RaiseAndSetIfChanged(ref owner, value); }
|
||||
}
|
||||
|
||||
[AllowNull]
|
||||
public UriString CloneUrl
|
||||
{
|
||||
[return: AllowNull]
|
||||
get { return cloneUrl; }
|
||||
set { this.RaiseAndSetIfChanged(ref cloneUrl, value); }
|
||||
}
|
||||
|
||||
public RepositoryModel()
|
||||
{
|
||||
nameWithOwner = this.WhenAny(x => x.Name, x => x.Owner, (name, owner) => new { Name = name.Value, Owner = owner.Value })
|
||||
.Select(x => x.Name != null && x.Owner != null ? String.Format(CultureInfo.InvariantCulture, "{0}/{1}", x.Owner, x.Name) : null)
|
||||
.ToProperty(this, x => x.NameWithOwner, null, Scheduler.Immediate);
|
||||
}
|
||||
|
||||
public RepositoryModel(Repository repo)
|
||||
{
|
||||
Id = repo.Id;
|
||||
Name = repo.Name;
|
||||
Description = repo.Description;
|
||||
Owner = repo.Owner.Login;
|
||||
CloneUrl = repo.CloneUrl;
|
||||
IsPrivate = repo.Private;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -336,6 +336,8 @@ namespace GitHub.SampleData
|
|||
private set;
|
||||
}
|
||||
|
||||
public IRepositoryModel SelectedRepository { get; set; }
|
||||
|
||||
public string Title { get { return "Clone a GitHub Repository"; } }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Input;
|
||||
using GitHub.Exports;
|
||||
using GitHub.Models;
|
||||
using Microsoft.TeamFoundation.Git.Controls.Extensibility;
|
||||
using NullGuard;
|
||||
using Octokit;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.ViewModels
|
||||
{
|
||||
[ExportViewModel(ViewType=UIViewType.Clone)]
|
||||
public class RepositoryCloneViewModel : IRepositoryCloneViewModel
|
||||
public class RepositoryCloneViewModel : ReactiveObject, IRepositoryCloneViewModel
|
||||
{
|
||||
public string Title { get { return "Clone a GitHub Repository"; } } // TODO: this needs to be contextual
|
||||
|
||||
IReactiveCommand<object> cloneCommand = ReactiveCommand.Create();
|
||||
IReactiveCommand<object> cloneCommand;
|
||||
|
||||
public ICommand CloneCommand { get { return cloneCommand; } }
|
||||
|
||||
|
@ -25,8 +28,17 @@ namespace GitHub.ViewModels
|
|||
private set;
|
||||
}
|
||||
|
||||
IRepositoryModel _selectedRepository;
|
||||
[AllowNull]
|
||||
public IRepositoryModel SelectedRepository
|
||||
{
|
||||
[return: AllowNull]
|
||||
get { return _selectedRepository; }
|
||||
set { this.RaiseAndSetIfChanged(ref _selectedRepository, value); }
|
||||
}
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositoryCloneViewModel(IRepositoryHosts hosts)
|
||||
public RepositoryCloneViewModel(IServiceProvider serviceProvider, IRepositoryHosts hosts)
|
||||
{
|
||||
// TODO: How do I know which host this dialog is associated with?
|
||||
// For now, I'll assume GitHub Host.
|
||||
|
@ -35,9 +47,28 @@ namespace GitHub.ViewModels
|
|||
.Catch<User, KeyNotFoundException>(_ => Observable.Empty<User>())
|
||||
.SelectMany(user => hosts.GitHubHost.ApiClient.GetUserRepositories(user.Id))
|
||||
.SelectMany(repo => repo)
|
||||
.Select(repo => new RepositoryModel { Owner = repo.Owner.Login, Name = repo.Name })
|
||||
.Select(repo => new RepositoryModel(repo))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(Repositories.Add);
|
||||
|
||||
cloneCommand = ReactiveCommand.CreateAsyncObservable(_ =>
|
||||
{
|
||||
var repo = SelectedRepository;
|
||||
var uri = repo.CloneUrl;
|
||||
return Observable.Start<object>(() =>
|
||||
{
|
||||
var gitExt = serviceProvider.GetService(typeof(IGitRepositoriesExt)) as IGitRepositoriesExt;
|
||||
Debug.Assert(gitExt != null, "Could not get an instance of IGitRepositoriesExt");
|
||||
|
||||
// TODO: use VS default dir for projects (https://github.com/github/VisualStudio/issues/96)
|
||||
var tmp = System.IO.Path.GetTempFileName();
|
||||
System.IO.File.Delete(tmp);
|
||||
var tmpname = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(tmp), System.IO.Path.GetFileNameWithoutExtension(tmp));
|
||||
System.IO.Directory.CreateDirectory(tmpname);
|
||||
gitExt.Clone(uri.ToString(), tmpname, CloneOptions.RecurseSubmodule);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,8 @@
|
|||
<None Include="..\..\script\Key.snk">
|
||||
<Link>Key.snk</Link>
|
||||
</None>
|
||||
<Compile Include="Helpers\ExceptionHelper.cs" />
|
||||
<Compile Include="Helpers\Guard.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="..\..\script\SolutionInfo.cs">
|
||||
<Link>Properties\SolutionInfo.cs</Link>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Helpers
|
||||
{
|
||||
public static class ExceptionHelper
|
||||
{
|
||||
public static IObservable<T> ObservableThrowKeyNotFoundException<T>(string key, Exception innerException = null)
|
||||
{
|
||||
return Observable.Throw<T>(
|
||||
new KeyNotFoundException(String.Format(CultureInfo.InvariantCulture,
|
||||
"The given key '{0}' was not present in the cache.", key), innerException));
|
||||
}
|
||||
|
||||
public static IObservable<T> ObservableThrowObjectDisposedException<T>(string obj, Exception innerException = null)
|
||||
{
|
||||
return Observable.Throw<T>(
|
||||
new ObjectDisposedException(String.Format(CultureInfo.InvariantCulture,
|
||||
"The cache '{0}' was disposed.", obj), innerException));
|
||||
}
|
||||
|
||||
public static IObservable<T> ObservableThrowInvalidOperationException<T>(string obj, Exception innerException = null)
|
||||
{
|
||||
return Observable.Throw<T>(
|
||||
new InvalidOperationException(obj, innerException));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Splat;
|
||||
|
||||
namespace GitHub
|
||||
{
|
||||
public static class Guard
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that the string is not empty.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public static void ArgumentNotEmptyString(string value, string name)
|
||||
{
|
||||
// We already know the value is not null because of NullGuard.Fody.
|
||||
if (!string.IsNullOrWhiteSpace(value)) return;
|
||||
|
||||
string message = string.Format(CultureInfo.InvariantCulture, "The value for '{0}' must not be empty", name);
|
||||
#if DEBUG
|
||||
if (!ModeDetector.InUnitTestRunner())
|
||||
{
|
||||
Debug.Fail(message);
|
||||
}
|
||||
#endif
|
||||
throw new ArgumentException("String cannot be empty", name);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
internal sealed class ValidatedNotNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,5 +13,6 @@ namespace GitHub.Models
|
|||
string Description { get; }
|
||||
Uri HostUri { get; }
|
||||
bool IsPrivate { get; }
|
||||
UriString CloneUrl { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,14 +44,16 @@ namespace GitHub.Primitives
|
|||
{
|
||||
WebUri = new Uri(enterpriseUri, new Uri("/", UriKind.Relative));
|
||||
ApiUri = new Uri(enterpriseUri, new Uri("/api/v3/", UriKind.Relative));
|
||||
CredentialCacheKeyHost = ApiUri.Host;
|
||||
//CredentialCacheKeyHost = ApiUri.Host;
|
||||
CredentialCacheKeyHost = WebUri.ToString();
|
||||
}
|
||||
|
||||
private HostAddress()
|
||||
{
|
||||
WebUri = new Uri("https://github.com");
|
||||
ApiUri = new Uri("https://api.github.com");
|
||||
CredentialCacheKeyHost = "github.com";
|
||||
//CredentialCacheKeyHost = "github.com";
|
||||
CredentialCacheKeyHost = WebUri.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace GitHub.Services
|
|||
public interface IUIProvider
|
||||
{
|
||||
ExportProvider ExportProvider { get; }
|
||||
IServiceProvider GitServiceProvider { get; set; }
|
||||
object GetService(Type t);
|
||||
T GetService<T>();
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
|
||||
|
|
|
@ -18,5 +18,7 @@ namespace GitHub.ViewModels
|
|||
/// The list of repositories the current user may clone from the specified host.
|
||||
/// </summary>
|
||||
ICollection<IRepositoryModel> Repositories { get; }
|
||||
|
||||
IRepositoryModel SelectedRepository { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@
|
|||
<Reference Include="Microsoft.TeamFoundation.Controls, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Microsoft.TeamFoundation.Controls.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.TeamFoundation.Git.Controls, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\lib\Microsoft.TeamFoundation.Git.Controls.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.CoreUtility, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
<Reference Include="Microsoft.VisualStudio.Shell.14.0, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
|
|
|
@ -19,6 +19,16 @@ namespace GitHub.VisualStudio
|
|||
[AllowNull]
|
||||
public ExportProvider ExportProvider { get; private set; }
|
||||
|
||||
IServiceProvider gitServiceProvider;
|
||||
[AllowNull]
|
||||
public IServiceProvider GitServiceProvider {
|
||||
get { return gitServiceProvider; }
|
||||
set {
|
||||
if (gitServiceProvider == null)
|
||||
gitServiceProvider = value;
|
||||
}
|
||||
}
|
||||
|
||||
readonly IServiceProvider serviceProvider;
|
||||
|
||||
[ImportingConstructor]
|
||||
|
@ -43,6 +53,13 @@ namespace GitHub.VisualStudio
|
|||
if (instance != null)
|
||||
return instance;
|
||||
|
||||
if (gitServiceProvider != null)
|
||||
{
|
||||
instance = gitServiceProvider.GetService(serviceType);
|
||||
if (instance != null)
|
||||
return instance;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
|
||||
"Could not locate any instances of contract {0}.", contract));
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ using GitHub.VisualStudio.UI;
|
|||
using GitHub.VisualStudio.UI.Views;
|
||||
using Microsoft.TeamFoundation.Controls;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using Microsoft.TeamFoundation.Git.Controls.Extensibility;
|
||||
|
||||
namespace GitHub.VisualStudio.TeamExplorerConnect
|
||||
{
|
||||
|
@ -15,6 +16,8 @@ namespace GitHub.VisualStudio.TeamExplorerConnect
|
|||
{
|
||||
public const string PlaceholderGitHubSectionId = "519B47D3-F2A9-4E19-8491-8C9FA25ABE97";
|
||||
|
||||
IServiceProvider gitServiceProvider;
|
||||
|
||||
protected GitHubConnectContent View
|
||||
{
|
||||
get { return this.SectionContent as GitHubConnectContent; }
|
||||
|
@ -34,12 +37,20 @@ namespace GitHub.VisualStudio.TeamExplorerConnect
|
|||
View.ViewModel = this;
|
||||
}
|
||||
|
||||
public override void Initialize(object sender, SectionInitializeEventArgs e)
|
||||
{
|
||||
base.Initialize(sender, e);
|
||||
|
||||
gitServiceProvider = e.ServiceProvider;
|
||||
}
|
||||
|
||||
|
||||
public void DoCreate()
|
||||
{
|
||||
// this is done here and not via the constructor so nothing gets loaded
|
||||
// until we get here
|
||||
var ui = ServiceProvider.GetExportedValue<IUIProvider>();
|
||||
|
||||
ui.GitServiceProvider = gitServiceProvider;
|
||||
var factory = ui.GetService<ExportFactoryProvider>();
|
||||
var d = factory.UIControllerFactory.CreateExport();
|
||||
var creation = d.Value.SelectFlow(UIControllerFlow.Create);
|
||||
|
@ -55,7 +66,7 @@ namespace GitHub.VisualStudio.TeamExplorerConnect
|
|||
// this is done here and not via the constructor so nothing gets loaded
|
||||
// until we get here
|
||||
var ui = ServiceProvider.GetExportedValue<IUIProvider>();
|
||||
|
||||
ui.GitServiceProvider = gitServiceProvider;
|
||||
var factory = ui.GetService<ExportFactoryProvider>();
|
||||
var d = factory.UIControllerFactory.CreateExport();
|
||||
var creation = d.Value.SelectFlow(UIControllerFlow.Clone);
|
||||
|
|
|
@ -32,6 +32,7 @@ namespace GitHub.VisualStudio.UI.Views.Controls
|
|||
this.WhenActivated(d =>
|
||||
{
|
||||
d(this.OneWayBind(ViewModel, vm => vm.Repositories, v => v.repositoryList.ItemsSource, CreateRepositoryListCollectionView));
|
||||
d(this.Bind(ViewModel, vm => vm.SelectedRepository, v => v.repositoryList.SelectedItem));
|
||||
d(this.BindCommand(ViewModel, vm => vm.CloneCommand, v => v.cloneButton));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 5d32f2581c1b0ce38c969c9ea63cf60c4562dee8
|
||||
Subproject commit b83b3ff68796901378bdc215bed6c7bbe3d996f0
|
Загрузка…
Ссылка в новой задаче