diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index b661e0b78a..9d153e0336 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -76,7 +76,7 @@ jobs:
# ================================
linux:
name: Linux
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.runtime == 'linux-x64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
strategy:
matrix:
runtime: [ linux-x64, linux-arm64, linux-arm ]
@@ -89,6 +89,13 @@ jobs:
with:
dotnet-version: 10.0.x
+ - name: Install cross-compilation toolchain (arm)
+ if: matrix.runtime == 'linux-arm'
+ run: |
+ sudo apt-get update
+ # Brings in cross-compilation toolchain for armhf including binutils
+ sudo apt-get install -y gcc-arm-linux-gnueabihf
+
- name: Install dependencies
run: dotnet restore
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d19eef0708..2374d94c61 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,10 +6,9 @@
-
-
-
-
+
+
+
@@ -23,7 +22,6 @@
-
diff --git a/src/linux/Packaging.Linux/install-from-source.sh b/src/linux/Packaging.Linux/install-from-source.sh
index 1337b75273..64f925811f 100755
--- a/src/linux/Packaging.Linux/install-from-source.sh
+++ b/src/linux/Packaging.Linux/install-from-source.sh
@@ -259,5 +259,9 @@ if [ -z "$DOTNET_ROOT" ]; then
fi
cd "$toplevel_path"
-$sudo_cmd env "PATH=$PATH" $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:installPrefix=$installPrefix
+# Build a non-AOT binary so installing from source needs only the .NET SDK,
+# not a C toolchain (clang + zlib) to link a native build. PublishAot is read
+# from the environment by Git-Credential-Manager.csproj and inherited by the
+# nested publish.
+$sudo_cmd env "PATH=$PATH" PublishAot=false $DOTNET_ROOT/dotnet build ./src/linux/Packaging.Linux/Packaging.Linux.csproj -c Release -p:InstallFromSource=true -p:installPrefix=$installPrefix
add_to_PATH "$installPrefix/bin"
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs
index 1ca23d0f57..84e76518f3 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketOAuth2Client.cs
@@ -42,7 +42,9 @@ protected override bool TryCreateTokenEndpointResult(string json, out OAuth2Toke
// We override the token endpoint response parsing because the Bitbucket authority returns
// the non-standard 'scopes' property for the list of scopes, rather than the (optional)
// 'scope' (note the singular vs plural) property as outlined in the standard.
- if (TryDeserializeJson(json, out BitbucketTokenEndpointResponseJson jsonObj))
+ if (TryDeserializeJson(json,
+ BitbucketOAuthJsonContext.Default.BitbucketTokenEndpointResponseJson,
+ out BitbucketTokenEndpointResponseJson jsonObj))
{
result = jsonObj.ToResult();
return true;
diff --git a/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs b/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs
index c28e63de11..afc8c43a5f 100644
--- a/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs
+++ b/src/shared/Atlassian.Bitbucket/BitbucketTokenEndpointResponseJson.cs
@@ -5,6 +5,9 @@
namespace Atlassian.Bitbucket
{
+ [JsonSerializable(typeof(BitbucketTokenEndpointResponseJson))]
+ public partial class BitbucketOAuthJsonContext : JsonSerializerContext;
+
[JsonConverter(typeof(BitbucketCustomTokenEndpointResponseJsonConverter))]
public class BitbucketTokenEndpointResponseJson : TokenEndpointResponseJson
{
diff --git a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs
index 94021e14da..c90932f381 100644
--- a/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs
+++ b/src/shared/Atlassian.Bitbucket/Cloud/BitbucketRestApi.cs
@@ -8,6 +8,13 @@
namespace Atlassian.Bitbucket.Cloud
{
+ [JsonSerializable(typeof(UserInfo))]
+ [JsonSourceGenerationOptions(
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ )]
+ public partial class BitbucketCloudRestApiJsonContext : JsonSerializerContext;
+
public class BitbucketRestApi : IBitbucketRestApi
{
private readonly ICommandContext _context;
@@ -43,11 +50,7 @@ public async Task> GetUserInformationAsync(string userN
if (response.IsSuccessStatusCode)
{
- var obj = JsonSerializer.Deserialize(json,
- new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true,
- });
+ UserInfo obj = JsonSerializer.Deserialize(json, BitbucketCloudRestApiJsonContext.Default.UserInfo);
return new RestApiResult(response.StatusCode, obj);
}
diff --git a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs
index 159229885d..65c2520d16 100644
--- a/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs
+++ b/src/shared/Atlassian.Bitbucket/DataCenter/BitbucketRestApi.cs
@@ -10,6 +10,15 @@
namespace Atlassian.Bitbucket.DataCenter
{
+ [JsonSerializable(typeof(UserInfo))]
+ [JsonSerializable(typeof(LoginOption))]
+ [JsonSerializable(typeof(LoginOptions))]
+ [JsonSourceGenerationOptions(
+ PropertyNameCaseInsensitive = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ )]
+ public partial class BitbucketDataCenterRestApiJsonContext : JsonSerializerContext;
+
public class BitbucketRestApi : IBitbucketRestApi
{
private readonly ICommandContext _context;
@@ -107,12 +116,8 @@ public async Task> GetAuthenticationMethodsAsync()
if (response.IsSuccessStatusCode)
{
- var loginOptions = JsonSerializer.Deserialize(json,
- new JsonSerializerOptions
- {
- PropertyNameCaseInsensitive = true,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
- });
+ LoginOptions loginOptions = JsonSerializer.Deserialize(
+ json, BitbucketDataCenterRestApiJsonContext.Default.LoginOptions);
if (loginOptions.Results.Any(r => "LOGIN_FORM".Equals(r.Type)))
{
@@ -151,4 +156,4 @@ private Uri ApiUri
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml b/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
index 717500b7be..895587b610 100644
--- a/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
+++ b/src/shared/Atlassian.Bitbucket/UI/Views/CredentialsView.axaml
@@ -5,6 +5,7 @@
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:DataType="vm:CredentialsViewModel"
x:Class="Atlassian.Bitbucket.UI.Views.CredentialsView">
@@ -63,11 +64,11 @@
-
-
-
-
-
diff --git a/src/shared/Core/Trace2.cs b/src/shared/Core/Trace2.cs
index de6ca58224..ebce213da2 100644
--- a/src/shared/Core/Trace2.cs
+++ b/src/shared/Core/Trace2.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.IO.Pipes;
using System.Text;
+using System.Text.Json.Serialization;
using System.Threading;
namespace GitCredentialManager;
@@ -13,13 +14,21 @@ namespace GitCredentialManager;
///
public enum Trace2Event
{
+ [JsonStringEnumMemberName("version")]
Version = 0,
+ [JsonStringEnumMemberName("start")]
Start = 1,
+ [JsonStringEnumMemberName("exit")]
Exit = 2,
+ [JsonStringEnumMemberName("child_start")]
ChildStart = 3,
+ [JsonStringEnumMemberName("child_exit")]
ChildExit = 4,
+ [JsonStringEnumMemberName("error")]
Error = 5,
+ [JsonStringEnumMemberName("region_enter")]
RegionEnter = 6,
+ [JsonStringEnumMemberName("region_leave")]
RegionLeave = 7,
}
@@ -28,9 +37,13 @@ public enum Trace2Event
///
public enum Trace2ProcessClass
{
+ [JsonStringEnumMemberName("none")]
None = 0,
+ [JsonStringEnumMemberName("ui_helper")]
UIHelper = 1,
+ [JsonStringEnumMemberName("git")]
Git = 2,
+ [JsonStringEnumMemberName("other")]
Other = 3
}
diff --git a/src/shared/Core/Trace2Message.cs b/src/shared/Core/Trace2Message.cs
index 78eb05a203..175fd8bf02 100644
--- a/src/shared/Core/Trace2Message.cs
+++ b/src/shared/Core/Trace2Message.cs
@@ -6,17 +6,27 @@
namespace GitCredentialManager;
+[JsonSerializable(typeof(VersionMessage))]
+[JsonSerializable(typeof(StartMessage))]
+[JsonSerializable(typeof(ExitMessage))]
+[JsonSerializable(typeof(ChildStartMessage))]
+[JsonSerializable(typeof(ChildExitMessage))]
+[JsonSerializable(typeof(ErrorMessage))]
+[JsonSerializable(typeof(RegionEnterMessage))]
+[JsonSerializable(typeof(RegionLeaveMessage))]
+[JsonSourceGenerationOptions(
+ UseStringEnumConverter = true,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
+ PropertyNameCaseInsensitive = true
+)]
+public partial class Trace2JsonContext : JsonSerializerContext;
+
public abstract class Trace2Message
{
private const int SourceColumnMaxWidth = 23;
private const string NormalPerfTimeFormat = "HH:mm:ss.ffffff";
protected const string EmptyPerformanceSpan = "| | | | ";
- protected static readonly JsonSerializerOptions JsonSerializerOptions = new()
- {
- PropertyNameCaseInsensitive = true,
- Converters = { new JsonStringEnumConverter(new SnakeCaseNamingPolicy()) }
- };
[JsonPropertyName("event")]
[JsonPropertyOrder(1)]
@@ -194,7 +204,7 @@ public class VersionMessage : Trace2Message
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.VersionMessage);
}
public override string ToNormalString()
@@ -230,7 +240,7 @@ public class StartMessage : Trace2Message
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.StartMessage);
}
public override string ToNormalString()
@@ -266,7 +276,7 @@ public class ExitMessage : Trace2Message
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.ExitMessage);
}
public override string ToNormalString()
@@ -314,7 +324,7 @@ public class ChildStartMessage : Trace2Message
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.ChildStartMessage);
}
public override string ToNormalString()
@@ -371,7 +381,7 @@ public class ChildExitMessage : Trace2Message
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.ChildExitMessage);
}
public override string ToNormalString()
@@ -415,7 +425,7 @@ public class ErrorMessage : Trace2Message
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.ErrorMessage);
}
public override string ToNormalString()
@@ -473,7 +483,7 @@ public class RegionEnterMessage : RegionMessage
{
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.RegionEnterMessage);
}
public override string ToNormalString()
@@ -504,7 +514,7 @@ public class RegionLeaveMessage : RegionMessage
public override string ToJson()
{
- return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ return JsonSerializer.Serialize(this, Trace2JsonContext.Default.RegionLeaveMessage);
}
public override string ToNormalString()
@@ -527,9 +537,3 @@ protected override string GetEventMessage(Trace2FormatTarget formatTarget)
return Message;
}
}
-
-public class SnakeCaseNamingPolicy : JsonNamingPolicy
-{
- public override string ConvertName(string name) =>
- name.ToSnakeCase();
-}
diff --git a/src/shared/Core/UI/Controls/DialogWindow.axaml b/src/shared/Core/UI/Controls/DialogWindow.axaml
index 42d4636ebb..6e65489d2c 100644
--- a/src/shared/Core/UI/Controls/DialogWindow.axaml
+++ b/src/shared/Core/UI/Controls/DialogWindow.axaml
@@ -7,7 +7,8 @@
mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="520"
x:Class="GitCredentialManager.UI.Controls.DialogWindow"
ExtendClientAreaToDecorationsHint="{Binding ExtendClientArea}"
- ExtendClientAreaChromeHints="{Binding ShowCustomChrome, Converter={x:Static converters:WindowClientAreaConverters.BoolToChromeHints}}"
+ WindowDecorations="{Binding WindowDecorations}"
+ x:DataType="vm:WindowViewModel"
Title="{Binding Title}"
SizeToContent="Height" CanResize="False"
Width="420" MaxHeight="520" MinHeight="280"
@@ -18,54 +19,58 @@
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
diff --git a/src/shared/Core/UI/Controls/ProgressWindow.axaml b/src/shared/Core/UI/Controls/ProgressWindow.axaml
index 3bfc20f5ca..e8a7346774 100644
--- a/src/shared/Core/UI/Controls/ProgressWindow.axaml
+++ b/src/shared/Core/UI/Controls/ProgressWindow.axaml
@@ -4,7 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="182" d:DesignHeight="46"
SizeToContent="WidthAndHeight" CanResize="False" Topmost="True"
- ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaToDecorationsHint="True"
+ WindowDecorations="None"
ShowInTaskbar="False" Title="Git Credential Manager" WindowStartupLocation="CenterScreen"
x:Class="GitCredentialManager.UI.Controls.ProgressWindow">
(
- x => x
- ? ExtendClientAreaChromeHints.NoChrome
- : ExtendClientAreaChromeHints.PreferSystemChrome);
- }
-}
diff --git a/src/shared/Core/UI/ViewModels/WindowViewModel.cs b/src/shared/Core/UI/ViewModels/WindowViewModel.cs
index 078d7c3b83..f95c845a1b 100644
--- a/src/shared/Core/UI/ViewModels/WindowViewModel.cs
+++ b/src/shared/Core/UI/ViewModels/WindowViewModel.cs
@@ -1,14 +1,17 @@
using System;
+using Avalonia.Controls;
namespace GitCredentialManager.UI.ViewModels
{
public class WindowViewModel : ViewModel
{
private bool _extendClientArea;
- private bool _showCustomChromeOverride;
+ private WindowDecorations _windowDecorations;
private bool _showDebugControls;
private string _title;
+ public static readonly WindowDecorations[] WindowDecorationsValues = Enum.GetValues();
+
public event EventHandler Accepted;
public event EventHandler Canceled;
@@ -18,6 +21,13 @@ public WindowViewModel()
// Extend the client area on Windows and macOS only
ExtendClientArea = PlatformUtils.IsMacOS() || PlatformUtils.IsWindows();
+
+ // On Windows we prefer to show our own title bar, but we want the system
+ // to continue to draw the window border for us, which includes rounded
+ // window corners and shadow.
+ WindowDecorations = PlatformUtils.IsWindows()
+ ? WindowDecorations.BorderOnly
+ : WindowDecorations.Full;
}
public bool WindowResult { get; private set; }
@@ -28,39 +38,24 @@ public bool ShowDebugControls
set => SetAndRaisePropertyChanged(ref _showDebugControls, value);
}
- public bool ShowCustomChrome
- {
- // On macOS we typically do NOT want to show the custom chrome if we've extended the client area
- // because the native 'traffic light' controls will still be visible and we don't want to show our own.
- get => ShowCustomChromeOverride || (ExtendClientArea && !PlatformUtils.IsMacOS());
- }
-
- public bool ShowCustomWindowBorder
- {
- // Draw the window border explicitly on Windows
- get => ShowCustomChrome && PlatformUtils.IsWindows();
- }
-
- public bool ShowCustomChromeOverride
+ public WindowDecorations WindowDecorations
{
- get => _showCustomChromeOverride;
+ get => _windowDecorations;
set
{
- SetAndRaisePropertyChanged(ref _showCustomChromeOverride, value);
- RaisePropertyChanged(nameof(ShowCustomChrome));
- RaisePropertyChanged(nameof(ShowCustomWindowBorder));
+ SetAndRaisePropertyChanged(ref _windowDecorations, value);
+ RaisePropertyChanged(nameof(ShowCustomTitleBar));
}
}
+ // Without system window decorations there's now way to close the window,
+ // so we need to draw our own title bar and close button.
+ public bool ShowCustomTitleBar => WindowDecorations != WindowDecorations.Full;
+
public bool ExtendClientArea
{
get => _extendClientArea;
- set
- {
- SetAndRaisePropertyChanged(ref _extendClientArea, value);
- RaisePropertyChanged(nameof(ShowCustomChrome));
- RaisePropertyChanged(nameof(ShowCustomWindowBorder));
- }
+ set => SetAndRaisePropertyChanged(ref _extendClientArea, value);
}
public string Title
diff --git a/src/shared/Core/UI/Views/CredentialsView.axaml b/src/shared/Core/UI/Views/CredentialsView.axaml
index 9e85c48720..0fb41261b3 100644
--- a/src/shared/Core/UI/Views/CredentialsView.axaml
+++ b/src/shared/Core/UI/Views/CredentialsView.axaml
@@ -5,6 +5,7 @@
xmlns:vm="clr-namespace:GitCredentialManager.UI.ViewModels;assembly=gcmcore"
xmlns:converters="clr-namespace:GitCredentialManager.UI.Converters;assembly=gcmcore"
mc:Ignorable="d" d:DesignWidth="420"
+ x:DataType="vm:CredentialsViewModel"
x:Class="GitCredentialManager.UI.Views.CredentialsView">
@@ -34,10 +35,10 @@