Skip to content

Commit 5c8d929

Browse files
committed
github: add in-proc UI implementation
1 parent 4a8d5e4 commit 5c8d929

File tree

2 files changed

+103
-11
lines changed

2 files changed

+103
-11
lines changed

src/shared/GitHub.Tests/GitHubAuthenticationTests.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Threading.Tasks;
5+
using Avalonia.Animation;
56
using GitCredentialManager;
67
using GitCredentialManager.Tests.Objects;
78
using Moq;
@@ -78,10 +79,14 @@ public async Task GitHubAuthentication_GetAuthenticationAsync_AuthenticationMode
7879
[Fact]
7980
public async Task GitHubAuthentication_GetAuthenticationAsync_Helper_Basic()
8081
{
82+
const string unixHelperPath = "/usr/local/bin/GitHub.UI";
83+
const string windowsHelperPath = @"C:\Program Files\Git Credential Manager\GitHub.UI.exe";
84+
string helperPath = PlatformUtils.IsWindows() ? windowsHelperPath : unixHelperPath;
85+
8186
var context = new TestCommandContext();
82-
context.FileSystem.Files["/usr/local/bin/GitHub.UI"] = new byte[0];
83-
context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\GitHub.UI.exe"] = new byte[0];
87+
context.FileSystem.Files[helperPath] = Array.Empty<byte>();
8488
context.SessionManager.IsDesktopSession = true;
89+
context.Environment.Variables[GitHubConstants.EnvironmentVariables.AuthenticationHelper] = helperPath;
8590
var auth = new Mock<GitHubAuthentication>(MockBehavior.Strict, context);
8691
auth.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), "prompt --all", It.IsAny<IDictionary<string, string>>(), It.IsAny<System.Threading.CancellationToken>()))
8792
.Returns(Task.FromResult<IDictionary<string, string>>(

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
using GitCredentialManager;
88
using GitCredentialManager.Authentication;
99
using GitCredentialManager.Authentication.OAuth;
10+
using GitCredentialManager.UI;
11+
using GitHub.UI.ViewModels;
12+
using GitHub.UI.Views;
1013

1114
namespace GitHub
1215
{
@@ -86,15 +89,69 @@ public async Task<AuthenticationPromptResult> GetAuthenticationAsync(Uri targetU
8689

8790
ThrowIfUserInteractionDisabled();
8891

89-
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
90-
TryFindHelperCommand(out string command, out string args))
92+
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
9193
{
92-
return await GetAuthenticationViaHelperAsync(targetUri, userName, modes, command, args);
94+
if (TryFindHelperCommand(out string command, out string args))
95+
{
96+
return await GetAuthenticationViaHelperAsync(targetUri, userName, modes, command, args);
97+
}
98+
99+
return await GetAuthenticationViaUiAsync(targetUri, userName, modes);
93100
}
94101

95102
return GetAuthenticationViaTty(targetUri, userName, modes);
96103
}
97104

105+
private async Task<AuthenticationPromptResult> GetAuthenticationViaUiAsync(
106+
Uri targetUri, string userName, AuthenticationModes modes)
107+
{
108+
var viewModel = new CredentialsViewModel(Context.Environment, Context.ProcessManager)
109+
{
110+
ShowBrowserLogin = (modes & AuthenticationModes.Browser) != 0,
111+
ShowDeviceLogin = (modes & AuthenticationModes.Device) != 0,
112+
ShowTokenLogin = (modes & AuthenticationModes.Pat) != 0,
113+
ShowBasicLogin = (modes & AuthenticationModes.Basic) != 0,
114+
};
115+
116+
if (!GitHubHostProvider.IsGitHubDotCom(targetUri))
117+
{
118+
viewModel.EnterpriseUrl = targetUri.ToString();
119+
}
120+
121+
if (!string.IsNullOrWhiteSpace(userName))
122+
{
123+
viewModel.UserName = userName;
124+
}
125+
126+
await AvaloniaUi.ShowViewAsync<CredentialsView>(viewModel, GetParentWindowHandle(), CancellationToken.None);
127+
128+
ThrowIfWindowCancelled(viewModel);
129+
130+
switch (viewModel.SelectedMode)
131+
{
132+
case AuthenticationModes.Basic:
133+
return new AuthenticationPromptResult(
134+
AuthenticationModes.Basic,
135+
new GitCredential(viewModel.UserName, viewModel.Password)
136+
);
137+
138+
case AuthenticationModes.Browser:
139+
return new AuthenticationPromptResult(AuthenticationModes.Browser);
140+
141+
case AuthenticationModes.Device:
142+
return new AuthenticationPromptResult(AuthenticationModes.Device);
143+
144+
case AuthenticationModes.Pat:
145+
return new AuthenticationPromptResult(
146+
AuthenticationModes.Pat,
147+
new GitCredential(userName, viewModel.Password)
148+
);
149+
150+
default:
151+
throw new ArgumentOutOfRangeException();
152+
}
153+
}
154+
98155
private AuthenticationPromptResult GetAuthenticationViaTty(Uri targetUri, string userName, AuthenticationModes modes)
99156
{
100157
ThrowIfTerminalPromptsDisabled();
@@ -229,15 +286,33 @@ public async Task<string> GetTwoFactorCodeAsync(Uri targetUri, bool isSms)
229286
{
230287
ThrowIfUserInteractionDisabled();
231288

232-
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
233-
TryFindHelperCommand(out string command, out string args))
289+
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
234290
{
235-
return await GetTwoFactorCodeViaHelperAsync(isSms, args, command);
291+
if (TryFindHelperCommand(out string command, out string args))
292+
{
293+
return await GetTwoFactorCodeViaHelperAsync(isSms, args, command);
294+
}
295+
296+
return await GetTwoFactorCodeViaUiAsync(targetUri, isSms);
236297
}
237298

238299
return GetTwoFactorCodeViaTty(isSms);
239300
}
240301

302+
private async Task<string> GetTwoFactorCodeViaUiAsync(Uri targetUri, bool isSms)
303+
{
304+
var viewModel = new TwoFactorViewModel(Context.Environment, Context.ProcessManager)
305+
{
306+
IsSms = isSms
307+
};
308+
309+
await AvaloniaUi.ShowViewAsync<TwoFactorView>(viewModel, GetParentWindowHandle(), CancellationToken.None);
310+
311+
ThrowIfWindowCancelled(viewModel);
312+
313+
return viewModel.Code;
314+
}
315+
241316
private string GetTwoFactorCodeViaTty(bool isSms)
242317
{
243318
ThrowIfTerminalPromptsDisabled();
@@ -314,14 +389,15 @@ public async Task<OAuth2TokenResult> GetOAuthTokenViaDeviceCodeAsync(Uri targetU
314389
OAuth2DeviceCodeResult dcr = await oauthClient.GetDeviceCodeAsync(scopes, CancellationToken.None);
315390

316391
// If we have a desktop session show the device code in a dialog
317-
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
318-
TryFindHelperCommand(out string command, out string args))
392+
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
319393
{
320394
var promptCts = new CancellationTokenSource();
321395
var tokenCts = new CancellationTokenSource();
322396

323397
// Show the dialog with the device code but don't await its closure
324-
Task promptTask = ShowDeviceCodeViaHelperAsync(dcr, command, args, promptCts.Token);
398+
Task promptTask = TryFindHelperCommand(out string command, out string args)
399+
? ShowDeviceCodeViaHelperAsync(dcr, command, args, promptCts.Token)
400+
: ShowDeviceCodeViaUiAsync(dcr, promptCts.Token);
325401

326402
// Start the request for an OAuth token but don't wait
327403
Task<OAuth2TokenResult> tokenTask = oauthClient.GetTokenByDeviceCodeAsync(dcr, tokenCts.Token);
@@ -354,6 +430,17 @@ public async Task<OAuth2TokenResult> GetOAuthTokenViaDeviceCodeAsync(Uri targetU
354430
return await GetOAuthTokenViaDeviceCodeViaTtyAsync(oauthClient, dcr);
355431
}
356432

433+
private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationToken ct)
434+
{
435+
var viewModel = new DeviceCodeViewModel(Context.Environment)
436+
{
437+
UserCode = dcr.UserCode,
438+
VerificationUrl = dcr.VerificationUri.ToString(),
439+
};
440+
441+
return AvaloniaUi.ShowViewAsync<DeviceCodeView>(viewModel, GetParentWindowHandle(), ct);
442+
}
443+
357444
private async Task<OAuth2TokenResult> GetOAuthTokenViaDeviceCodeViaTtyAsync(GitHubOAuth2Client oauthClient, OAuth2DeviceCodeResult dcr)
358445
{
359446
ThrowIfTerminalPromptsDisabled();

0 commit comments

Comments
 (0)