sourcegit/src/Commands/Command.cs

242 lines
8 KiB
C#
Raw Normal View History

2024-03-20 00:36:10 -07:00
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
2024-03-20 00:36:10 -07:00
using Avalonia.Threading;
namespace SourceGit.Commands
{
public partial class Command
{
public class CancelToken
{
public bool Requested { get; set; } = false;
}
public class ReadToEndResult
{
public bool IsSuccess { get; set; } = false;
public string StdOut { get; set; } = "";
public string StdErr { get; set; } = "";
2024-03-20 00:36:10 -07:00
}
public enum EditorType
{
None,
CoreEditor,
RebaseEditor,
}
2024-03-20 00:36:10 -07:00
public string Context { get; set; } = string.Empty;
public CancelToken Cancel { get; set; } = null;
public string WorkingDirectory { get; set; } = null;
public EditorType Editor { get; set; } = EditorType.CoreEditor; // Only used in Exec() mode
2024-07-09 03:13:15 -07:00
public string SSHKey { get; set; } = string.Empty;
2024-03-20 00:36:10 -07:00
public string Args { get; set; } = string.Empty;
public bool RaiseError { get; set; } = true;
public bool TraitErrorAsOutput { get; set; } = false;
public bool Exec()
{
var start = CreateGitStartInfo();
2024-03-20 00:36:10 -07:00
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
var isCancelled = false;
proc.OutputDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
2024-03-20 00:36:10 -07:00
return;
}
if (e.Data != null)
OnReadline(e.Data);
2024-03-20 00:36:10 -07:00
};
proc.ErrorDataReceived += (_, e) =>
{
if (Cancel != null && Cancel.Requested)
{
isCancelled = true;
proc.CancelErrorRead();
proc.CancelOutputRead();
if (!proc.HasExited)
proc.Kill(true);
2024-03-20 00:36:10 -07:00
return;
}
if (string.IsNullOrEmpty(e.Data))
return;
if (TraitErrorAsOutput)
OnReadline(e.Data);
2024-03-20 00:36:10 -07:00
// Ignore progress messages
if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal))
return;
if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal))
return;
if (REG_PROGRESS().IsMatch(e.Data))
return;
2024-03-20 00:36:10 -07:00
errs.Add(e.Data);
};
try
{
proc.Start();
}
catch (Exception e)
{
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, e.Message);
});
}
return false;
}
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
proc.WaitForExit();
int exitCode = proc.ExitCode;
proc.Close();
if (!isCancelled && exitCode != 0 && errs.Count > 0)
{
if (RaiseError)
{
Dispatcher.UIThread.Invoke(() =>
{
App.RaiseException(Context, string.Join("\n", errs));
});
}
return false;
}
return true;
2024-03-20 00:36:10 -07:00
}
public ReadToEndResult ReadToEnd()
{
var start = CreateGitStartInfo();
2024-03-20 00:36:10 -07:00
var proc = new Process() { StartInfo = start };
2024-03-20 00:36:10 -07:00
try
{
proc.Start();
}
catch (Exception e)
2024-03-20 00:36:10 -07:00
{
return new ReadToEndResult()
{
IsSuccess = false,
StdOut = string.Empty,
StdErr = e.Message,
2024-03-20 00:36:10 -07:00
};
}
var rs = new ReadToEndResult()
{
StdOut = proc.StandardOutput.ReadToEnd(),
StdErr = proc.StandardError.ReadToEnd(),
2024-03-20 00:36:10 -07:00
};
proc.WaitForExit();
rs.IsSuccess = proc.ExitCode == 0;
proc.Close();
return rs;
}
protected virtual void OnReadline(string line)
{
// Implemented by derived class
}
private ProcessStartInfo CreateGitStartInfo()
{
var start = new ProcessStartInfo();
start.FileName = Native.OS.GitExecutable;
start.Arguments = "--no-pager -c core.quotepath=off -c credential.helper=manager ";
start.UseShellExecute = false;
start.CreateNoWindow = true;
start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.StandardOutputEncoding = Encoding.UTF8;
start.StandardErrorEncoding = Encoding.UTF8;
// Force using this app as SSH askpass program
var selfExecFile = Process.GetCurrentProcess().MainModule!.FileName;
if (!OperatingSystem.IsLinux())
start.Environment.Add("DISPLAY", "required");
start.Environment.Add("SSH_ASKPASS", selfExecFile); // Can not use parameter here, because it invoked by SSH with `exec`
start.Environment.Add("SSH_ASKPASS_REQUIRE", "prefer");
start.Environment.Add("SOURCEGIT_LAUNCH_AS_ASKPASS", "TRUE");
// If an SSH private key was provided, sets the environment.
if (!string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new -i '{SSHKey}'");
else
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -o StrictHostKeyChecking=accept-new");
// Force using en_US.UTF-8 locale to avoid GCM crash
if (OperatingSystem.IsLinux())
start.Environment.Add("LANG", "en_US.UTF-8");
// Fix sometimes `LSEnvironment` not working on macOS
if (OperatingSystem.IsMacOS())
{
if (start.Environment.TryGetValue("PATH", out var path))
{
path = "/opt/homebrew/bin:/opt/homebrew/sbin:" + path;
}
else
{
path = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
}
start.Environment.Add("PATH", path);
}
// Force using this app as git editor.
switch (Editor)
{
case EditorType.CoreEditor:
start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --core-editor\" ";
break;
case EditorType.RebaseEditor:
start.Arguments += $"-c core.editor=\"\\\"{selfExecFile}\\\" --rebase-message-editor\" -c sequence.editor=\"\\\"{selfExecFile}\\\" --rebase-todo-editor\" -c rebase.abbreviateCommands=true ";
break;
default:
start.Arguments += "-c core.editor=true ";
break;
}
// Append command args
start.Arguments += Args;
// Working directory
if (!string.IsNullOrEmpty(WorkingDirectory))
start.WorkingDirectory = WorkingDirectory;
return start;
}
2024-03-20 00:36:10 -07:00
[GeneratedRegex(@"\d+%")]
private static partial Regex REG_PROGRESS();
2024-03-20 00:36:10 -07:00
}
}