feature: support git.core.askpass (#239)

This commit is contained in:
leo 2024-07-08 22:07:00 +08:00
parent 8fa19ecd0c
commit cbe4c36525
No known key found for this signature in database
14 changed files with 211 additions and 60 deletions

View file

@ -5,6 +5,7 @@ using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
@ -324,9 +325,17 @@ namespace SourceGit
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
BindingPlugins.DataValidators.RemoveAt(0);
var commandlines = Environment.GetCommandLineArgs();
if (TryParseAskpass(commandlines, out var keyname))
{
desktop.MainWindow = new Views.Askpass(Path.GetFileName(keyname));
}
else
{
Native.OS.SetupEnternalTools();
_launcher = new ViewModels.Launcher();
_launcher = new ViewModels.Launcher(commandlines);
desktop.MainWindow = new Views.Launcher() { DataContext = _launcher };
var pref = ViewModels.Preference.Instance;
@ -336,6 +345,7 @@ namespace SourceGit
Check4Update();
}
}
}
base.OnFrameworkInitializationCompleted();
}
@ -359,6 +369,23 @@ namespace SourceGit
});
}
private static bool TryParseAskpass(string[] args, out string keyname)
{
keyname = string.Empty;
if (args.Length != 2)
return false;
var match = REG_ASKPASS().Match(args[1]);
if (match.Success)
keyname = match.Groups[1].Value;
return match.Success;
}
[GeneratedRegex(@"Enter\s+passphrase\s*for\s*key\s*['""]([^'""]+)['""]\:\s*", RegexOptions.IgnoreCase)]
private static partial Regex REG_ASKPASS();
private ViewModels.Launcher _launcher = null;
private ResourceDictionary _activeLocale = null;
private ResourceDictionary _themeOverrides = null;

View file

@ -52,14 +52,10 @@
cmd.Context = repo;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
cmd.Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
if (string.IsNullOrEmpty(sshKey))
cmd.Args = "-c credential.helper=manager ";
}
else
cmd.UseSSHKey(sshKey);
cmd.Args += $"push {remote} --delete {name}";
return cmd.Exec();

View file

@ -13,13 +13,9 @@ namespace SourceGit.Commands
TraitErrorAsOutput = true;
if (string.IsNullOrEmpty(sshKey))
{
Args = "-c credential.helper=manager ";
}
else
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
UseSSHKey(sshKey);
Args += "clone --progress --verbose --recurse-submodules ";

View file

@ -28,6 +28,15 @@ namespace SourceGit.Commands
public string Args { get; set; } = string.Empty;
public bool RaiseError { get; set; } = true;
public bool TraitErrorAsOutput { get; set; } = false;
public Dictionary<string, string> Envs { get; set; } = new Dictionary<string, string>();
public void UseSSHKey(string key)
{
Envs.Add("DISPLAY", "required");
Envs.Add("SSH_ASKPASS", Process.GetCurrentProcess().MainModule.FileName);
Envs.Add("SSH_ASKPASS_REQUIRE", "prefer");
Envs.Add("GIT_SSH_COMMAND", $"ssh -i '{key}'");
}
public bool Exec()
{
@ -41,6 +50,10 @@ namespace SourceGit.Commands
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
// User environment overrides.
foreach (var kv in Envs)
start.Environment.Add(kv.Key, kv.Value);
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
@ -94,7 +107,7 @@ namespace SourceGit.Commands
return;
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal))
return;
if (_progressRegex().IsMatch(e.Data))
if (REG_PROGRESS().IsMatch(e.Data))
return;
errs.Add(e.Data);
};
@ -185,6 +198,9 @@ namespace SourceGit.Commands
protected virtual void OnReadline(string line) { }
[GeneratedRegex(@"\d+%")]
private static partial Regex _progressRegex();
private static partial Regex REG_PROGRESS();
[GeneratedRegex(@"Enter\s+passphrase\s*for\s*key\s*['""]([^'""]+)['""]\:\s*", RegexOptions.IgnoreCase)]
private static partial Regex REG_ASKPASS();
}
}

