using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { public interface ICustomActionControlParameter { string GetValue(); } public class CustomActionControlTextBox : ICustomActionControlParameter { public string Label { get; set; } public string Placeholder { get; set; } public string Text { get; set; } public string Formatter { get; set; } public CustomActionControlTextBox(string label, string placeholder, string defaultValue, string formatter) { Label = label + ":"; Placeholder = placeholder; Text = defaultValue; Formatter = formatter; } public string GetValue() { if (string.IsNullOrEmpty(Text)) return string.Empty; return string.IsNullOrEmpty(Formatter) ? Text : Formatter.Replace("${VALUE}", Text); } } public class CustomActionControlPathSelector : ObservableObject, ICustomActionControlParameter { public string Label { get; set; } public string Placeholder { get; set; } public bool IsFolder { get; set; } public string Path { get => _path; set => SetProperty(ref _path, value); } public CustomActionControlPathSelector(string label, string placeholder, bool isFolder, string defaultValue) { Label = label + ":"; Placeholder = placeholder; IsFolder = isFolder; _path = defaultValue; } public string GetValue() => _path; private string _path; } public class CustomActionControlCheckBox : ICustomActionControlParameter { public string Label { get; set; } public string ToolTip { get; set; } public string CheckedValue { get; set; } public bool IsChecked { get; set; } public CustomActionControlCheckBox(string label, string tooltip, string checkedValue, bool isChecked) { Label = label; ToolTip = string.IsNullOrEmpty(tooltip) ? null : tooltip; CheckedValue = checkedValue; IsChecked = isChecked; } public string GetValue() => IsChecked ? CheckedValue : string.Empty; } public class CustomActionControlComboBox : ObservableObject, ICustomActionControlParameter { public string Label { get; set; } public string Description { get; set; } public List Options { get; set; } = []; public string Value { get; set; } public CustomActionControlComboBox(string label, string description, string options) { Label = label; Description = description; var parts = options.Split('|', StringSplitOptions.TrimEntries); if (parts.Length > 0) { Options.AddRange(parts); Value = parts[0]; } } public string GetValue() => Value; } public class CustomActionControlBranchSelector : ObservableObject, ICustomActionControlParameter { public string Label { get; set; } public string Description { get; set; } public List Branches { get; set; } = []; public Models.Branch SelectedBranch { get; set; } public CustomActionControlBranchSelector(string label, string description, Repository repo, bool isLocal, bool useFriendlyName, object target) { Label = label; Description = description; _useFriendlyName = useFriendlyName; foreach (var b in repo.Branches) { if (b.IsLocal == isLocal && !b.IsDetachedHead) Branches.Add(b); } if (Branches.Count == 0) return; if (target is Models.Branch branch) { if (isLocal) { var prefer = Branches.Find(b => b.FullName.Equals(branch.FullName, StringComparison.Ordinal)); if (prefer != null) { SelectedBranch = prefer; return; } } else if (!string.IsNullOrEmpty(branch.Upstream)) { var upstream = Branches.Find(b => b.FullName.Equals(branch.Upstream, StringComparison.Ordinal)); if (upstream != null) { SelectedBranch = upstream; return; } } } SelectedBranch = Branches[0]; } public string GetValue() { if (SelectedBranch == null) return string.Empty; return _useFriendlyName ? SelectedBranch.FriendlyName : SelectedBranch.Name; } private bool _useFriendlyName = false; } public class ExecuteCustomAction : Popup { public Models.CustomAction CustomAction { get; } public object Target { get; } public List ControlParameters { get; } = []; public ExecuteCustomAction(Repository repo, Models.CustomAction action, object scopeTarget) { _repo = repo; CustomAction = action; Target = scopeTarget ?? new Models.Null(); foreach (var ctl in CustomAction.Controls) { switch (ctl.Type) { case Models.CustomActionControlType.TextBox: ControlParameters.Add(new CustomActionControlTextBox(ctl.Label, ctl.Description, PrepareStringByTarget(ctl.StringValue), ctl.StringFormatter)); break; case Models.CustomActionControlType.PathSelector: ControlParameters.Add(new CustomActionControlPathSelector(ctl.Label, ctl.Description, ctl.BoolValue, PrepareStringByTarget(ctl.StringValue))); break; case Models.CustomActionControlType.CheckBox: ControlParameters.Add(new CustomActionControlCheckBox(ctl.Label, ctl.Description, ctl.StringValue, ctl.BoolValue)); break; case Models.CustomActionControlType.ComboBox: ControlParameters.Add(new CustomActionControlComboBox(ctl.Label, ctl.Description, PrepareStringByTarget(ctl.StringValue))); break; case Models.CustomActionControlType.LocalBranchSelector: ControlParameters.Add(new CustomActionControlBranchSelector(ctl.Label, ctl.Description, _repo, true, false, scopeTarget ?? _repo.CurrentBranch)); break; case Models.CustomActionControlType.RemoteBranchSelector: ControlParameters.Add(new CustomActionControlBranchSelector(ctl.Label, ctl.Description, _repo, false, ctl.BoolValue, scopeTarget ?? _repo.CurrentBranch)); break; } } } public override async Task Sure() { using var lockWatcher = _repo.LockWatcher(); ProgressDescription = "Run custom action ..."; var cmdline = PrepareStringByTarget(CustomAction.Arguments); for (var i = ControlParameters.Count - 1; i >= 0; i--) { var param = ControlParameters[i]; cmdline = cmdline.Replace($"${i + 1}", param.GetValue()); } var log = _repo.CreateLog(CustomAction.Name); Use(log); log.AppendLine($"$ {CustomAction.Executable} {cmdline}\n"); if (CustomAction.WaitForExit) await RunAsync(cmdline, log); else _ = Task.Run(() => Run(cmdline)); log.Complete(); return true; } private string PrepareStringByTarget(string org) { var repoPath = OperatingSystem.IsWindows() ? _repo.FullPath.Replace("/", "\\") : _repo.FullPath; org = org.Replace("${REPO}", repoPath); return Target switch { Models.Branch b => org.Replace("${BRANCH_FRIENDLY_NAME}", b.FriendlyName).Replace("${BRANCH}", b.Name).Replace("${REMOTE}", b.Remote), Models.Commit c => org.Replace("${SHA}", c.SHA), Models.Tag t => org.Replace("${TAG}", t.Name), Models.Remote r => org.Replace("${REMOTE}", r.Name), Models.CustomActionTargetFile f => org.Replace("${FILE}", f.File).Replace("${SHA}", f.Revision?.SHA ?? string.Empty), _ => org.Replace("${BRANCH}", _repo.CurrentBranch?.Name ?? "HEAD") }; } private void Run(string args) { var start = new ProcessStartInfo(); start.FileName = CustomAction.Executable; start.Arguments = args; start.UseShellExecute = false; start.CreateNoWindow = true; start.WorkingDirectory = _repo.FullPath; try { Process.Start(start); } catch (Exception e) { _repo.SendNotification(e.Message, true); } } private async Task RunAsync(string args, Models.ICommandLog log) { var start = new ProcessStartInfo(); start.FileName = CustomAction.Executable; start.Arguments = args; start.UseShellExecute = false; start.CreateNoWindow = true; start.RedirectStandardOutput = true; start.RedirectStandardError = true; start.StandardOutputEncoding = Encoding.UTF8; start.StandardErrorEncoding = Encoding.UTF8; start.WorkingDirectory = _repo.FullPath; using var proc = new Process(); proc.StartInfo = start; proc.OutputDataReceived += (_, e) => { if (e.Data != null) log?.AppendLine(e.Data); }; var builder = new StringBuilder(); proc.ErrorDataReceived += (_, e) => { if (e.Data != null) { log?.AppendLine(e.Data); builder.AppendLine(e.Data); } }; try { proc.Start(); proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); await proc.WaitForExitAsync().ConfigureAwait(false); var exitCode = proc.ExitCode; if (exitCode != 0) { var errMsg = builder.ToString().Trim(); if (!string.IsNullOrEmpty(errMsg)) _repo.SendNotification(errMsg, true); } } catch (Exception e) { _repo.SendNotification(e.Message, true); } } private readonly Repository _repo = null; } }