diff --git a/Git-Credential-Manager.sln b/Git-Credential-Manager.sln
index 39248b52c..75e1254b7 100644
--- a/Git-Credential-Manager.sln
+++ b/Git-Credential-Manager.sln
@@ -41,18 +41,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packaging.Linux", "src\linu
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "linux", "linux", "{8F9D7E67-7DD7-4E32-9134-423281AF00E9}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\shared\GitHub.UI\GitHub.UI.csproj", "{B5F00B46-FE93-45F2-B283-52B74B3E13B9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI", "src\shared\Atlassian.Bitbucket.UI\Atlassian.Bitbucket.UI.csproj", "{EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.UI", "src\shared\Core.UI\Core.UI.csproj", "{001846B0-462B-4A27-90CD-2435D4C0F680}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.UI.Avalonia", "src\shared\Core.UI.Avalonia\Core.UI.Avalonia.csproj", "{DE620324-250C-4262-BA13-198FA6FDB82A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Avalonia", "src\shared\GitHub.UI.Avalonia\GitHub.UI.Avalonia.csproj", "{459501A8-31E6-41CB-BE54-D31FFF4B2007}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI.Avalonia", "src\shared\Atlassian.Bitbucket.UI.Avalonia\Atlassian.Bitbucket.UI.Avalonia.csproj", "{714ACBE7-0C69-4D8A-9224-22792CAA8264}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.Windows", "src\windows\GitHub.UI.Windows\GitHub.UI.Windows.csproj", "{0A86ED89-1FC5-42AA-925C-4578FA30607A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atlassian.Bitbucket.UI.Windows", "src\windows\Atlassian.Bitbucket.UI.Windows\Atlassian.Bitbucket.UI.Windows.csproj", "{3F015046-DAF2-4D2A-96EC-F9782F169E45}"
@@ -61,14 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab", "src\shared\GitLab
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.Tests", "src\shared\GitLab.Tests\GitLab.Tests.csproj", "{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI", "src\shared\GitLab.UI\GitLab.UI.csproj", "{9AFD88E2-7E2C-46DA-9D38-4342086426D3}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI.Avalonia", "src\shared\GitLab.UI.Avalonia\GitLab.UI.Avalonia.csproj", "{47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitLab.UI.Windows", "src\windows\GitLab.UI.Windows\GitLab.UI.Windows.csproj", "{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Git-Credential-Manager.UI.Avalonia", "src\shared\Git-Credential-Manager.UI.Avalonia\Git-Credential-Manager.UI.Avalonia.csproj", "{35659127-8859-4DB9-8DD6-A08C1952632E}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Git-Credential-Manager.UI.Windows", "src\windows\Git-Credential-Manager.UI.Windows\Git-Credential-Manager.UI.Windows.csproj", "{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}"
EndProject
Global
@@ -283,102 +265,6 @@ Global
{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
{AD2A935F-3720-4802-8119-6A9B35B254DF}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{AD2A935F-3720-4802-8119-6A9B35B254DF}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.Release|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.Release|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {001846B0-462B-4A27-90CD-2435D4C0F680}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.Release|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {DE620324-250C-4262-BA13-198FA6FDB82A}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.Release|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {459501A8-31E6-41CB-BE54-D31FFF4B2007}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.Release|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {714ACBE7-0C69-4D8A-9224-22792CAA8264}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{0A86ED89-1FC5-42AA-925C-4578FA30607A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A86ED89-1FC5-42AA-925C-4578FA30607A}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
{0A86ED89-1FC5-42AA-925C-4578FA30607A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -431,38 +317,6 @@ Global
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.MacRelease|Any CPU.Build.0 = Release|Any CPU
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.Release|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.Release|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -473,22 +327,6 @@ Global
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacDebug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.Release|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxDebug|Any CPU.ActiveCfg = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxDebug|Any CPU.Build.0 = Debug|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxRelease|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.LinuxRelease|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.MacRelease|Any CPU.Build.0 = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
- {35659127-8859-4DB9-8DD6-A08C1952632E}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
@@ -522,20 +360,11 @@ Global
{2B3CD8FF-84A6-4B53-A28B-D7A75B0AB4D7} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{8F9D7E67-7DD7-4E32-9134-423281AF00E9} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}
{AD2A935F-3720-4802-8119-6A9B35B254DF} = {8F9D7E67-7DD7-4E32-9134-423281AF00E9}
- {B5F00B46-FE93-45F2-B283-52B74B3E13B9} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {EB1AA840-6FFF-4464-A9B2-0AEA36F615EA} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {001846B0-462B-4A27-90CD-2435D4C0F680} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {DE620324-250C-4262-BA13-198FA6FDB82A} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {459501A8-31E6-41CB-BE54-D31FFF4B2007} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {714ACBE7-0C69-4D8A-9224-22792CAA8264} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{0A86ED89-1FC5-42AA-925C-4578FA30607A} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{3F015046-DAF2-4D2A-96EC-F9782F169E45} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
{570897DC-A85C-4598-B793-9A00CF710119} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{1AF9F7C5-FA2E-48F1-B216-4D5E9A27F393} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {9AFD88E2-7E2C-46DA-9D38-4342086426D3} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
- {47186A50-8889-4FC7-8A05-F9FCE7F8F4AE} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{83EAC1F9-8E1F-41FC-8FC9-2C452452D64E} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
- {35659127-8859-4DB9-8DD6-A08C1952632E} = {D5277A0E-997E-453A-8CB9-4EFCC8B16A29}
{01BF56EC-AAC1-4BCA-8204-EE51D968DF5C} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/src/linux/Packaging.Linux/Packaging.Linux.csproj b/src/linux/Packaging.Linux/Packaging.Linux.csproj
index e14eef0a4..edb7ef314 100644
--- a/src/linux/Packaging.Linux/Packaging.Linux.csproj
+++ b/src/linux/Packaging.Linux/Packaging.Linux.csproj
@@ -17,10 +17,6 @@
-
-
-
-
diff --git a/src/linux/Packaging.Linux/layout.sh b/src/linux/Packaging.Linux/layout.sh
index 0f2c8ab6a..74b76a313 100755
--- a/src/linux/Packaging.Linux/layout.sh
+++ b/src/linux/Packaging.Linux/layout.sh
@@ -35,10 +35,6 @@ ROOT="$( cd "$THISDIR"/../../.. ; pwd -P )"
SRC="$ROOT/src"
OUT="$ROOT/out"
GCM_SRC="$SRC/shared/Git-Credential-Manager"
-GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia"
-BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia"
-GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia"
-GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia"
PROJ_OUT="$OUT/linux/Packaging.Linux"
# Build parameters
@@ -81,42 +77,6 @@ $DOTNET_ROOT/dotnet publish "$GCM_SRC" \
-p:PublishSingleFile=true \
--output="$(make_absolute "$PAYLOAD")" || exit 1
-echo "Publishing core UI helper..."
-$DOTNET_ROOT/dotnet publish "$GCM_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing Bitbucket UI helper..."
-$DOTNET_ROOT/dotnet publish "$BITBUCKET_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitHub UI helper..."
-$DOTNET_ROOT/dotnet publish "$GITHUB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitLab UI helper..."
-$DOTNET_ROOT/dotnet publish "$GITLAB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained=true \
- -p:PublishSingleFile=true \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
# Collect symbols
echo "Collecting managed symbols..."
mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1
diff --git a/src/osx/Installer.Mac/Installer.Mac.csproj b/src/osx/Installer.Mac/Installer.Mac.csproj
index dd0f33ea8..e55f211e2 100644
--- a/src/osx/Installer.Mac/Installer.Mac.csproj
+++ b/src/osx/Installer.Mac/Installer.Mac.csproj
@@ -13,10 +13,6 @@
-
-
-
-
diff --git a/src/osx/Installer.Mac/layout.sh b/src/osx/Installer.Mac/layout.sh
index 0dc664338..a95eb1f0d 100755
--- a/src/osx/Installer.Mac/layout.sh
+++ b/src/osx/Installer.Mac/layout.sh
@@ -22,9 +22,6 @@ OUT="$ROOT/out"
INSTALLER_SRC="$SRC/osx/Installer.Mac"
GCM_SRC="$SRC/shared/Git-Credential-Manager"
GCM_UI_SRC="$SRC/shared/Git-Credential-Manager.UI.Avalonia"
-BITBUCKET_UI_SRC="$SRC/shared/Atlassian.Bitbucket.UI.Avalonia"
-GITHUB_UI_SRC="$SRC/shared/GitHub.UI.Avalonia"
-GITLAB_UI_SRC="$SRC/shared/GitLab.UI.Avalonia"
# Build parameters
FRAMEWORK=net6.0
@@ -103,38 +100,6 @@ dotnet publish "$GCM_SRC" \
--self-contained \
--output="$(make_absolute "$PAYLOAD")" || exit 1
-echo "Publishing core UI helper..."
-dotnet publish "$GCM_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing Bitbucket UI helper..."
-dotnet publish "$BITBUCKET_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitHub UI helper..."
-dotnet publish "$GITHUB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
-echo "Publishing GitLab UI helper..."
-dotnet publish "$GITLAB_UI_SRC" \
- --configuration="$CONFIGURATION" \
- --framework="$FRAMEWORK" \
- --runtime="$RUNTIME" \
- --self-contained \
- --output="$(make_absolute "$PAYLOAD")" || exit 1
-
# Collect symbols
echo "Collecting managed symbols..."
mv "$PAYLOAD"/*.pdb "$SYMBOLOUT" || exit 1
diff --git a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
index e92cb9061..a4a5e4afb 100644
--- a/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
+++ b/src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs
@@ -51,6 +51,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_ShowsMenu_OAut
{
var context = new TestCommandContext();
context.SessionManager.IsDesktopSession = true; // Allow OAuth mode
+ context.Settings.IsGuiPromptsEnabled = false; // Force text prompts
context.Terminal.Prompts["option (enter for default)"] = "1";
Uri targetUri = null;
@@ -71,6 +72,7 @@ public async Task BitbucketAuthentication_GetCredentialsAsync_All_ShowsMenu_Basi
var context = new TestCommandContext();
context.SessionManager.IsDesktopSession = true; // Allow OAuth mode
+ context.Settings.IsGuiPromptsEnabled = false; // Force text prompts
context.Terminal.Prompts["option (enter for default)"] = "2";
context.Terminal.Prompts["Username"] = username;
context.Terminal.SecretPrompts["Password"] = password;
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj
deleted file mode 100644
index a9185278a..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Atlassian.Bitbucket.UI.Avalonia.csproj
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- WinExe
- net6.0
- osx-x64;linux-x64;osx-arm64
- Atlassian.Bitbucket.UI
- Atlassian.Bitbucket.UI
-
-
-
-
-
-
-
-
-
- TesterWindow.axaml
- Code
-
-
-
-
-
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/CredentialsCommandImpl.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/CredentialsCommandImpl.cs
deleted file mode 100644
index e9be595ad..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Commands/CredentialsCommandImpl.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Atlassian.Bitbucket.UI.ViewModels;
-using Atlassian.Bitbucket.UI.Views;
-using GitCredentialManager;
-using GitCredentialManager.UI;
-
-namespace Atlassian.Bitbucket.UI.Commands
-{
- public class CredentialsCommandImpl : CredentialsCommand
- {
- public CredentialsCommandImpl(ICommandContext context) : base(context) { }
-
- protected override Task ShowAsync(CredentialsViewModel viewModel, CancellationToken ct)
- {
- return AvaloniaUi.ShowViewAsync(viewModel, GetParentHandle(), ct);
- }
- }
-}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml
deleted file mode 100644
index fca8daf48..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
deleted file mode 100644
index b08902fb4..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Controls/TesterWindow.axaml.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using Atlassian.Bitbucket.UI.ViewModels;
-using Atlassian.Bitbucket.UI.Views;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
-using GitCredentialManager;
-using GitCredentialManager.Interop.Linux;
-using GitCredentialManager.Interop.MacOS;
-using GitCredentialManager.Interop.Posix;
-using GitCredentialManager.Interop.Windows;
-using GitCredentialManager.UI.Controls;
-
-namespace Atlassian.Bitbucket.UI.Controls
-{
- public class TesterWindow : Window
- {
- private readonly IEnvironment _environment;
-
- public TesterWindow()
- {
- InitializeComponent();
-#if DEBUG
- this.AttachDevTools();
-#endif
- if (PlatformUtils.IsWindows())
- {
- _environment = new WindowsEnvironment(new WindowsFileSystem());
- }
- else
- {
- IFileSystem fs;
- if (PlatformUtils.IsMacOS())
- {
- fs = new MacOSFileSystem();
- }
- else
- {
- fs = new LinuxFileSystem();
- }
-
- _environment = new PosixEnvironment(fs);
- }
- }
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
-
- private void ShowCredentials(object sender, RoutedEventArgs e)
- {
- var vm = new CredentialsViewModel(_environment)
- {
- ShowOAuth = this.FindControl("showOAuth").IsChecked ?? false,
- ShowBasic = this.FindControl("showBasic").IsChecked ?? false,
- UserName = this.FindControl("username").Text
- };
-
- if (Uri.TryCreate(this.FindControl("url").Text, UriKind.Absolute, out Uri uri))
- {
- vm.Url = uri;
- }
-
- var view = new CredentialsView();
- var window = new DialogWindow(view) {DataContext = vm};
- window.ShowDialog(this);
- }
- }
-}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs b/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs
deleted file mode 100644
index 8bc3a6571..000000000
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Program.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Threading;
-using Atlassian.Bitbucket.UI.Commands;
-using Atlassian.Bitbucket.UI.Controls;
-using Avalonia;
-using GitCredentialManager;
-using GitCredentialManager.UI;
-
-namespace Atlassian.Bitbucket.UI
-{
- public static class Program
- {
- public static void Main(string[] args)
- {
- // If we have no arguments then just start the app with the test window.
- if (args.Length == 0)
- {
- BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
- return;
- }
-
- // Create the dispatcher on the main thread. This is required
- // for some platform UI services such as macOS that mandates
- // all controls are created/accessed on the initial thread
- // created by the process (the process entry thread).
- Dispatcher.Initialize();
-
- // Run AppMain in a new thread and keep the main thread free
- // to process the dispatcher's job queue.
- var appMain = new Thread(AppMain) {Name = nameof(AppMain)};
- appMain.Start(args);
-
- // Process the dispatcher job queue (aka: message pump, run-loop, etc...)
- // We must ensure to run this on the same thread that it was created on
- // (the main thread) so we cannot use any async/await calls between
- // Dispatcher.Create and Run.
- Dispatcher.MainThread.Run();
-
- // Execution should never reach here as AppMain terminates the process on completion.
- throw new InvalidOperationException("Main dispatcher job queue shutdown unexpectedly");
- }
-
- private static void AppMain(object o)
- {
- string[] args = (string[]) o;
-
- // Set the session id (sid) for the helper process, to be
- // used when TRACE2 tracing is enabled.
- ProcessManager.CreateSid();
- using (var context = new CommandContext())
- using (var app = new HelperApplication(context))
- {
- // Initialize TRACE2 system
- context.Trace2.Initialize(DateTimeOffset.UtcNow);
-
- // Write the start and version events
- context.Trace2.Start(context.ApplicationPath, args);
-
- app.RegisterCommand(new CredentialsCommandImpl(context));
-
- int exitCode = app.RunAsync(args)
- .ConfigureAwait(false)
- .GetAwaiter()
- .GetResult();
-
- context.Trace2.Stop(exitCode);
- Environment.Exit(exitCode);
- }
- }
-
- public static AppBuilder BuildAvaloniaApp()
- => AppBuilder.Configure(() => new AvaloniaApp(() => new TesterWindow()))
- .UsePlatformDetect()
- .LogToTrace();
- }
-}
diff --git a/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj b/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj
deleted file mode 100644
index 3a7610643..000000000
--- a/src/shared/Atlassian.Bitbucket.UI/Atlassian.Bitbucket.UI.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- net6.0
- net6.0;net472
- Atlassian.Bitbucket.UI
- Atlassian.Bitbucket.UI.Shared
-
-
-
-
-
-
-
-
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
index 01471edf8..6ebee08b8 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs
@@ -4,9 +4,12 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Atlassian.Bitbucket.UI.ViewModels;
+using Atlassian.Bitbucket.UI.Views;
using GitCredentialManager;
using GitCredentialManager.Authentication;
using GitCredentialManager.Authentication.OAuth;
+using GitCredentialManager.UI;
namespace Atlassian.Bitbucket
{
@@ -69,8 +72,6 @@ public async Task GetCredentialsAsync(Uri targetUri, st
{
ThrowIfUserInteractionDisabled();
- string password;
-
// If we don't have a desktop session/GUI then we cannot offer OAuth since the only
// supported grant is authcode (i.e, using a web browser; device code is not supported).
if (!Context.SessionManager.IsDesktopSession)
@@ -91,106 +92,152 @@ public async Task GetCredentialsAsync(Uri targetUri, st
}
// Shell out to the UI helper and show the Bitbucket u/p prompt
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string helperCommand, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- var promptArgs = new StringBuilder(args);
- promptArgs.Append("prompt");
- if (!BitbucketHelper.IsBitbucketOrg(targetUri))
+ if (TryFindHelperCommand(out string helperCommand, out string args))
{
- promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString()));
+ return await GetCredentialsViaHelperAsync(targetUri, userName, modes, helperCommand, args);
}
- if (!string.IsNullOrWhiteSpace(userName))
- {
- promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
- }
+ return await GetCredentialsViaUiAsync(targetUri, userName, modes);
+ }
- if ((modes & AuthenticationModes.Basic) != 0)
- {
- promptArgs.Append(" --show-basic");
- }
+ return GetCredentialsViaTty(targetUri, userName, modes);
+ }
- if ((modes & AuthenticationModes.OAuth) != 0)
- {
- promptArgs.Append(" --show-oauth");
- }
+ private async Task GetCredentialsViaUiAsync(
+ Uri targetUri, string userName, AuthenticationModes modes)
+ {
+ var viewModel = new CredentialsViewModel(Context.Environment)
+ {
+ Url = targetUri,
+ UserName = userName,
+ ShowOAuth = (modes & AuthenticationModes.OAuth) != 0,
+ ShowBasic = (modes & AuthenticationModes.Basic) != 0
+ };
- IDictionary output = await InvokeHelperAsync(helperCommand, promptArgs.ToString());
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
- if (output.TryGetValue("mode", out string mode) &&
- StringComparer.OrdinalIgnoreCase.Equals(mode, "oauth"))
- {
+ ThrowIfWindowCancelled(viewModel);
+
+ switch (viewModel.SelectedMode)
+ {
+ case AuthenticationModes.OAuth:
return new CredentialsPromptResult(AuthenticationModes.OAuth);
- }
- else
- {
- if (!output.TryGetValue("username", out userName))
+
+ case AuthenticationModes.Basic:
+ return new CredentialsPromptResult(
+ AuthenticationModes.Basic,
+ new GitCredential(viewModel.UserName, viewModel.Password)
+ );
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(AuthenticationModes),
+ "Unknown authentication mode", viewModel.SelectedMode.ToString());
+ }
+ }
+
+ private CredentialsPromptResult GetCredentialsViaTty(Uri targetUri, string userName, AuthenticationModes modes)
+ {
+ ThrowIfTerminalPromptsDisabled();
+
+ switch (modes)
+ {
+ case AuthenticationModes.Basic:
+ Context.Terminal.WriteLine("Enter Bitbucket credentials for '{0}'...", targetUri);
+
+ if (!string.IsNullOrWhiteSpace(userName))
{
- throw new Trace2Exception(Context.Trace2, "Missing username in response");
+ // Don't need to prompt for the username if it has been specified already
+ Context.Terminal.WriteLine("Username: {0}", userName);
}
-
- if (!output.TryGetValue("password", out password))
+ else
{
- throw new Trace2Exception(Context.Trace2, "Missing password in response");
+ // Prompt for username
+ userName = Context.Terminal.Prompt("Username");
}
+ // Prompt for password
+ string password = Context.Terminal.PromptSecret("Password");
+
return new CredentialsPromptResult(
AuthenticationModes.Basic,
new GitCredential(userName, password));
- }
- }
- else
- {
- ThrowIfTerminalPromptsDisabled();
- switch (modes)
- {
- case AuthenticationModes.Basic:
- Context.Terminal.WriteLine("Enter Bitbucket credentials for '{0}'...", targetUri);
+ case AuthenticationModes.OAuth:
+ return new CredentialsPromptResult(AuthenticationModes.OAuth);
- if (!string.IsNullOrWhiteSpace(userName))
- {
- // Don't need to prompt for the username if it has been specified already
- Context.Terminal.WriteLine("Username: {0}", userName);
- }
- else
- {
- // Prompt for username
- userName = Context.Terminal.Prompt("Username");
- }
+ case AuthenticationModes.None:
+ throw new ArgumentOutOfRangeException(nameof(modes),
+ @$"At least one {nameof(AuthenticationModes)} must be supplied");
- // Prompt for password
- password = Context.Terminal.PromptSecret("Password");
+ default:
+ var menuTitle = $"Select an authentication method for '{targetUri}'";
+ var menu = new TerminalMenu(Context.Terminal, menuTitle);
- return new CredentialsPromptResult(
- AuthenticationModes.Basic,
- new GitCredential(userName, password));
+ TerminalMenuItem oauthItem = null;
+ TerminalMenuItem basicItem = null;
- case AuthenticationModes.OAuth:
- return new CredentialsPromptResult(AuthenticationModes.OAuth);
+ if ((modes & AuthenticationModes.OAuth) != 0) oauthItem = menu.Add("OAuth");
+ if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add("Username/password");
- case AuthenticationModes.None:
- throw new ArgumentOutOfRangeException(nameof(modes), @$"At least one {nameof(AuthenticationModes)} must be supplied");
+ // Default to the 'first' choice in the menu
+ TerminalMenuItem choice = menu.Show(0);
- default:
- var menuTitle = $"Select an authentication method for '{targetUri}'";
- var menu = new TerminalMenu(Context.Terminal, menuTitle);
+ if (choice == oauthItem) goto case AuthenticationModes.OAuth;
+ if (choice == basicItem) goto case AuthenticationModes.Basic;
- TerminalMenuItem oauthItem = null;
- TerminalMenuItem basicItem = null;
+ throw new Exception();
+ }
+ }
- if ((modes & AuthenticationModes.OAuth) != 0) oauthItem = menu.Add("OAuth");
- if ((modes & AuthenticationModes.Basic) != 0) basicItem = menu.Add("Username/password");
+ private async Task GetCredentialsViaHelperAsync(
+ Uri targetUri, string userName, AuthenticationModes modes, string helperCommand, string args)
+ {
+ var promptArgs = new StringBuilder(args);
+ promptArgs.Append("prompt");
+ if (!BitbucketHelper.IsBitbucketOrg(targetUri))
+ {
+ promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString()));
+ }
- // Default to the 'first' choice in the menu
- TerminalMenuItem choice = menu.Show(0);
+ if (!string.IsNullOrWhiteSpace(userName))
+ {
+ promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
+ }
- if (choice == oauthItem) goto case AuthenticationModes.OAuth;
- if (choice == basicItem) goto case AuthenticationModes.Basic;
+ if ((modes & AuthenticationModes.Basic) != 0)
+ {
+ promptArgs.Append(" --show-basic");
+ }
- throw new Exception();
+ if ((modes & AuthenticationModes.OAuth) != 0)
+ {
+ promptArgs.Append(" --show-oauth");
+ }
+
+ IDictionary output = await InvokeHelperAsync(helperCommand, promptArgs.ToString());
+
+ if (output.TryGetValue("mode", out string mode) &&
+ StringComparer.OrdinalIgnoreCase.Equals(mode, "oauth"))
+ {
+ return new CredentialsPromptResult(AuthenticationModes.OAuth);
+ }
+ else
+ {
+ if (!output.TryGetValue("username", out userName))
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing username in response");
}
+
+ if (!output.TryGetValue("password", out string password))
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing password in response");
+ }
+
+ return new CredentialsPromptResult(
+ AuthenticationModes.Basic,
+ new GitCredential(userName, password));
}
}
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Assets/atlassian-logo.png b/src/shared/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png
similarity index 100%
rename from src/shared/Atlassian.Bitbucket.UI.Avalonia/Assets/atlassian-logo.png
rename to src/shared/Atlassian.Bitbucket/UI/Assets/atlassian-logo.png
diff --git a/src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs b/src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs
similarity index 100%
rename from src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs
rename to src/shared/Atlassian.Bitbucket/UI/Commands/CredentialsCommand.cs
diff --git a/src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs b/src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs
similarity index 100%
rename from src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs
rename to src/shared/Atlassian.Bitbucket/UI/ViewModels/CredentialsViewModel.cs
diff --git a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml b/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
similarity index 95%
rename from src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
rename to src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
index f42feced3..f74161a5c 100644
--- a/src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml
+++ b/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
@@ -2,8 +2,8 @@
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"
- xmlns:vm="clr-namespace:Atlassian.Bitbucket.UI.ViewModels;assembly=Atlassian.Bitbucket.UI.Shared"
- xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcoreuiavn"
+ xmlns:vm="clr-namespace:Atlassian.Bitbucket.UI.ViewModels;assembly=Atlassian.Bitbucket"
+ xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Atlassian.Bitbucket.UI.Views.CredentialsView">
@@ -22,7 +22,7 @@
-
+
();
var auth = new Mock(MockBehavior.Strict, context);
auth.Setup(x => x.InvokeHelperAsync(
@@ -109,19 +119,29 @@ public async Task BasicAuthentication_GetCredentials_DesktopSession_CallsHelper(
}
[Fact]
- public async Task BasicAuthentication_GetCredentials_DesktopSession_UserName_CallsHelper()
+ public async Task BasicAuthentication_GetCredentials_DesktopSession_UIHelper_UserName_CallsHelper()
{
const string testResource = "https://example.com";
const string testUserName = "john.doe";
const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
+ const string unixHelperPath = "/usr/local/bin/git-credential-manager-ui";
+ const string windowsHelperPath = @"C:\Program Files\Git Credential Manager\git-credential-manager-ui.exe";
+ string helperPath = PlatformUtils.IsWindows() ? windowsHelperPath : unixHelperPath;
+
var context = new TestCommandContext
{
- SessionManager = {IsDesktopSession = true}
+ SessionManager = { IsDesktopSession = true },
+ Environment =
+ {
+ Variables =
+ {
+ [Constants.EnvironmentVariables.GcmUiHelper] = helperPath
+ }
+ }
};
- context.FileSystem.Files["/usr/local/bin/git-credential-manager-ui"] = new byte[0];
- context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\git-credential-manager-ui.exe"] = new byte[0];
+ context.FileSystem.Files[helperPath] = Array.Empty();
var auth = new Mock(MockBehavior.Strict, context);
auth.Setup(x => x.InvokeHelperAsync(
diff --git a/src/shared/Core.UI.Avalonia/Assets/DarkBase.axaml b/src/shared/Core.UI.Avalonia/Assets/DarkBase.axaml
deleted file mode 100644
index 13365a69c..000000000
--- a/src/shared/Core.UI.Avalonia/Assets/DarkBase.axaml
+++ /dev/null
@@ -1,5 +0,0 @@
-
- #22272D
-
-
diff --git a/src/shared/Core.UI.Avalonia/Assets/LightBase.axaml b/src/shared/Core.UI.Avalonia/Assets/LightBase.axaml
deleted file mode 100644
index 646fd826f..000000000
--- a/src/shared/Core.UI.Avalonia/Assets/LightBase.axaml
+++ /dev/null
@@ -1,5 +0,0 @@
-
- #FBFBFB
-
-
diff --git a/src/shared/Core.UI.Avalonia/Converters/BoolConvertersEx.cs b/src/shared/Core.UI.Avalonia/Converters/BoolConvertersEx.cs
deleted file mode 100644
index d130473bc..000000000
--- a/src/shared/Core.UI.Avalonia/Converters/BoolConvertersEx.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Linq;
-using Avalonia.Data.Converters;
-
-namespace GitCredentialManager.UI.Converters
-{
- public static class BoolConvertersEx
- {
- public static readonly IMultiValueConverter Or =
- new FuncMultiValueConverter(x => x.Aggregate(false, (a, b) => a || b));
-
- public static readonly IMultiValueConverter And =
- new FuncMultiValueConverter(x => x.Aggregate(true, (a, b) => a && b));
- }
-}
diff --git a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj b/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj
deleted file mode 100644
index 961f67649..000000000
--- a/src/shared/Core.UI.Avalonia/Core.UI.Avalonia.csproj
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- net6.0
- gcmcoreuiavn
- GitCredentialManager.UI
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AvaloniaApp.axaml
- Code
-
-
-
-
diff --git a/src/shared/Core.UI/Core.UI.csproj b/src/shared/Core.UI/Core.UI.csproj
deleted file mode 100644
index fefd97dac..000000000
--- a/src/shared/Core.UI/Core.UI.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net6.0
- net6.0;net472
- gcmcoreui
- GitCredentialManager.UI
-
-
-
-
-
-
-
diff --git a/src/shared/Core/Authentication/AuthenticationBase.cs b/src/shared/Core/Authentication/AuthenticationBase.cs
index c16c3e0a8..594f2be33 100644
--- a/src/shared/Core/Authentication/AuthenticationBase.cs
+++ b/src/shared/Core/Authentication/AuthenticationBase.cs
@@ -5,6 +5,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using GitCredentialManager.UI.ViewModels;
namespace GitCredentialManager.Authentication
{
@@ -109,6 +110,24 @@ protected void ThrowIfTerminalPromptsDisabled()
throw new Trace2InvalidOperationException(Context.Trace2, "Cannot prompt because terminal prompts have been disabled.");
}
}
+
+ protected void ThrowIfWindowCancelled(WindowViewModel viewModel)
+ {
+ if (!viewModel.WindowResult)
+ {
+ throw new Exception("User cancelled dialog.");
+ }
+ }
+
+ protected IntPtr GetParentWindowHandle()
+ {
+ if (int.TryParse(Context.Settings.ParentWindowId, out int id))
+ {
+ return new IntPtr(id);
+ }
+
+ return IntPtr.Zero;
+ }
protected bool TryFindHelperCommand(string envar, string configName, string defaultValue, out string command, out string args)
{
@@ -147,8 +166,22 @@ protected bool TryFindHelperCommand(string envar, string configName, string defa
}
else
{
- Context.Trace.WriteLine($"Using default UI helper: '{defaultValue}'.");
- helperName = defaultValue;
+ // Whilst we evaluate using the Avalonia/in-proc GUIs on Windows we include
+ // a 'fallback' flag that lets us continue to use the WPF out-of-proc helpers.
+ if (PlatformUtils.IsWindows() &&
+ Context.Settings.TryGetSetting(
+ Constants.EnvironmentVariables.GcmDevUseLegacyUiHelpers,
+ Constants.GitConfiguration.Credential.SectionName,
+ Constants.GitConfiguration.Credential.DevUseLegacyUiHelpers,
+ out string str) && str.IsTruthy())
+ {
+ Context.Trace.WriteLine($"Using default legacy UI helper: '{defaultValue}'.");
+ helperName = defaultValue;
+ }
+ else
+ {
+ return false;
+ }
}
//
diff --git a/src/shared/Core/Authentication/BasicAuthentication.cs b/src/shared/Core/Authentication/BasicAuthentication.cs
index b194893d3..7715c27a9 100644
--- a/src/shared/Core/Authentication/BasicAuthentication.cs
+++ b/src/shared/Core/Authentication/BasicAuthentication.cs
@@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
+using GitCredentialManager.UI;
+using GitCredentialManager.UI.ViewModels;
+using GitCredentialManager.UI.Views;
namespace GitCredentialManager.Authentication
{
@@ -34,18 +38,39 @@ public async Task GetCredentialsAsync(string resource, string userN
ThrowIfUserInteractionDisabled();
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string command, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- return await GetCredentialsByUiAsync(command, args, resource, userName);
+ if (TryFindHelperCommand(out string command, out string args))
+ {
+ return await GetCredentialsViaHelperAsync(command, args, resource, userName);
+ }
+
+ return await GetCredentialsViaUiAsync(resource, userName);
}
ThrowIfTerminalPromptsDisabled();
- return GetCredentialsByTty(resource, userName);
+ return GetCredentialsViaTty(resource, userName);
+ }
+
+ private async Task GetCredentialsViaUiAsync(string resource, string userName)
+ {
+ var viewModel = new CredentialsViewModel
+ {
+ Description = !string.IsNullOrWhiteSpace(resource)
+ ? $"Enter your credentials for '{resource}'"
+ : "Enter your credentials",
+ UserName = userName,
+ };
+
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
+
+ ThrowIfWindowCancelled(viewModel);
+
+ return new GitCredential(viewModel.UserName, viewModel.Password);
}
- private ICredential GetCredentialsByTty(string resource, string userName)
+ private ICredential GetCredentialsViaTty(string resource, string userName)
{
Context.Terminal.WriteLine("Enter basic credentials for '{0}':", resource);
@@ -66,7 +91,7 @@ private ICredential GetCredentialsByTty(string resource, string userName)
return new GitCredential(userName, password);
}
- private async Task GetCredentialsByUiAsync(string command, string args, string resource, string userName)
+ private async Task GetCredentialsViaHelperAsync(string command, string args, string resource, string userName)
{
var promptArgs = new StringBuilder(args);
promptArgs.Append("basic");
diff --git a/src/shared/Core/Authentication/OAuthAuthentication.cs b/src/shared/Core/Authentication/OAuthAuthentication.cs
index aa5cc93ee..641baeb08 100644
--- a/src/shared/Core/Authentication/OAuthAuthentication.cs
+++ b/src/shared/Core/Authentication/OAuthAuthentication.cs
@@ -4,6 +4,9 @@
using System.Threading;
using System.Threading.Tasks;
using GitCredentialManager.Authentication.OAuth;
+using GitCredentialManager.UI;
+using GitCredentialManager.UI.ViewModels;
+using GitCredentialManager.UI.Views;
namespace GitCredentialManager.Authentication
{
@@ -57,77 +60,118 @@ public async Task GetAuthenticationModeAsync(
return modes;
}
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string command, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- var promptArgs = new StringBuilder(args);
- promptArgs.Append("oauth");
-
- if (!string.IsNullOrWhiteSpace(resource))
+ if (TryFindHelperCommand(out string command, out string args))
{
- promptArgs.AppendFormat(" --resource {0}", QuoteCmdArg(resource));
+ return await GetAuthenticationModeViaHelperAsync(resource, modes, args, command);
}
- if ((modes & OAuthAuthenticationModes.Browser) != 0)
- {
- promptArgs.Append(" --browser");
- }
+ return await GetAuthenticationModeViaUiAsync(resource, modes);
+ }
- if ((modes & OAuthAuthenticationModes.DeviceCode) != 0)
- {
- promptArgs.Append(" --device-code");
- }
+ return GetAuthenticationModeViaTty(resource, modes);
+ }
- IDictionary resultDict = await InvokeHelperAsync(command, promptArgs.ToString());
+ private async Task GetAuthenticationModeViaUiAsync(string resource, OAuthAuthenticationModes modes)
+ {
+ var viewModel = new OAuthViewModel
+ {
+ Description = !string.IsNullOrWhiteSpace(resource)
+ ? $"Sign in to '{resource}'"
+ : "Select a sign-in option",
+ ShowBrowserLogin = (modes & OAuthAuthenticationModes.Browser) != 0,
+ ShowDeviceCodeLogin = (modes & OAuthAuthenticationModes.DeviceCode) != 0,
+ };
- if (!resultDict.TryGetValue("mode", out string responseMode))
- {
- throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response");
- }
+ await AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
- switch (responseMode.ToLowerInvariant())
- {
- case "browser":
- return OAuthAuthenticationModes.Browser;
+ ThrowIfWindowCancelled(viewModel);
- case "devicecode":
- return OAuthAuthenticationModes.DeviceCode;
+ switch (viewModel.SelectedMode)
+ {
+ case OAuthAuthenticationModes.Browser:
+ return OAuthAuthenticationModes.Browser;
- default:
- throw new Trace2Exception(Context.Trace2,
- $"Unknown mode value in response '{responseMode}'");
- }
+ case OAuthAuthenticationModes.DeviceCode:
+ return OAuthAuthenticationModes.DeviceCode;
+
+ default:
+ throw new ArgumentOutOfRangeException();
}
- else
+ }
+
+ private OAuthAuthenticationModes GetAuthenticationModeViaTty(string resource, OAuthAuthenticationModes modes)
+ {
+ ThrowIfTerminalPromptsDisabled();
+
+ switch (modes)
{
- ThrowIfTerminalPromptsDisabled();
+ case OAuthAuthenticationModes.Browser:
+ return OAuthAuthenticationModes.Browser;
- switch (modes)
- {
- case OAuthAuthenticationModes.Browser:
- return OAuthAuthenticationModes.Browser;
+ case OAuthAuthenticationModes.DeviceCode:
+ return OAuthAuthenticationModes.DeviceCode;
+
+ default:
+ var menuTitle = $"Select an authentication method for '{resource}'";
+ var menu = new TerminalMenu(Context.Terminal, menuTitle);
- case OAuthAuthenticationModes.DeviceCode:
- return OAuthAuthenticationModes.DeviceCode;
+ TerminalMenuItem browserItem = null;
+ TerminalMenuItem deviceItem = null;
- default:
- var menuTitle = $"Select an authentication method for '{resource}'";
- var menu = new TerminalMenu(Context.Terminal, menuTitle);
+ if ((modes & OAuthAuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser");
+ if ((modes & OAuthAuthenticationModes.DeviceCode) != 0) deviceItem = menu.Add("Device code");
- TerminalMenuItem browserItem = null;
- TerminalMenuItem deviceItem = null;
+ // Default to the 'first' choice in the menu
+ TerminalMenuItem choice = menu.Show(0);
- if ((modes & OAuthAuthenticationModes.Browser) != 0) browserItem = menu.Add("Web browser");
- if ((modes & OAuthAuthenticationModes.DeviceCode) != 0) deviceItem = menu.Add("Device code");
+ if (choice == browserItem) goto case OAuthAuthenticationModes.Browser;
+ if (choice == deviceItem) goto case OAuthAuthenticationModes.DeviceCode;
- // Default to the 'first' choice in the menu
- TerminalMenuItem choice = menu.Show(0);
+ throw new Exception();
+ }
+ }
- if (choice == browserItem) goto case OAuthAuthenticationModes.Browser;
- if (choice == deviceItem) goto case OAuthAuthenticationModes.DeviceCode;
+ private async Task GetAuthenticationModeViaHelperAsync(
+ string resource, OAuthAuthenticationModes modes, string args, string command)
+ {
+ var promptArgs = new StringBuilder(args);
+ promptArgs.Append("oauth");
- throw new Exception();
- }
+ if (!string.IsNullOrWhiteSpace(resource))
+ {
+ promptArgs.AppendFormat(" --resource {0}", QuoteCmdArg(resource));
+ }
+
+ if ((modes & OAuthAuthenticationModes.Browser) != 0)
+ {
+ promptArgs.Append(" --browser");
+ }
+
+ if ((modes & OAuthAuthenticationModes.DeviceCode) != 0)
+ {
+ promptArgs.Append(" --device-code");
+ }
+
+ IDictionary resultDict = await InvokeHelperAsync(command, promptArgs.ToString());
+
+ if (!resultDict.TryGetValue("mode", out string responseMode))
+ {
+ throw new Trace2Exception(Context.Trace2, "Missing 'mode' in response");
+ }
+
+ switch (responseMode.ToLowerInvariant())
+ {
+ case "browser":
+ return OAuthAuthenticationModes.Browser;
+
+ case "devicecode":
+ return OAuthAuthenticationModes.DeviceCode;
+
+ default:
+ throw new Trace2Exception(Context.Trace2,
+ $"Unknown mode value in response '{responseMode}'");
}
}
@@ -155,19 +199,15 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie
OAuth2DeviceCodeResult dcr = await client.GetDeviceCodeAsync(scopes, CancellationToken.None);
// If we have a desktop session show the device code in a dialog
- if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
- TryFindHelperCommand(out string command, out string args))
+ if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession)
{
- var promptArgs = new StringBuilder(args);
- promptArgs.Append("device");
- promptArgs.AppendFormat(" --code {0} ", QuoteCmdArg(dcr.UserCode));
- promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(dcr.VerificationUri.ToString()));
-
var promptCts = new CancellationTokenSource();
var tokenCts = new CancellationTokenSource();
// Show the dialog with the device code but don't await its closure
- Task promptTask = InvokeHelperAsync(command, promptArgs.ToString(), null, promptCts.Token);
+ Task promptTask = TryFindHelperCommand(out string command, out string args)
+ ? ShowDeviceCodeViaHelperAsync(dcr, command, args, promptCts.Token)
+ : ShowDeviceCodeViaUiAsync(dcr, promptCts.Token);
// Start the request for an OAuth token but don't wait
Task tokenTask = client.GetTokenByDeviceCodeAsync(dcr, tokenCts.Token);
@@ -195,17 +235,43 @@ public async Task GetTokenByDeviceCodeAsync(OAuth2Client clie
return tokenResult;
}
- else
+
+ return await GetTokenByDeviceCodeViaTtyAsync(client, dcr);
+ }
+
+ private Task ShowDeviceCodeViaUiAsync(OAuth2DeviceCodeResult dcr, CancellationToken ct)
+ {
+ var viewModel = new DeviceCodeViewModel(Context.Environment)
{
- ThrowIfTerminalPromptsDisabled();
+ UserCode = dcr.UserCode,
+ VerificationUrl = dcr.VerificationUri.ToString(),
+ };
- string deviceMessage = $"To complete authentication please visit {dcr.VerificationUri} and enter the following code:" +
- Environment.NewLine +
- dcr.UserCode;
- Context.Terminal.WriteLine(deviceMessage);
+ return AvaloniaUi.ShowViewAsync(viewModel, GetParentWindowHandle(), CancellationToken.None);
+ }
- return await client.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None);
- }
+ private Task ShowDeviceCodeViaHelperAsync(
+ OAuth2DeviceCodeResult dcr, string command, string args, CancellationToken ct)
+ {
+ var promptArgs = new StringBuilder(args);
+ promptArgs.Append("device");
+ promptArgs.AppendFormat(" --code {0} ", QuoteCmdArg(dcr.UserCode));
+ promptArgs.AppendFormat(" --url {0}", QuoteCmdArg(dcr.VerificationUri.ToString()));
+
+ return InvokeHelperAsync(command, promptArgs.ToString(), null, ct);
+ }
+
+ private async Task GetTokenByDeviceCodeViaTtyAsync(OAuth2Client client, OAuth2DeviceCodeResult dcr)
+ {
+ ThrowIfTerminalPromptsDisabled();
+
+ string deviceMessage =
+ $"To complete authentication please visit {dcr.VerificationUri} and enter the following code:" +
+ Environment.NewLine +
+ dcr.UserCode;
+ Context.Terminal.WriteLine(deviceMessage);
+
+ return await client.GetTokenByDeviceCodeAsync(dcr, CancellationToken.None);
}
private bool TryFindHelperCommand(out string command, out string args)
diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs
index 249066569..5447981cd 100644
--- a/src/shared/Core/Constants.cs
+++ b/src/shared/Core/Constants.cs
@@ -5,6 +5,7 @@ namespace GitCredentialManager
{
public static class Constants
{
+ public const string DefaultWindowTitle = "Git Credential Manager";
public const string PersonalAccessTokenUserName = "PersonalAccessToken";
public const string DefaultCredentialNamespace = "git";
public const int DefaultAutoDetectProviderTimeoutMs = 2000; // 2 seconds
@@ -102,6 +103,7 @@ public static class EnvironmentVariables
public const string OAuthDeviceEndpoint = "GCM_OAUTH_DEVICE_ENDPOINT";
public const string OAuthClientAuthHeader = "GCM_OAUTH_USE_CLIENT_AUTH_HEADER";
public const string OAuthDefaultUserName = "GCM_OAUTH_DEFAULT_USERNAME";
+ public const string GcmDevUseLegacyUiHelpers = "GCM_DEV_USELEGACYUIHELPERS";
}
public static class Http
@@ -138,6 +140,7 @@ public static class Credential
public const string AutoDetectTimeout = "autoDetectTimeout";
public const string GuiPromptsEnabled = "guiPrompt";
public const string UiHelper = "uiHelper";
+ public const string DevUseLegacyUiHelpers = "devUseLegacyUiHelpers";
public const string OAuthAuthenticationModes = "oauthAuthModes";
public const string OAuthClientId = "oauthClientId";
diff --git a/src/shared/Core/Core.csproj b/src/shared/Core/Core.csproj
index b83de117e..7107cf921 100644
--- a/src/shared/Core/Core.csproj
+++ b/src/shared/Core/Core.csproj
@@ -14,6 +14,11 @@
+
+
+
+
+
@@ -21,6 +26,13 @@
+
+
+
+
+
+
+
diff --git a/src/shared/Core/UI/Assets/Base.axaml b/src/shared/Core/UI/Assets/Base.axaml
new file mode 100644
index 000000000..da3bc9643
--- /dev/null
+++ b/src/shared/Core/UI/Assets/Base.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+ #F6F6F6
+
+
+
+
+
+
+ #282828
+
+
+
+
+
+
+
diff --git a/src/shared/Core.UI.Avalonia/Assets/ButtonHyperlink.axaml b/src/shared/Core/UI/Assets/ButtonHyperlink.axaml
similarity index 100%
rename from src/shared/Core.UI.Avalonia/Assets/ButtonHyperlink.axaml
rename to src/shared/Core/UI/Assets/ButtonHyperlink.axaml
diff --git a/src/shared/Core.UI.Avalonia/Assets/Controls.axaml b/src/shared/Core/UI/Assets/Controls.axaml
similarity index 75%
rename from src/shared/Core.UI.Avalonia/Assets/Controls.axaml
rename to src/shared/Core/UI/Assets/Controls.axaml
index 9f10e4747..f3c941bd9 100644
--- a/src/shared/Core.UI.Avalonia/Assets/Controls.axaml
+++ b/src/shared/Core/UI/Assets/Controls.axaml
@@ -1,6 +1,9 @@
-
+
+
diff --git a/src/shared/Core.UI.Avalonia/Assets/Images.axaml b/src/shared/Core/UI/Assets/Images.axaml
similarity index 100%
rename from src/shared/Core.UI.Avalonia/Assets/Images.axaml
rename to src/shared/Core/UI/Assets/Images.axaml
diff --git a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml b/src/shared/Core/UI/AvaloniaApp.axaml
similarity index 70%
rename from src/shared/Core.UI.Avalonia/AvaloniaApp.axaml
rename to src/shared/Core/UI/AvaloniaApp.axaml
index 33e285d77..19730f7eb 100644
--- a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml
+++ b/src/shared/Core/UI/AvaloniaApp.axaml
@@ -3,14 +3,14 @@
x:Class="GitCredentialManager.UI.AvaloniaApp"
Name="Git Credential Manager">
-
-
+
+
-
-
+
+
diff --git a/src/shared/Core.UI.Avalonia/AvaloniaApp.axaml.cs b/src/shared/Core/UI/AvaloniaApp.axaml.cs
similarity index 100%
rename from src/shared/Core.UI.Avalonia/AvaloniaApp.axaml.cs
rename to src/shared/Core/UI/AvaloniaApp.axaml.cs
diff --git a/src/shared/Core.UI.Avalonia/AvaloniaUi.cs b/src/shared/Core/UI/AvaloniaUi.cs
similarity index 80%
rename from src/shared/Core.UI.Avalonia/AvaloniaUi.cs
rename to src/shared/Core/UI/AvaloniaUi.cs
index 0b0bea8fb..f71d7dd18 100644
--- a/src/shared/Core.UI.Avalonia/AvaloniaUi.cs
+++ b/src/shared/Core/UI/AvaloniaUi.cs
@@ -3,6 +3,8 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
using GitCredentialManager.Interop.Windows.Native;
using GitCredentialManager.UI.Controls;
using GitCredentialManager.UI.ViewModels;
@@ -38,31 +40,45 @@ public static Task ShowWindowAsync(Func windowFunc, object dataContext,
{
_isAppStarted = true;
- var appRunning = new ManualResetEventSlim();
+ var appInitialized = new ManualResetEventSlim();
- // Fire and forget (this action never returns) the Avalonia app main loop
- // over to our dispatcher (running on the main/entry thread).
- Dispatcher.MainThread.Post(() =>
+ // Fire and forget the Avalonia app main loop over to our dispatcher (running on the main/entry thread).
+ // This action only returns on our dispatcher shutdown.
+ Dispatcher.MainThread.Post(appCancelToken =>
{
- AppBuilder appBuilder = AppBuilder.Configure()
+ AppBuilder.Configure()
+#if NETFRAMEWORK
+ .UseWin32()
+ .UseSkia()
+#else
.UsePlatformDetect()
+#endif
.LogToTrace()
- .SetupWithoutStarting();
-
- appRunning.Set();
-
- // Run the application loop (never exits!)
- var appCts = new CancellationTokenSource();
- appBuilder.Instance.Run(appCts.Token);
+ // Workaround https://github.com/AvaloniaUI/Avalonia/issues/10296
+ // by always setting a application lifetime.
+ .SetupWithLifetime(
+ new ClassicDesktopStyleApplicationLifetime
+ {
+ ShutdownMode = ShutdownMode.OnExplicitShutdown
+ }
+ );
+
+ appInitialized.Set();
+
+ // Run the application loop (only exit when the dispatcher is shutting down)
+ AvnDispatcher.UIThread.MainLoop(appCancelToken);
});
// Wait for the action posted above to be dequeued from the dispatcher's job queue
// and for the Avalonia framework (and their dispatcher) to be initialized.
- appRunning.Wait();
+ appInitialized.Wait();
}
// Post the window action to the Avalonia dispatcher (which should be running)
- return AvnDispatcher.UIThread.InvokeAsync(() => ShowWindowInternal(windowFunc, dataContext, parentHandle, ct));
+ return AvnDispatcher.UIThread.InvokeAsync(
+ () => ShowWindowInternal(windowFunc, dataContext, parentHandle, ct),
+ DispatcherPriority.Send
+ );
}
private static Task ShowWindowInternal(Func windowFunc, object dataContext, IntPtr parentHandle, CancellationToken ct)
diff --git a/src/shared/Core.UI/Commands/CredentialsCommand.cs b/src/shared/Core/UI/Commands/CredentialsCommand.cs
similarity index 94%
rename from src/shared/Core.UI/Commands/CredentialsCommand.cs
rename to src/shared/Core/UI/Commands/CredentialsCommand.cs
index 001693359..bb5ae3eab 100644
--- a/src/shared/Core.UI/Commands/CredentialsCommand.cs
+++ b/src/shared/Core/UI/Commands/CredentialsCommand.cs
@@ -44,9 +44,10 @@ private async Task ExecuteAsync(CommandOptions options)
{
var viewModel = new CredentialsViewModel();
- viewModel.Title = !string.IsNullOrWhiteSpace(options.Title)
- ? options.Title
- : "Git Credential Manager";
+ if (!string.IsNullOrWhiteSpace(options.Title))
+ {
+ viewModel.Title = options.Title;
+ }
viewModel.Description = !string.IsNullOrWhiteSpace(options.Resource)
? $"Enter your credentials for '{options.Resource}'"
diff --git a/src/shared/Core.UI/Commands/DeviceCodeCommand.cs b/src/shared/Core/UI/Commands/DeviceCodeCommand.cs
similarity index 100%
rename from src/shared/Core.UI/Commands/DeviceCodeCommand.cs
rename to src/shared/Core/UI/Commands/DeviceCodeCommand.cs
diff --git a/src/shared/Core.UI/Commands/OAuthCommand.cs b/src/shared/Core/UI/Commands/OAuthCommand.cs
similarity index 95%
rename from src/shared/Core.UI/Commands/OAuthCommand.cs
rename to src/shared/Core/UI/Commands/OAuthCommand.cs
index 619b1d921..785c6d10e 100644
--- a/src/shared/Core.UI/Commands/OAuthCommand.cs
+++ b/src/shared/Core/UI/Commands/OAuthCommand.cs
@@ -50,9 +50,10 @@ private async Task ExecuteAsync(CommandOptions options)
{
var viewModel = new OAuthViewModel();
- viewModel.Title = !string.IsNullOrWhiteSpace(options.Title)
- ? options.Title
- : "Git Credential Manager";
+ if (!string.IsNullOrWhiteSpace(options.Title))
+ {
+ viewModel.Title = options.Title;
+ }
viewModel.Description = !string.IsNullOrWhiteSpace(options.Resource)
? $"Sign in to '{options.Resource}'"
diff --git a/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml b/src/shared/Core/UI/Controls/AboutWindow.axaml
similarity index 59%
rename from src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml
rename to src/shared/Core/UI/Controls/AboutWindow.axaml
index e05c0251f..bccc1217d 100644
--- a/src/shared/Core.UI.Avalonia/Controls/AboutWindow.axaml
+++ b/src/shared/Core/UI/Controls/AboutWindow.axaml
@@ -2,30 +2,33 @@
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" d:DesignWidth="800" d:DesignHeight="450"
+ xmlns:gcm="clr-namespace:GitCredentialManager"
+ mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="450"
x:Class="GitCredentialManager.UI.Controls.AboutWindow"
Title="About Git Credential Manager"
CanResize="False" Width="300" SizeToContent="Height"
- Background="#F6F6F6">
-
-
-
+ x:Name="window"
+ Background="{DynamicResource WindowBackgroundBrush}">
+ Source="{DynamicResource GcmLogo}" Width="64" Height="64" />
+
$"Version {Constants.GcmVersion}";
+ public string ProjectUrl => Constants.HelpUrls.GcmProjectUrl;
+
public AboutWindow()
{
InitializeComponent();
@@ -23,7 +26,7 @@ private void InitializeComponent()
private void ProjectButton_Click(object sender, RoutedEventArgs e)
{
- var psi = new ProcessStartInfo("https://aka.ms/gcm")
+ var psi = new ProcessStartInfo(ProjectUrl)
{
UseShellExecute = true
};
diff --git a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml b/src/shared/Core/UI/Controls/DialogWindow.axaml
similarity index 86%
rename from src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml
rename to src/shared/Core/UI/Controls/DialogWindow.axaml
index f995a8790..ce8598fbd 100644
--- a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml
+++ b/src/shared/Core/UI/Controls/DialogWindow.axaml
@@ -2,7 +2,7 @@
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"
- xmlns:vm="clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcoreui"
+ xmlns:vm="clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore"
xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters"
mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="520"
x:Class="GitCredentialManager.UI.Controls.DialogWindow"
@@ -15,17 +15,11 @@
ShowInTaskbar="True" ShowActivated="True"
PointerPressed="Window_PointerPressed"
KeyUp="Window_KeyUp">
-
-
+
@@ -39,7 +33,7 @@
-
+
diff --git a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml.cs b/src/shared/Core/UI/Controls/DialogWindow.axaml.cs
similarity index 95%
rename from src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml.cs
rename to src/shared/Core/UI/Controls/DialogWindow.axaml.cs
index 1bed4d454..79501edb8 100644
--- a/src/shared/Core.UI.Avalonia/Controls/DialogWindow.axaml.cs
+++ b/src/shared/Core/UI/Controls/DialogWindow.axaml.cs
@@ -9,7 +9,7 @@
namespace GitCredentialManager.UI.Controls
{
- public class DialogWindow : Window
+ public partial class DialogWindow : Window
{
private readonly Control _view;
private ContentControl _contentHolder;
@@ -46,7 +46,7 @@ protected override void OnDataContextChanged(EventArgs e)
// Send a focus request to the child view on idle
if (_view is IFocusable focusable)
{
- Avalonia.Threading.Dispatcher.UIThread.Post(() => focusable.SetFocus(), DispatcherPriority.ApplicationIdle);
+ Avalonia.Threading.Dispatcher.UIThread.Post(() => focusable.SetFocus(), DispatcherPriority.Normal);
}
}
}
diff --git a/src/shared/Core.UI/Controls/IFocusable.cs b/src/shared/Core/UI/Controls/IFocusable.cs
similarity index 100%
rename from src/shared/Core.UI/Controls/IFocusable.cs
rename to src/shared/Core/UI/Controls/IFocusable.cs
diff --git a/src/shared/Core/UI/Converters/BoolConvertersEx.cs b/src/shared/Core/UI/Converters/BoolConvertersEx.cs
new file mode 100644
index 000000000..836df8b8e
--- /dev/null
+++ b/src/shared/Core/UI/Converters/BoolConvertersEx.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Avalonia;
+using Avalonia.Data.Converters;
+
+namespace GitCredentialManager.UI.Converters
+{
+ public static class BoolConvertersEx
+ {
+ public static readonly IValueConverter ToThickness = new BoolToThicknessConverter();
+
+ public static readonly IMultiValueConverter Or =
+ new FuncMultiValueConverter(x => x.Aggregate(false, (a, b) => a || b));
+
+ public static readonly IMultiValueConverter And =
+ new FuncMultiValueConverter(x => x.Aggregate(true, (a, b) => a && b));
+
+ private class BoolToThicknessConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not bool b)
+ {
+ return null;
+ }
+
+ if (parameter is int i)
+ {
+ return b ? new Thickness(i) : new Thickness(0);
+ }
+
+ return b ? new Thickness(1) : new Thickness(0);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/shared/Core.UI.Avalonia/Converters/WindowClientAreaConverters.cs b/src/shared/Core/UI/Converters/WindowClientAreaConverters.cs
similarity index 100%
rename from src/shared/Core.UI.Avalonia/Converters/WindowClientAreaConverters.cs
rename to src/shared/Core/UI/Converters/WindowClientAreaConverters.cs
diff --git a/src/shared/Core.UI.Avalonia/Dispatcher.cs b/src/shared/Core/UI/Dispatcher.cs
similarity index 87%
rename from src/shared/Core.UI.Avalonia/Dispatcher.cs
rename to src/shared/Core/UI/Dispatcher.cs
index dcbf1b224..9dd991438 100644
--- a/src/shared/Core.UI.Avalonia/Dispatcher.cs
+++ b/src/shared/Core/UI/Dispatcher.cs
@@ -59,7 +59,7 @@ public void VerifyAccess()
/// Post work to be run on the thread associated with this dispatcher.
///
/// Work to be run.
- public void Post(Action work)
+ public void Post(Action work)
{
Task _ = InvokeAsync(work);
}
@@ -69,14 +69,14 @@ public void Post(Action work)
/// synchronously until the work is complete.
///
/// Work to be run.
- public Task InvokeAsync(Action work)
+ public Task InvokeAsync(Action work)
{
var tcs = new TaskCompletionSource