View file

@ -15,14 +15,10 @@ namespace SourceGit.Commands
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
if (string.IsNullOrEmpty(sshKey))
Args = "-c credential.helper=manager ";
}
else
UseSSHKey(sshKey);
Args += "fetch --progress --verbose ";
if (prune)
@ -46,14 +42,10 @@ namespace SourceGit.Commands
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
if (string.IsNullOrEmpty(sshKey))
Args = "-c credential.helper=manager ";
}
else
UseSSHKey(sshKey);
Args += $"fetch --progress --verbose {remote} {remoteBranch}:{localBranch}";
}

View file

@ -12,14 +12,10 @@ namespace SourceGit.Commands
TraitErrorAsOutput = true;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
if (string.IsNullOrEmpty(sshKey))
Args = "-c credential.helper=manager ";
}
else
UseSSHKey(sshKey);
Args += "pull --verbose --progress --tags ";
if (useRebase)

View file

@ -12,14 +12,10 @@ namespace SourceGit.Commands
_outputHandler = onProgress;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
if (string.IsNullOrEmpty(sshKey))
Args = "-c credential.helper=manager ";
}
else
UseSSHKey(sshKey);
Args += "push --progress --verbose ";
@ -39,14 +35,10 @@ namespace SourceGit.Commands
Context = repo;
var sshKey = new Config(repo).Get($"remote.{remote}.sshkey");
if (!string.IsNullOrEmpty(sshKey))
{
Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" ";
}
else
{
if (string.IsNullOrEmpty(sshKey))
Args = "-c credential.helper=manager ";
}
else
UseSSHKey(sshKey);
Args += "push ";
if (isDelete)

View file

@ -50,6 +50,7 @@
<StreamGeometry x:Key="Icons.Info">M512 0C229 0 0 229 0 512s229 512 512 512 512-229 512-512S795 0 512 0zM512 928c-230 0-416-186-416-416S282 96 512 96s416 186 416 416S742 928 512 928zM538 343c47 0 83-38 83-78 0-32-21-61-62-61-55 0-82 45-82 77C475 320 498 343 538 343zM533 729c-8 0-11-10-3-40l43-166c16-61 11-100-22-100-39 0-131 40-211 108l16 27c25-17 68-35 78-35 8 0 7 10 0 36l-38 158c-23 89 1 110 34 110 33 0 118-30 196-110l-19-25C575 717 543 729 533 729z</StreamGeometry>
<StreamGeometry x:Key="Icons.Init">M412 66C326 132 271 233 271 347c0 17 1 34 4 50-41-48-98-79-162-83a444 444 0 00-46 196c0 207 142 382 337 439h2c19 0 34 15 34 33 0 11-6 21-14 26l1 14C183 973 0 763 0 511 0 272 166 70 393 7A35 35 0 01414 0c19 0 34 15 34 33a33 33 0 01-36 33zm200 893c86-66 141-168 141-282 0-17-1-34-4-50 41 48 98 79 162 83a444 444 0 0046-196c0-207-142-382-337-439h-2a33 33 0 01-34-33c0-11 6-21 14-26L596 0C841 51 1024 261 1024 513c0 239-166 441-393 504A35 35 0 01610 1024a33 33 0 01-34-33 33 33 0 0136-33zM512 704a192 192 0 110-384 192 192 0 010 384z</StreamGeometry>
<StreamGeometry x:Key="Icons.InteractiveRebase">M512 64A447 447 0 0064 512c0 248 200 448 448 448s448-200 448-448S760 64 512 64zM218 295h31c54 0 105 19 145 55 13 12 13 31 3 43a35 35 0 01-22 10 36 36 0 01-21-7 155 155 0 00-103-39h-31a32 32 0 01-31-31c0-18 13-31 30-31zm31 433h-31a32 32 0 01-31-31c0-16 13-31 31-31h31A154 154 0 00403 512 217 217 0 01620 295h75l-93-67a33 33 0 01-7-43 33 33 0 0143-7l205 148-205 148a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67H620a154 154 0 00-154 154c0 122-97 220-217 220zm390 118a29 29 0 01-18 6 32 32 0 01-31-31c0-10 4-19 13-25l93-67h-75c-52 0-103-19-143-54-12-12-13-31-1-43a30 30 0 0142-3 151 151 0 00102 39h75L602 599a33 33 0 01-7-43 33 33 0 0143-7l205 148-203 151z</StreamGeometry>
<StreamGeometry x:Key="Icons.Password">M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutHorizontal">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zM139 832V192c0-6 4-11 11-11h331v661H149c-6 0-11-4-11-11zm747 0c0 6-4 11-11 11H544v-661H875c6 0 11 4 11 11v640z</StreamGeometry>
<StreamGeometry x:Key="Icons.LayoutVertical">M875 117H149C109 117 75 151 75 192v640c0 41 34 75 75 75h725c41 0 75-34 75-75V192c0-41-34-75-75-75zm-725 64h725c6 0 11 4 11 11v288h-747V192c0-6 4-11 11-11zm725 661H149c-6 0-11-4-11-11V544h747V832c0 6-4 11-11 11z</StreamGeometry>
<StreamGeometry x:Key="Icons.LFS">M40 9 15 23 15 31 9 28 9 20 34 5 24 0 0 14 0 34 25 48 25 28 49 14zM26 29 26 48 49 34 49 15z</StreamGeometry>

