diff --git a/GitCommands/StringExtensions.cs b/GitCommands/StringExtensions.cs index 38dde28ff5b..2950f41e417 100644 --- a/GitCommands/StringExtensions.cs +++ b/GitCommands/StringExtensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Text; +using GitCommands.Utils; using GitExtUtils; // ReSharper disable once CheckNamespace @@ -189,6 +190,18 @@ public static string Quote(this string? s, string q = "\"") return string.IsNullOrEmpty(s) ? s : s.Quote(); } + /// + /// Quotes and escapes this string for use as a command line argument. + /// + [Pure] + public static string QuoteForCommandLine(this string s, bool? forWindows = null) + { + return $"\"{((forWindows ?? EnvUtils.RunningOnWindows()) ? EscapeForWindowsCommandLine(s) : EscapeForPosixCommandLine(s))}\""; + + static string EscapeForWindowsCommandLine(string s) => s.Replace("\"", "\"\""); + static string EscapeForPosixCommandLine(string s) => s.Replace(@"\", @"\\").Replace("\"", "\\\"").Replace("'", @"\'"); + } + /// /// Adds parentheses if string is not null and not empty. /// diff --git a/GitUI/CommandsDialogs/FormCommit.Designer.cs b/GitUI/CommandsDialogs/FormCommit.Designer.cs index f2e3bec615d..8c505a9758b 100644 --- a/GitUI/CommandsDialogs/FormCommit.Designer.cs +++ b/GitUI/CommandsDialogs/FormCommit.Designer.cs @@ -46,6 +46,8 @@ private void InitializeComponent() assumeUnchangedToolStripMenuItem = new ToolStripMenuItem(); doNotAssumeUnchangedToolStripMenuItem = new ToolStripMenuItem(); interactiveAddToolStripMenuItem = new ToolStripMenuItem(); + toolStripSeparatorScript = new ToolStripSeparator(); + runScriptToolStripMenuItem = new ToolStripMenuItem(); fileTooltip = new ToolTip(components); StageInSuperproject = new CheckBox(); StagedFileContext = new ContextMenuStrip(components); @@ -61,6 +63,8 @@ private void InitializeComponent() stagedCopyPathToolStripMenuItem14 = new ToolStripMenuItem(); stagedOpenFolderToolStripMenuItem10 = new ToolStripMenuItem(); stagedEditFileToolStripMenuItem11 = new ToolStripMenuItem(); + stagedToolStripSeparatorScript = new ToolStripSeparator(); + stagedRunScriptToolStripMenuItem = new ToolStripMenuItem(); UnstagedSubmoduleContext = new ContextMenuStrip(components); commitSubmoduleChanges = new ToolStripMenuItem(); resetSubmoduleChanges = new ToolStripMenuItem(); @@ -211,7 +215,9 @@ private void InitializeComponent() skipWorktreeToolStripMenuItem, doNotSkipWorktreeToolStripMenuItem, assumeUnchangedToolStripMenuItem, - doNotAssumeUnchangedToolStripMenuItem}); + doNotAssumeUnchangedToolStripMenuItem, + toolStripSeparatorScript, + runScriptToolStripMenuItem}); UnstagedFileContext.Name = "UnstagedFileContext"; UnstagedFileContext.Size = new Size(233, 414); UnstagedFileContext.Opening += UnstagedFileContext_Opening; @@ -374,6 +380,18 @@ private void InitializeComponent() interactiveAddToolStripMenuItem.Text = "Interactive Add"; interactiveAddToolStripMenuItem.Click += interactiveAddToolStripMenuItem_Click; // + // toolStripSeparatorScript + // + toolStripSeparatorScript.Name = "toolStripSeparatorScript"; + toolStripSeparatorScript.Size = new Size(259, 6); + // + // runScriptToolStripMenuItem + // + runScriptToolStripMenuItem.Image = Properties.Images.Console; + runScriptToolStripMenuItem.Name = "runScriptToolStripMenuItem"; + runScriptToolStripMenuItem.Size = new Size(262, 22); + runScriptToolStripMenuItem.Text = "Run script"; + // // StageInSuperproject // StageInSuperproject.AutoSize = true; @@ -401,7 +419,9 @@ private void InitializeComponent() stagedCopyPathToolStripMenuItem14, stagedOpenFolderToolStripMenuItem10, stagedFileHistoryToolStripSeparator, - stagedFileHistoryToolStripMenuItem6}); + stagedFileHistoryToolStripMenuItem6, + stagedToolStripSeparatorScript, + stagedRunScriptToolStripMenuItem}); StagedFileContext.Name = "StagedFileContext"; StagedFileContext.Size = new Size(233, 198); StagedFileContext.Opening += StagedFileContext_Opening; @@ -491,6 +511,18 @@ private void InitializeComponent() stagedEditFileToolStripMenuItem11.Text = "Edit file"; stagedEditFileToolStripMenuItem11.Click += editFileToolStripMenuItem_Click; // + // stagedToolStripSeparatorScript + // + stagedToolStripSeparatorScript.Name = "stagedToolStripSeparatorScript"; + stagedToolStripSeparatorScript.Size = new Size(259, 6); + // + // stagedRunScriptToolStripMenuItem + // + stagedRunScriptToolStripMenuItem.Image = Properties.Images.Console; + stagedRunScriptToolStripMenuItem.Name = "stagedRunScriptToolStripMenuItem"; + stagedRunScriptToolStripMenuItem.Size = new Size(262, 22); + stagedRunScriptToolStripMenuItem.Text = "Run script"; + // // UnstagedSubmoduleContext // UnstagedSubmoduleContext.Items.AddRange(new ToolStripItem[] { @@ -1660,6 +1692,10 @@ private void InitializeComponent() private ToolStripMenuItem stagedCopyPathToolStripMenuItem14; private ToolStripSeparator toolStripSeparator12; private ToolStripMenuItem interactiveAddToolStripMenuItem; + private ToolStripSeparator toolStripSeparatorScript; + private ToolStripMenuItem runScriptToolStripMenuItem; + private ToolStripSeparator stagedToolStripSeparatorScript; + private ToolStripMenuItem stagedRunScriptToolStripMenuItem; private TableLayoutPanel tableLayoutPanel1; private StatusStrip commitStatusStrip; private ToolStripStatusLabel commitAuthorStatus; diff --git a/GitUI/CommandsDialogs/FormCommit.cs b/GitUI/CommandsDialogs/FormCommit.cs index 85368375920..7b8e4a84c34 100644 --- a/GitUI/CommandsDialogs/FormCommit.cs +++ b/GitUI/CommandsDialogs/FormCommit.cs @@ -404,6 +404,8 @@ void AddToSelectionFilter() } }); + UICommands.PostRepositoryChanged += UICommands_PostRepositoryChanged; + return; void ConfigureMessageBox() @@ -430,6 +432,11 @@ protected override void Dispose(bool disposing) { if (disposing) { + if (!IsDesignMode && !IsUnitTestActive) + { + UICommands.PostRepositoryChanged -= UICommands_PostRepositoryChanged; + } + _unstagedLoader.Dispose(); _customDiffToolsSequence.Dispose(); _interactiveAddSequence.Dispose(); @@ -601,6 +608,20 @@ protected override void OnKeyUp(KeyEventArgs e) return; } + protected override void OnUICommandsChanged(GitUICommandsChangedEventArgs e) + { + GitUICommands oldCommands = e.OldCommands; + + if (oldCommands is not null) + { + oldCommands.PostRepositoryChanged -= UICommands_PostRepositoryChanged; + } + + UICommands.PostRepositoryChanged += UICommands_PostRepositoryChanged; + + base.OnUICommandsChanged(e); + } + public override bool ProcessHotkey(Keys keyData) { if (IsDesignMode || !HotkeysEnabled) @@ -889,6 +910,11 @@ protected override bool ExecuteCommand(int cmd) } } + public override IScriptOptionsProvider? GetScriptOptionsProvider() + { + return new ScriptOptionsProvider(_currentFilesList, () => _fullPathResolver, () => SelectedDiff.CurrentFileLine); + } + #endregion private void ComputeUnstagedFiles(Action> onComputed, bool doAsync) @@ -1647,6 +1673,8 @@ private void UnstagedFileContext_Opening(object sender, System.ComponentModel.Ca openWithToolStripMenuItem.Enabled = !isAnyDeleted; deleteFileToolStripMenuItem.Enabled = !isAnyDeleted; openContainingFolderToolStripMenuItem.Enabled = !isAnyDeleted; + + UnstagedFileContext.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, script => script.OnEvent == ScriptEvent.ShowInFileList, UICommands); } private void StagedFileContext_Opening(object sender, System.ComponentModel.CancelEventArgs e) @@ -1666,6 +1694,8 @@ private void StagedFileContext_Opening(object sender, System.ComponentModel.Canc stagedOpenToolStripMenuItem7.Enabled = !isAnyDeleted; stagedOpenWithToolStripMenuItem8.Enabled = !isAnyDeleted; stagedOpenFolderToolStripMenuItem10.Enabled = !isAnyDeleted; + + StagedFileContext.AddUserScripts(stagedRunScriptToolStripMenuItem, ExecuteCommand, script => script.OnEvent == ScriptEvent.ShowInFileList, UICommands); } private void UnstagedSubmoduleContext_Opening(object sender, System.ComponentModel.CancelEventArgs e) @@ -3463,6 +3493,18 @@ private void UpdateButtonStates() : TranslatedStrings.ButtonPush; } + private void UICommands_PostRepositoryChanged(object sender, GitUIEventArgs e) + { + if (!_skipUpdate && !_bypassActivatedEventHandler) + { + ThreadHelper.FileAndForget(async () => + { + await this.SwitchToMainThreadAsync(); + RescanChanges(); + }); + } + } + internal TestAccessor GetTestAccessor() => new(this); diff --git a/GitUI/CommandsDialogs/RevisionDiffControl.Designer.cs b/GitUI/CommandsDialogs/RevisionDiffControl.Designer.cs index 493b2ecd7e2..33215db837a 100644 --- a/GitUI/CommandsDialogs/RevisionDiffControl.Designer.cs +++ b/GitUI/CommandsDialogs/RevisionDiffControl.Designer.cs @@ -61,6 +61,8 @@ private void InitializeComponent() blameToolStripMenuItem = new ToolStripMenuItem(); findInDiffToolStripMenuItem = new ToolStripMenuItem(); showSearchCommitToolStripMenuItem = new ToolStripMenuItem(); + toolStripSeparatorScript = new ToolStripSeparator(); + runScriptToolStripMenuItem = new ToolStripMenuItem(); DiffText = new GitUI.Editor.FileViewer(); BlameControl = new Blame.BlameControl(); saveToolStripMenuItem = new ToolStripMenuItem(); @@ -146,7 +148,9 @@ private void InitializeComponent() fileHistoryDiffToolstripMenuItem, blameToolStripMenuItem, findInDiffToolStripMenuItem, - showSearchCommitToolStripMenuItem}); + showSearchCommitToolStripMenuItem, + toolStripSeparatorScript, + runScriptToolStripMenuItem}); DiffContextMenu.Name = "DiffContextMenu"; DiffContextMenu.Size = new Size(263, 534); DiffContextMenu.Opening += DiffContextMenu_Opening; @@ -444,6 +448,18 @@ private void InitializeComponent() showSearchCommitToolStripMenuItem.Text = "Sear&ch files in commit..."; showSearchCommitToolStripMenuItem.Click += showSearchCommitToolStripMenuItem_Click; // + // toolStripSeparatorScript + // + toolStripSeparatorScript.Name = "toolStripSeparatorScript"; + toolStripSeparatorScript.Size = new Size(259, 6); + // + // runScriptToolStripMenuItem + // + runScriptToolStripMenuItem.Image = Properties.Images.Console; + runScriptToolStripMenuItem.Name = "runScriptToolStripMenuItem"; + runScriptToolStripMenuItem.Size = new Size(262, 22); + runScriptToolStripMenuItem.Text = "Run script"; + // // DiffText // DiffText.Dock = DockStyle.Fill; @@ -526,5 +542,7 @@ private void InitializeComponent() private ToolStripMenuItem diffOpenWorkingDirectoryFileWithToolStripMenuItem; private ToolStripMenuItem diffOpenRevisionFileToolStripMenuItem; private ToolStripMenuItem diffOpenRevisionFileWithToolStripMenuItem; + private ToolStripSeparator toolStripSeparatorScript; + private ToolStripMenuItem runScriptToolStripMenuItem; } } diff --git a/GitUI/CommandsDialogs/RevisionDiffControl.cs b/GitUI/CommandsDialogs/RevisionDiffControl.cs index 709b5fd1d5c..8aca25dabe9 100644 --- a/GitUI/CommandsDialogs/RevisionDiffControl.cs +++ b/GitUI/CommandsDialogs/RevisionDiffControl.cs @@ -5,6 +5,7 @@ using GitUI.CommandDialogs; using GitUI.CommandsDialogs.BrowseDialog; using GitUI.HelperDialogs; +using GitUI.ScriptsEngine; using GitUI.UserControls; using GitUI.UserControls.RevisionGrid; using GitUIPluginInterfaces; @@ -188,6 +189,11 @@ bool SelectFirstGroupChangesIfFileNotFocused() } } + protected override IScriptOptionsProvider? GetScriptOptionsProvider() + { + return new ScriptOptionsProvider(DiffFiles, () => _fullPathResolver, () => BlameControl.Visible ? BlameControl.CurrentFileLine : DiffText.CurrentFileLine); + } + public void ReloadHotkeys() { LoadHotkeys(HotkeySettingsName); @@ -747,6 +753,8 @@ private void UpdateStatusOfMenuItems() { blameToolStripMenuItem.Checked = false; } + + DiffContextMenu.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, script => script.OnEvent == ScriptEvent.ShowInFileList, UICommands); } private void DiffContextMenu_Opening(object sender, CancelEventArgs e) diff --git a/GitUI/CommandsDialogs/RevisionFileTreeControl.Designer.cs b/GitUI/CommandsDialogs/RevisionFileTreeControl.Designer.cs index 8331f8466da..9b95ddc07c3 100644 --- a/GitUI/CommandsDialogs/RevisionFileTreeControl.Designer.cs +++ b/GitUI/CommandsDialogs/RevisionFileTreeControl.Designer.cs @@ -48,6 +48,8 @@ private void InitializeComponent() findToolStripMenuItem = new ToolStripMenuItem(); expandToolStripMenuItem = new ToolStripMenuItem(); collapseAllToolStripMenuItem = new ToolStripMenuItem(); + toolStripSeparatorScript = new ToolStripSeparator(); + runScriptToolStripMenuItem = new ToolStripMenuItem(); FileText = new GitUI.Editor.FileViewer(); BlameControl = new Blame.BlameControl(); ((System.ComponentModel.ISupportInitialize)(FileTreeSplitContainer)).BeginInit(); @@ -125,7 +127,9 @@ private void InitializeComponent() toolStripSeparatorGitTrackingActions, findToolStripMenuItem, expandToolStripMenuItem, - collapseAllToolStripMenuItem}); + collapseAllToolStripMenuItem, + toolStripSeparatorScript, + runScriptToolStripMenuItem}); FileTreeContextMenu.Name = "FileTreeContextMenu"; FileTreeContextMenu.Size = new Size(326, 474); FileTreeContextMenu.Opening += FileTreeContextMenu_Opening; @@ -323,6 +327,18 @@ private void InitializeComponent() collapseAllToolStripMenuItem.Text = "Co&llapse all"; collapseAllToolStripMenuItem.Click += collapseAllToolStripMenuItem_Click; // + // toolStripSeparatorScript + // + toolStripSeparatorScript.Name = "toolStripSeparatorScript"; + toolStripSeparatorScript.Size = new Size(259, 6); + // + // runScriptToolStripMenuItem + // + runScriptToolStripMenuItem.Image = Properties.Images.Console; + runScriptToolStripMenuItem.Name = "runScriptToolStripMenuItem"; + runScriptToolStripMenuItem.Size = new Size(262, 22); + runScriptToolStripMenuItem.Text = "Run script"; + // // FileText // FileText.Dock = DockStyle.Fill; @@ -391,5 +407,7 @@ private void InitializeComponent() private ToolStripSeparator toolStripSeparatorGitActions; private ToolStripMenuItem stopTrackingThisFileToolStripMenuItem; private ToolStripMenuItem expandToolStripMenuItem; + private ToolStripSeparator toolStripSeparatorScript; + private ToolStripMenuItem runScriptToolStripMenuItem; } } diff --git a/GitUI/CommandsDialogs/RevisionFileTreeControl.cs b/GitUI/CommandsDialogs/RevisionFileTreeControl.cs index c7e19f2f361..eb1de4af366 100644 --- a/GitUI/CommandsDialogs/RevisionFileTreeControl.cs +++ b/GitUI/CommandsDialogs/RevisionFileTreeControl.cs @@ -7,6 +7,7 @@ using GitUI.CommandDialogs; using GitUI.CommandsDialogs.BrowseDialog; using GitUI.Properties; +using GitUI.ScriptsEngine; using GitUI.UserControls; using GitUIPluginInterfaces; using Microsoft; @@ -290,6 +291,21 @@ protected override bool ExecuteCommand(int cmd) return true; } + protected override IScriptOptionsProvider? GetScriptOptionsProvider() + { + return new ScriptOptionsProvider(() => + { + if (tvGitTree.SelectedNode?.Tag is not GitItem gitItem || gitItem.ObjectType != GitObjectType.Blob) + { + return Array.Empty(); + } + + return new string[] { gitItem.FileName }; + }, + () => _fullPathResolver, + () => BlameControl.Visible ? BlameControl.CurrentFileLine : FileText.CurrentFileLine); + } + public override bool ProcessHotkey(Keys keyData) { return base.ProcessHotkey(keyData) @@ -733,6 +749,8 @@ private void FileTreeContextMenu_Opening(object sender, System.ComponentModel.Ca findToolStripMenuItem.Enabled = tvGitTree.Nodes.Count > 0; expandToolStripMenuItem.Visible = isFolder; collapseAllToolStripMenuItem.Visible = isFolder; + + FileTreeContextMenu.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, script => script.OnEvent == ScriptEvent.ShowInFileList, UICommands); } private void fileTreeOpenContainingFolderToolStripMenuItem_Click(object sender, EventArgs e) diff --git a/GitUI/CommandsDialogs/ScriptOptionsProvider.cs b/GitUI/CommandsDialogs/ScriptOptionsProvider.cs new file mode 100644 index 00000000000..e4e919ee854 --- /dev/null +++ b/GitUI/CommandsDialogs/ScriptOptionsProvider.cs @@ -0,0 +1,49 @@ +using GitUI.ScriptsEngine; +using GitUIPluginInterfaces; + +namespace GitUI.CommandsDialogs; + +internal class ScriptOptionsProvider : IScriptOptionsProvider +{ + private const string _selectedFiles = "SelectedFiles"; + private const string _lineNumber = "LineNumber"; + + private Func> _getSelectedFiles; + private Func _getFullPathResolver; + private Func _getCurrentLineNumber; + + public ScriptOptionsProvider(Func> getSelectedFiles, Func getFullPathResolver, Func getCurrentLineNumber) + { + _getSelectedFiles = getSelectedFiles; + _getFullPathResolver = getFullPathResolver; + _getCurrentLineNumber = getCurrentLineNumber; + } + + public ScriptOptionsProvider(FileStatusList fileStatusList, Func getFullPathResolver, Func getCurrentLineNumber) + : this(() => fileStatusList.SelectedItems.Select(item => item.Item.Name), getFullPathResolver, getCurrentLineNumber) + { + } + + IReadOnlyList IScriptOptionsProvider.Options { get; } = new[] { _selectedFiles, _lineNumber }; + + string? IScriptOptionsProvider.GetValue(string option) + { + switch (option) + { + case _selectedFiles: + IEnumerable selectedFiles = _getSelectedFiles(); + if (!selectedFiles.Any()) + { + return null; + } + + IFullPathResolver fullPathResolver = _getFullPathResolver(); + return string.Join(" ", selectedFiles.Select(item => fullPathResolver.Resolve(item).QuoteForCommandLine())); + case _lineNumber: + int? line = _getCurrentLineNumber(); + return line?.ToString(); + default: + throw new NotImplementedException(option); + } + } +} diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs index 03fa42eb169..fddadc90344 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs @@ -67,7 +67,11 @@ public partial class ScriptsSettingsPage : SettingsPageWithHeader {cCommitDate} {cDefaultRemote} {cDefaultRemoteUrl} -{cDefaultRemotePathFromUrl}"); +{cDefaultRemotePathFromUrl} + +File(s): +{SelectedFiles} +{LineNumber}"); private static readonly string[] WatchedProxyProperties = new string[] { diff --git a/GitUI/CommandsDialogs/UserScriptContextMenuExtensions.cs b/GitUI/CommandsDialogs/UserScriptContextMenuExtensions.cs index 79bd7ad6379..b66413cec1e 100644 --- a/GitUI/CommandsDialogs/UserScriptContextMenuExtensions.cs +++ b/GitUI/CommandsDialogs/UserScriptContextMenuExtensions.cs @@ -18,7 +18,7 @@ public static class UserScriptContextMenuExtensions /// The menu item user scripts not marked as are added to. /// The handler that handles user script invocation. /// The DI service provider. - public static void AddUserScripts(this ContextMenuStrip contextMenu, ToolStripMenuItem hostMenuItem, Func scriptInvoker, IServiceProvider serviceProvider) + public static void AddUserScripts(this ContextMenuStrip contextMenu, ToolStripMenuItem hostMenuItem, Func scriptInvoker, Func scriptFilterAddDirect, IServiceProvider serviceProvider) { ArgumentNullException.ThrowIfNull(contextMenu); ArgumentNullException.ThrowIfNull(hostMenuItem); @@ -51,7 +51,7 @@ public static void AddUserScripts(this ContextMenuStrip contextMenu, ToolStripMe scriptInvoker(scriptId); }; - if (script.AddToRevisionGridContextMenu) + if (scriptFilterAddDirect(script)) { // insert items after hostMenuItem contextMenu.Items.Insert(++lastScriptItemIndex, item); diff --git a/GitUI/GitModuleControl.cs b/GitUI/GitModuleControl.cs index 535a1237633..1b0ef47021a 100644 --- a/GitUI/GitModuleControl.cs +++ b/GitUI/GitModuleControl.cs @@ -134,11 +134,42 @@ protected override bool ExecuteCommand(int command) bool ExecuteScriptCommand() { + IScriptsManager scriptsManager = UICommands.GetRequiredService(); + ScriptInfo? scriptInfo = scriptsManager.GetScript(command); + if (scriptInfo is null) + { + return false; + } + IScriptsRunner scriptsRunner = UICommands.GetRequiredService(); - return scriptsRunner.RunScript(command, owner: this, UICommands, this as IScriptOptionsProvider); + _ = scriptsRunner.RunScript(scriptInfo, owner: this, UICommands, FindScriptOptionsProvider()); + return true; + + IScriptOptionsProvider? FindScriptOptionsProvider() + { + for (Control control = this; control != null; control = control.Parent) + { + if (control is GitModuleControl gitModuleControl && gitModuleControl.GetScriptOptionsProvider() is IScriptOptionsProvider scriptOptionsProvider) + { + return scriptOptionsProvider; + } + + if (control is GitModuleForm gitModuleForm) + { + return gitModuleForm.GetScriptOptionsProvider(); + } + } + + return null; + } } } + protected virtual IScriptOptionsProvider? GetScriptOptionsProvider() + { + return null; + } + /// Raises the event. protected virtual void OnUICommandsSourceSet(IGitUICommandsSource source) { diff --git a/GitUI/GitModuleForm.cs b/GitUI/GitModuleForm.cs index 4d602056bb5..125f69757c2 100644 --- a/GitUI/GitModuleForm.cs +++ b/GitUI/GitModuleForm.cs @@ -105,8 +105,26 @@ protected GitModuleForm([NotNull] GitUICommands commands) protected override bool ExecuteCommand(int command) { - return ScriptsRunner.RunScript(command, owner: this, UICommands) + return ExecuteScriptCommand() || base.ExecuteCommand(command); + + bool ExecuteScriptCommand() + { + IScriptsManager scriptsManager = UICommands.GetRequiredService(); + ScriptInfo? scriptInfo = scriptsManager.GetScript(command); + if (scriptInfo is null) + { + return false; + } + + _ = ScriptsRunner.RunScript(scriptInfo, owner: this, UICommands, GetScriptOptionsProvider()); + return true; + } + } + + public virtual IScriptOptionsProvider? GetScriptOptionsProvider() + { + return null; } protected virtual void OnUICommandsChanged(GitUICommandsChangedEventArgs e) diff --git a/GitUI/LeftPanel/RepoObjectsTree.ContextActions.cs b/GitUI/LeftPanel/RepoObjectsTree.ContextActions.cs index 88150050b8c..345751811b9 100644 --- a/GitUI/LeftPanel/RepoObjectsTree.ContextActions.cs +++ b/GitUI/LeftPanel/RepoObjectsTree.ContextActions.cs @@ -245,7 +245,7 @@ private void contextMenu_Opening(object sender, CancelEventArgs e) if (hasSingleSelection && selectedLocalBranch?.Visible == true) { - contextMenu.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, UICommands); + contextMenu.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, script => script.AddToRevisionGridContextMenu, UICommands); } else { diff --git a/GitUI/ScriptsEngine/IScriptOptionsProvider.cs b/GitUI/ScriptsEngine/IScriptOptionsProvider.cs index 4b36bdaa51e..51fdaa5d2ac 100644 --- a/GitUI/ScriptsEngine/IScriptOptionsProvider.cs +++ b/GitUI/ScriptsEngine/IScriptOptionsProvider.cs @@ -12,5 +12,5 @@ public interface IScriptOptionsProvider /// /// The option identifier which is to be replaced. /// The value to be used for the script argument. - string GetValue(string option); + string? GetValue(string option); } diff --git a/GitUI/ScriptsEngine/IScriptsRunner.cs b/GitUI/ScriptsEngine/IScriptsRunner.cs index 413db650426..f40eaf2c0ad 100644 --- a/GitUI/ScriptsEngine/IScriptsRunner.cs +++ b/GitUI/ScriptsEngine/IScriptsRunner.cs @@ -8,6 +8,6 @@ public interface IScriptsRunner bool RunEventScripts(ScriptEvent scriptEvent, THostForm form) where THostForm : IGitModuleForm, IWin32Window; - bool RunScript(int scriptId, IWin32Window owner, IGitUICommands commands, IScriptOptionsProvider? scriptOptionsProvider = null); + bool RunScript(ScriptInfo scriptInfo, IWin32Window owner, IGitUICommands commands, IScriptOptionsProvider? scriptOptionsProvider = null); } } diff --git a/GitUI/ScriptsEngine/ScriptEvent.cs b/GitUI/ScriptsEngine/ScriptEvent.cs index eef741bf048..55fa3457916 100644 --- a/GitUI/ScriptsEngine/ScriptEvent.cs +++ b/GitUI/ScriptsEngine/ScriptEvent.cs @@ -15,6 +15,7 @@ public enum ScriptEvent BeforeMerge, AfterMerge, BeforeFetch, - AfterFetch + AfterFetch, + ShowInFileList } } diff --git a/GitUI/ScriptsEngine/ScriptsManager.cs b/GitUI/ScriptsEngine/ScriptsManager.cs index 8f2cdef07a4..050785c9c31 100644 --- a/GitUI/ScriptsEngine/ScriptsManager.cs +++ b/GitUI/ScriptsEngine/ScriptsManager.cs @@ -68,15 +68,8 @@ public bool RunEventScripts(ScriptEvent scriptEvent, THostForm form) return true; } - public bool RunScript(int scriptId, IWin32Window owner, IGitUICommands commands, IScriptOptionsProvider? scriptOptionsProvider = null) + public bool RunScript(ScriptInfo scriptInfo, IWin32Window owner, IGitUICommands commands, IScriptOptionsProvider? scriptOptionsProvider = null) { - ScriptInfo? scriptInfo = GetScript(scriptId); - if (scriptInfo is null) - { - throw new UserExternalOperationException($"{TranslatedStrings.ScriptErrorCantFind}: '{scriptId}'", - new ExternalOperationException(workingDirectory: commands.Module.WorkingDir)); - } - return ScriptRunner.RunScript(scriptInfo, owner, commands, scriptOptionsProvider); } diff --git a/GitUI/Translation/English.xlf b/GitUI/Translation/English.xlf index eff2bb2b9db..e8a6e480453 100644 --- a/GitUI/Translation/English.xlf +++ b/GitUI/Translation/English.xlf @@ -3950,6 +3950,10 @@ You can unset the template: Reset submodule changes + + Run script + + Selection filter @@ -4010,6 +4014,10 @@ You can unset the template: Reset file or directory changes + + Run script + + Stash submodule changes @@ -8949,6 +8957,10 @@ Reset the filter via View > Show all branches. &Reset file(s) to + + Run script + + S&ave selected as... @@ -9119,6 +9131,10 @@ See the changes in the commit form. &Reset to selected revision + + Run script + + S&ave as... @@ -9744,7 +9760,11 @@ Currently checked out revision: {cCommitDate} {cDefaultRemote} {cDefaultRemoteUrl} -{cDefaultRemotePathFromUrl} +{cDefaultRemotePathFromUrl} + +File(s): +{SelectedFiles} +{LineNumber} diff --git a/GitUI/UserControls/RevisionGrid/RevisionGridControl.cs b/GitUI/UserControls/RevisionGrid/RevisionGridControl.cs index 4aa5635f0ac..cf869e11f91 100644 --- a/GitUI/UserControls/RevisionGrid/RevisionGridControl.cs +++ b/GitUI/UserControls/RevisionGrid/RevisionGridControl.cs @@ -2138,7 +2138,7 @@ private void ContextMenuOpening(object sender, CancelEventArgs e) SetEnabled(openPullRequestPageStripMenuItem, !string.IsNullOrWhiteSpace(revision.BuildStatus?.PullRequestUrl)); - mainContextMenu.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, UICommands); + mainContextMenu.AddUserScripts(runScriptToolStripMenuItem, ExecuteCommand, script => script.AddToRevisionGridContextMenu, UICommands); UpdateSeparators(); diff --git a/UnitTests/GitCommands.Tests/StringExtensionTests.cs b/UnitTests/GitCommands.Tests/StringExtensionTests.cs index 929ea06c016..10d95316d0c 100644 --- a/UnitTests/GitCommands.Tests/StringExtensionTests.cs +++ b/UnitTests/GitCommands.Tests/StringExtensionTests.cs @@ -130,5 +130,23 @@ public void SubstringAfterLast_string_works_as_expected(string str, string s, st { Assert.AreEqual(expected, str.SubstringAfterLast(s)); } + + [TestCase("/usr/bin", false, "\"/usr/bin\"")] + [TestCase("/usr/bin", true, "\"/usr/bin\"")] + [TestCase("\\usr\\bin", false, "\"\\\\usr\\\\bin\"")] + [TestCase("\\usr\\bin", true, "\"\\usr\\bin\"")] + [TestCase("C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\setup.exe", true, "\"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\setup.exe\"")] + [TestCase("echo \"Hello world\"", false, "\"echo \\\"Hello world\\\"\"")] + [TestCase("echo \"Hello world\"", true, "\"echo \"\"Hello world\"\"\"")] + [TestCase("echo \'Hello world\'", false, "\"echo \\\'Hello world\\\'\"")] + [TestCase("echo \'Hello world\'", true, "\"echo \'Hello world\'\"")] + [TestCase("cmd /c \"echo \\\"Hello world\\\"\"", false, "\"cmd /c \\\"echo \\\\\\\"Hello world\\\\\\\"\\\"\"")] + [TestCase("cmd /c \"echo \\\"Hello world\\\"\"", true, "\"cmd /c \"\"echo \\\"\"Hello world\\\"\"\"\"\"")] + [TestCase("cmd /c \"echo \"\"Hello world\"\"\"", false, "\"cmd /c \\\"echo \\\"\\\"Hello world\\\"\\\"\\\"\"")] + [TestCase("cmd /c \"echo \"\"Hello world\"\"\"", true, "\"cmd /c \"\"echo \"\"\"\"Hello world\"\"\"\"\"\"\"")] + public void QuoteForCommandLine_works_as_expected(string s, bool forWindows, string expected) + { + Assert.AreEqual(expected, s.QuoteForCommandLine(forWindows)); + } } }