View file

@ -36,6 +36,7 @@
<x:String x:Key="Text.Archive.File.Placeholder" xml:space="preserve">Select archive file path</x:String>
<x:String x:Key="Text.Archive.Revision" xml:space="preserve">Revision:</x:String>
<x:String x:Key="Text.Archive.Title" xml:space="preserve">Archive</x:String>
<x:String x:Key="Text.Askpass" xml:space="preserve">SourceGit Askpass</x:String>
<x:String x:Key="Text.AssumeUnchanged" xml:space="preserve">FILES ASSUME UNCHANGED</x:String>
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">NO FILES ASSUMED AS UNCHANGED</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">REMOVE</x:String>

View file

@ -39,6 +39,7 @@
<x:String x:Key="Text.Archive.File.Placeholder" xml:space="preserve">选择存档文件的存放路径</x:String>
<x:String x:Key="Text.Archive.Revision" xml:space="preserve">指定的提交:</x:String>
<x:String x:Key="Text.Archive.Title" xml:space="preserve">存档</x:String>
<x:String x:Key="Text.Askpass" xml:space="preserve">SourceGit Askpass</x:String>
<x:String x:Key="Text.AssumeUnchanged" xml:space="preserve">不跟踪更改的文件</x:String>
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">没有不跟踪更改的文件</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">移除</x:String>

View file

@ -39,6 +39,7 @@
<x:String x:Key="Text.Archive.File.Placeholder" xml:space="preserve">選擇存檔檔案的存放路徑</x:String>
<x:String x:Key="Text.Archive.Revision" xml:space="preserve">指定的提交:</x:String>
<x:String x:Key="Text.Archive.Title" xml:space="preserve">存檔</x:String>
<x:String x:Key="Text.Askpass" xml:space="preserve">SourceGit Askpass</x:String>
<x:String x:Key="Text.AssumeUnchanged" xml:space="preserve">不跟蹤更改的檔案</x:String>
<x:String x:Key="Text.AssumeUnchanged.Empty" xml:space="preserve">沒有不跟蹤更改的檔案</x:String>
<x:String x:Key="Text.AssumeUnchanged.Remove" xml:space="preserve">移除</x:String>

View file

@ -28,12 +28,11 @@ namespace SourceGit.ViewModels
}
}
public Launcher()
public Launcher(string[] commandlines)
{
Pages = new AvaloniaList<LauncherPage>();
AddNewTab();
var commandlines = Environment.GetCommandLineArgs();
if (commandlines.Length == 2)
{
var path = commandlines[1];

81
src/Views/Askpass.axaml Normal file
View file

@ -0,0 +1,81 @@
<v:ChromelessWindow xmlns="https://github.com/avaloniaui"
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="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.Askpass"
x:DataType="v:Askpass"
Icon="/App.ico"
Title="{DynamicResource Text.Askpass}"
SizeToContent="WidthAndHeight"
CanResize="False"
WindowStartupLocation="CenterScreen">
<Grid RowDefinitions="Auto,*">
<!-- TitleBar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="30">
<Border Grid.Column="0" Grid.ColumnSpan="3"
Background="{DynamicResource Brush.TitleBar}"
BorderThickness="0,0,0,1" BorderBrush="{DynamicResource Brush.Border0}"
PointerPressed="BeginMoveWindow"/>
<Path Grid.Column="0"
Width="14" Height="14"
Margin="10,0,0,0"
Data="{StaticResource Icons.Password}"
IsVisible="{OnPlatform True, macOS=False}"/>
<Grid Grid.Column="0" Classes="caption_button_box" Margin="2,4,0,0" IsVisible="{OnPlatform False, macOS=True}">
<Button Classes="caption_button_macos" Click="CloseWindow">
<Grid>
<Ellipse Fill="{DynamicResource Brush.MacOS.Close}"/>
<Path Height="6" Width="6" Stretch="Fill" Fill="#404040" Stroke="#404040" StrokeThickness="1" Data="{StaticResource Icons.Window.Close}"/>
</Grid>
</Button>
</Grid>
<TextBlock Grid.Column="0" Grid.ColumnSpan="3"
Classes="bold"
Text="{DynamicResource Text.Askpass}"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsHitTestVisible="False"/>
<Button Grid.Column="2"
Classes="caption_button"
Click="CloseWindow"
IsVisible="{OnPlatform True, macOS=False}">
<Path Data="{StaticResource Icons.Window.Close}"/>
</Button>
</Grid>
<StackPanel Grid.Row="1" Margin="0,16" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="16,0">
<TextBlock Text="Enter passphrase for key: "/>
<Border Background="{DynamicResource Brush.Accent}" CornerRadius="4">
<TextBlock Text="{Binding KeyName}" Classes="monospace" Margin="4,0" Foreground="#FFDDDDDD"/>
</Border>
</StackPanel>
<TextBox Margin="16"
MinWidth="300"
Height="32"
Text="{Binding Passphrase, Mode=TwoWay}"
PasswordChar="*"
HorizontalAlignment="Stretch"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Classes="flat primary"
Width="80"
Content="{DynamicResource Text.Sure}"
Click="EnterPassword"
HotKey="Enter"/>
<Button Classes="flat"
Width="80"
Margin="16,0,0,0"
Content="{DynamicResource Text.Cancel}"
Click="CloseWindow"/>
</StackPanel>
</StackPanel>
</Grid>
</v:ChromelessWindow>

View file

@ -0,0 +1,52 @@
using System;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace SourceGit.Views
{
public partial class Askpass : ChromelessWindow
{
public string KeyName
{
get;
private set;
} = string.Empty;
public string Passphrase
{
get;
set;
} = string.Empty;
public Askpass()
{
DataContext = this;
InitializeComponent();
}
public Askpass(string keyname)
{
KeyName = keyname;
DataContext = this;
InitializeComponent();
}
private void BeginMoveWindow(object sender, PointerPressedEventArgs e)
{
BeginMoveDrag(e);
}
private void CloseWindow(object sender, RoutedEventArgs e)
{
Console.Out.WriteLine("No passphrase entered.");
Environment.Exit(-1);
}
private void EnterPassword(object sender, RoutedEventArgs e)
{
Console.Out.Write($"{Passphrase}\n");
Environment.Exit(0);
}
}
}