Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user scripts to operate on selected files #11239

Merged
merged 4 commits into from
Apr 14, 2024

Conversation

SlugFiller
Copy link
Contributor

@SlugFiller SlugFiller commented Oct 3, 2023

Fixes #9974
Fixes #8769
Alternative to #11183

Proposed changes

  • Add Run Scripts option to diff file list, file tree, and commit form file lists
  • Add SelectedFiles and LineNumber parameters to script parameters, based on currently selected files in the matching list, and the current selected line in the diff/blame view.
  • Add conditional parameters, e.g. {if:SelectedFiles} --files {SelectedFiles}{/if}. The condition is based on whether the parameter is available, e.g. the script was executed from the diff view and any files are selected. Otherwise, the text in between is discarded. Also added {ifnot:option} for the opposite condition/fallback.
  • Added ShowInFileList event for showing an option directly in the context menu for file lists, and not in the Run Scripts submenu. Similar to Show in RevisionGrid for the revision grid.

Screenshots

image
image
image

Before

Run Script was only available in the revision grid,

After

Run Script is available in revision grid, diff file list, file tree, and commit file list (staged and unstaged).

Test methodology

  • Added a test user script using all the new features (notepad++.exe {if:SelectedFiles}{SelectedFiles} {if:LineNumber}-n{LineNumber}{/if}{/if})
  • Ran from the menu
  • Ran using a shortcut from multiple locations (revision tree, diff file list, file tree, commit window, diff view, blame view

Test environment(s)

  • Git Extensions 33.33.33
  • Build 98f366d
  • Git 2.42.0.windows.1
  • Microsoft Windows NT 10.0.19045.0
  • .NET 6.0.22
  • DPI 96dpi (no scaling)
  • Portable: False
  • Microsoft.WindowsDesktop.App Versions
    Microsoft.WindowsDesktop.App 6.0.21 [D:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
    Microsoft.WindowsDesktop.App 6.0.22 [D:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Merge strategy

I agree that the maintainer squash merge this PR (if the commit message is clear).


✒️ I contribute this code under The Developer Certificate of Origin.

@ghost ghost assigned SlugFiller Oct 3, 2023
@SlugFiller SlugFiller force-pushed the file-scripts branch 3 times, most recently from eeb9700 to c18df2d Compare October 3, 2023 14:47
@RussKie RussKie mentioned this pull request Oct 4, 2023
@RussKie
Copy link
Member

RussKie commented Oct 4, 2023

I wish you've indicated your desire to work on this so that we could discuss the approach and the implementation.

The script engine is being refactored to enable DI support as well as enable per-repo scripts support (#10866). #11242 provides the foundation for that work.
Please have a look.

  • Add Run Scripts option to diff file list, file tree, and commit form file lists

This is a "thin ice" kind of territory. This is what #6135 attempted to do.
The issue here is the script can only be executed for an existing file - which works in the context of the Commit form. However, in the context of diff file list and the file tree - it is not always the case. Most of the time those show committed files - i.e., a fixed snapshot in time.

So, the menu must either

  • be shown for the current working folder (and the artificial commits), or
  • the script has to extract the selected files to a temp location before being executed.

@SlugFiller
Copy link
Contributor Author

I wish you've indicated your desire to work on this so that we could discuss the approach and the implementation.

The time between me posting this PR and you responding is longer than the time I spent working on it. So even if I posted about it the second I started working, it wouldn't make much of a difference.

Also, I'm not sure what's the recommended forum for "discussing the approach and the implementation"

Please have a look.

If that PR is a priority, and is going to be merged first (It's much newer and larger, so hard to tell), I can pre-emptively rebase on top of it. Of course, it's possible the merge order would be opposite.

Feature-wise, I believe they are orthogonal, and I'm not even sure how much they collide code-wise.

If there's one detail I would add to a refactor, is allowing a script to be set to multiple events, so that Show in RevisionGrid can be converted from a separate boolean to an event. This would also allow splitting ShowInFileList to per-list events.

The issue here is the script can only be executed for an existing file

This is not universally true. For example, Notepad++ would create the file if it doesn't exist. Although, that might not be what the user intends. However, the exact same issue exists for the Edit working directory file option. And also when dragging and dropping a file from the list to another program. So it's not unprecedented/unacceptable behavior.

the script has to extract the selected files to a temp location before being executed

This could be added as an extra option, e.g. {fcSelectedFiles}. This is what TortoiseHG does on drag and drop. But I can definitely say, from direct experience, this is not what a user expects to happen by default.

This is especially the case when you consider neither the Working directory nor Commit index have a File tree view. Navigating through the File tree of the last commit, and doing operations on the files presented there, as they exist in the working directory, is a common flow.

Ergo, it should be a separate option, which can be added in a separate PR.

This is what #6135 attempted to do.

From what I can tell, the primary issue with that one is the same as with #11183 : It implements one specific operation, instead of allowing arbitrary ones. Ergo, this PR is a replacement for both.

@gerhardol
Copy link
Member

Also, I'm not sure what's the recommended forum for "discussing the approach and the implementation"

(One of) the existing issues.

The issue here is the script can only be executed for an existing file

This is not universally true. For example, Notepad++ would create the file if it doesn't exist. Although, that might not be what the user intends. However, the exact same issue exists for the Edit working directory file option. And also when dragging and dropping a file from the list to another program. So it's not unprecedented/unacceptable behavior.

The existing behavior is an existing problem.
I expect the current file to be changed though, but there have been reports (including drag&drop).
Some actions like delete is limited to WorkTree, which occasionally is a limitation.
A File tree with WorkTree would be useful (but that is not opposing current file changes as in this PR).

Even if we add script options by default I believe this is a good change. The first change (that could be added by default) is to open the file in VS Code.

Have not looked at it, it is slightly bigger than what can be done quickly.

@mstv
Copy link
Member

mstv commented Oct 6, 2023

I think the new file options do not need the prefix "f". "c" stands for the current / checked-out commit in contrast to "s" for the selected commit.


If that PR is a priority, and is going to be merged first (It's much newer and larger, so hard to tell), I can pre-emptively rebase on top of it. Of course, it's possible the merge order would be opposite.

The architect's PRs come first. 😉 #10866 is older anyway. Though that draft has a few open points yet. But the DI refactoring should be straight forward.

Feature-wise, I believe they are orthogonal, and I'm not even sure how much they collide code-wise.

I think so, too.

If there's one detail I would add to a refactor, is allowing a script to be set to multiple events, so that Show in RevisionGrid can be converted from a separate boolean to an event. This would also allow splitting ShowInFileList to per-list events.

"Show in RevisionGrid" just means "Do not 'hide' the item in the 'Run script' submenu". This option could be renamed accordingly.

The issue here is the script can only be executed for an existing file

...

I agree to ignore this.

@SlugFiller
Copy link
Contributor Author

I think the new file options do not need the prefix "f". "c" stands for the current / checked-out commit in contrast to "s" for the selected commit.

And "f" stands for files list. More importantly, the prefix is used, in all 3 cases, to decide whether to query the passed parameter for the relevant detail. For instance, if an option starting with "s" is present in a script, it will query to see which revision is currently selected, and throw an error if none are.

"f" initially behaved the same, but the error was incredibly user-unfriendly (it looked like the program crashed), and there was no clean way to suppress the error if the use of the option was surrounded by a matching {if:. Still, if an option starting with "f" is not present (nor the if form thereof), then a query for the currently selected files is avoided.

"Show in RevisionGrid" just means "Do not 'hide' the item in the 'Run script' submenu". This option could be renamed accordingly.

And ShowInFileList is implemented the same. But as an event. My investigation of the setting's history suggested that this is tech debt: GitUI.Script.ScriptInfo.AddToRevisionGridContextMenu was created along with the first scripts prototype, and GitUI.Script.ScriptInfo.OnEvent was added a couple of months afterwards. Later, when ShowInUserMenuBar was added, it was added as a ScriptEvent, not a new boolean member of GitUI.Script.ScriptInfo.

And IMO, this is the correct way to do it. It doesn't make sense to add an extra boolean for every place an option could appear (or made more prominent than a menu). And C# has HashSet/ISet. WinForm ComboBoxes don't have multiple selection, but that can be worked around. GitUI.Script.ScriptInfo.OnEvent should be an ISet<GitUI.Script.ScriptEvent>, not a GitUI.Script.ScriptEvent. That's just my opinion, though.

@mstv
Copy link
Member

mstv commented Oct 6, 2023

The prefix "f" does not have a meaning for the user. There will be no "gSelectedFiles". So it should be omitted.

It doesn't make sense to add an extra boolean for every place an option could appear

I have not suggested anything like this.
Up to now, all scripts shall appear in the context menu of the RevisionGrid. AddToRevisionGridContextMenu just controls whether they are added as top level item or in the submenu.
I agree ScriptEvent.ShowInFileList scripts should not be shown in the RevisionGrid context menu.
But please do not filter for AddToRevisionGridContextMenu! Rather rename this flag.

@SlugFiller
Copy link
Contributor Author

The prefix "f" does not have a meaning for the user. There will be no "gSelectedFiles". So it should be omitted.

There might be "fs", if there's popular request for running scripts on a read-only temporary copy of a file from a specific revision.

But please do not filter for AddToRevisionGridContextMenu! Rather rename this flag.

I did not understand what you are trying to say, here. Rename it to what, why, in what situation, and what would it accomplish?

For comparison, if multiple events are allowed for a script, it would be possible to split ScriptEvent.ShowInFileList to ScriptEvent.ShowInDiffFileList, ScriptEvent.ShowInFileTree, ScriptEvent.ShowInStagedFiles and ScriptEvent.ShowInUnstagedFiles. Then you could have a script that applies to all 4, but also a script that applies to only one.

This would control whether said script appears in the top level of the context menu for the specific list, or is hidden inside the Run Script sub-menu.

It would also be possible to add something like ScriptEvent.HideInRevisionGrid, so it does not appear even in Run Script sub-menu of the revision list. (Although I'm not sure if the limitation can be applied to hotkeys as well)

@mstv
Copy link
Member

mstv commented Oct 6, 2023

The prefix "f" does not have a meaning for the user. There will be no "gSelectedFiles". So it should be omitted.

There might be "fs", if there's popular request for running scripts on a read-only temporary copy of a file from a specific revision.

I do not expect that this will be implemented.
If, then rather "w" for "file in workdir" and "t" or "s" for "temp copy of selected file of selected commit".
And no prefix for "{LineNumber}" in neither case.

"Show in RevisionGrid" just means "Do not 'hide' the item in the 'Run script' submenu".
But please do not filter for AddToRevisionGridContextMenu! Rather rename this flag.

I did not understand what you are trying to say, here. Rename it to what, why, in what situation, and what would it accomplish?

refer to the XMLdoc and the usage of AddToRevisionGridContextMenu
i.e. rename it to ShowInRootOfContextMenu or the like

@RussKie
Copy link
Member

RussKie commented Oct 7, 2023

@SlugFiller I consider #11242 complete to the state where you could consider rebasing on top of it.

RE: prefixes - I don't like the current prefix system and do not see why we need to shorten those. All these "s", "c", "f", "w", etc. feel like nonsense. These are a part of our public API surface, and in my view, the readability of these should be a paramount.

"Show in RevisionGrid" just means "Do not 'hide' the item in the 'Run script' submenu". This option could be renamed accordingly.

And ShowInFileList is implemented the same. But as an event. My investigation of the setting's history suggested that this is tech debt: GitUI.Script.ScriptInfo.AddToRevisionGridContextMenu was created along with the first scripts prototype, and GitUI.Script.ScriptInfo.OnEvent was added a couple of months afterwards. Later, when ShowInUserMenuBar was added, it was added as a ScriptEvent, not a new boolean member of GitUI.Script.ScriptInfo.

A lot of original implementations were likely added in ad-hoc manner, which resulted in... certain inconsistencies. It's my view that we're in a position to re-imagine and re-implement some of these (though we should be consider the upgrade experience).

@mstv mstv added this to the vNext milestone Oct 7, 2023
@SlugFiller
Copy link
Contributor Author

I tried rebasing, but hit a snag: In the current master, whenever I open the settings window, I get an exception here:

throw new InvalidOperationException("IGitUICommands should have been assigned");

I can push the rebased version anyway, but I feel iffy about pushing a version that I can only test partially.

@SlugFiller
Copy link
Contributor Author

Pushed anyway, because dotnet test passed, and it's better than a version with conflicts. Changes:

  • Rebased
  • Removed f prefix
  • s and c prefix options will only generate an error if a revision is not selected and they are not discarded by an {if: or {ifnot: option.
  • File options will now generate an error if a file is not selected, and they were not discarded by an {if: or {ifnot: option.

@SlugFiller
Copy link
Contributor Author

Rebased

Copy link
Member

@RussKie RussKie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass.

GitUI/CommandsDialogs/FormCommit.cs Outdated Show resolved Hide resolved
@@ -306,13 +307,14 @@ private async Task SetDiffsAsync(IReadOnlyList<GitRevision> revisions)
}
}

public void Bind(IRevisionGridInfo revisionGridInfo, IRevisionGridUpdate revisionGridUpdate, RevisionFileTreeControl revisionFileTree, Func<string>? pathFilter, Action? refreshGitStatus)
public void Bind(IRevisionGridInfo revisionGridInfo, IRevisionGridUpdate revisionGridUpdate, RevisionFileTreeControl revisionFileTree, Func<string>? pathFilter, Action? refreshGitStatus, IRunScript scriptRunner)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. IRunScript shouldn't be used, it will be removed in the future. Scripts should be invoked via IScriptsRunner interface
  2. IScriptsRunner interface must be resolved from the service provider, which is UICommands.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of IScriptsRunner is to run the script, and it is a replacement of ScriptRunner. The purpose of IRunScript is to refresh the UI after a command has completed, a purpose which is not (and should not be) served by IScriptsRunner. Case in point: RevisionGridControl still extends IRunScript. There is, as of yet, no replacement for it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case in point: RevisionGridControl still extends IRunScript. There is, as of yet, no replacement for it.

This is correct, and this is the fundamental task that needs to be solved first - work out how to support and execute scripts outside the RevisionGrid.

Currently, the scripts are assumed to be run in the context of the RevisionGrid. Hence, it's expected to be available.
We have IScriptHostControl abstraction (introduced in #7950) which purpose is to extend the script support to other controls - e.g., it allows executing user scripts in the left panel. The same approach should work for the FileTree and the RevisionDiff controls as well. The interface can/should be extended as necessary.

I think this method's signature must change taking IScriptHostControl instead of RevisionGridControl:

public static CommandStatus RunScript<THostForm>(ScriptInfo script, THostForm form, RevisionGridControl? revisionGrid)
where THostForm : IGitModuleForm, IWin32Window

Consequently, each control providing the script menu would implement IScriptHostControl and then pass itself to the script runner.

Here's a very old prototype of mine (that served as a foundation for #7950) which attempted to add the script support to RevisionFileTreeControl.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IScriptHostControl is currently a bit different, because it's used to get the current revision details. Something that, incidentally, would not be possible in the commit window. More importantly, since it doesn't wrap script running, the only way it could handle a UI refresh, is if it implemented a refresh callback, and it would become the duty of GitUI.ScriptsEngine.ScriptsManager.ScriptRunner.RunScript to call it.

This gives me an idea, though. It's possible to unify IScriptHostControl, IRunScript.IFileListSource, and IRunScript into a single interface IScriptHostControl that would handle all of the above. It would simply return null or be a no-op in cases where it's not relevant (e.g. getting the file list when running a script on the revision grid).

Then, methods like RevisionDiffControl.Bind can take n IScriptHostControlinstead ofIRunScript`, and forward any methods it doesn't handle.

I can do it, but I'm not sure what your opinion is regarding a PR that changes a bunch of things at once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. I do want scripts in the sidebar too. But it has to be done correctly, and would be material for a separate PR. I have a script that runs git branch -rd, and currently, since it can only be run in the revision grid, it requires me to pick a remote branch from a tiny dialog, if I have multiple remote branches on the same revision. Being able to run it directly on a remote branch instead would be pretty great. But it would require modifying IScriptHostControl to be able to return a specific branch, instead of a revision.

GitUI/ScriptsEngine/IRunScript.cs Outdated Show resolved Hide resolved
@@ -15,6 +15,7 @@ public enum ScriptEvent
BeforeMerge,
AfterMerge,
BeforeFetch,
AfterFetch
AfterFetch,
ShowInFileList
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enum contains "git events". What does "ShowInFileList" mean in this context?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShowInUserMenuBar is also an event. This enum is called ScriptEvent, not GitEvent. It represents situations in which a script should appear/be executed.

GitUI/ScriptsEngine/IRunScript.cs Outdated Show resolved Hide resolved
GitUI/ScriptsEngine/IScriptsRunner.cs Outdated Show resolved Hide resolved
GitUI/CommandsDialogs/RevisionDiffControl.cs Outdated Show resolved Hide resolved
GitUI/CommandsDialogs/RevisionDiffControl.cs Outdated Show resolved Hide resolved
GitUI/ScriptsEngine/IRunScript.cs Outdated Show resolved Hide resolved
@ghost ghost added the 📭 needs: author feedback More info/confirmation awaited from OP; issues typically get closed after 30 days of inactivity label Oct 12, 2023
@ghost ghost removed the 📭 needs: author feedback More info/confirmation awaited from OP; issues typically get closed after 30 days of inactivity label Oct 12, 2023
Copy link
Member

@gerhardol gerhardol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1
Only speed review, not run
I like the functionality

@RussKie
Copy link
Member

RussKie commented Oct 15, 2023 via email

@SlugFiller
Copy link
Contributor Author

I'll try to make a tiny "Remove IRunScript, replace with IScriptHostControl" PR, and then rebase this PR on top of it.

@SlugFiller
Copy link
Contributor Author

Had to rebase again due to conflict (Minor change in an deleted line)

Copy link
Member

@mstv mstv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your patience!

@mstv mstv requested a review from RussKie December 11, 2023 10:46
@pmiossec
Copy link
Member

pmiossec commented Jan 1, 2024

Working good.
The only thing surprising me (sorry if it has already been discussed earlier) is that, because we are acting on at least one file, I would have expected that the "ShowInFileList" event only show the script where you could select file(s).

I understand that the consequence of this option is in fact to display the script directly in the menu and not in the "Run script" sub-menu but it is a bit counterintuitive for me.

Should we not have a "ShowAtRootInFileList" and "ShowInFileList" that displays the script only where file(s) could be selected and with the 1st one keeping the current position in menu and the second displaying it in the "Run script" sub-menu (for less frequent usages)?

Because, whatever option we select, it is available in the revision grid where no file(s) could be selected and the script is failing.
Or is it overkill, we change nothing, and we let the user be "smart" (even if it happens that some scripts are not use that often and it is sometimes difficult to remember the context where each one could be used)?

@SlugFiller
Copy link
Contributor Author

Show In RevisionGrid and ShowInUserMenuBar operate largely the same way, i.e. they do not remove the script from other places where it might appear. And script being unable to run is equally likely due to use of parameters like revision or remote branch name where it is unavailable. This is why I'm working on a separate {if:parameter} feature. Running an editor might still be useful even with no files selected, so long as it doesn't try sending parameters that don't exist. Although I've put work on it on pause until I see this one getting merged, as they're not sufficiently orthogonal.

@SlugFiller
Copy link
Contributor Author

So, like, what's the status of this? I haven't received any updates. What is the merge lifecycle like in this repo?

@vbjay
Copy link
Contributor

vbjay commented Jan 7, 2024

Screenshot_20240107-080117_GitHub.jpg

Note current status of pr.

@SlugFiller
Copy link
Contributor Author

I've made all requested changes, though, and even got an approval above. I'm not sure what else to do.

@mstv
Copy link
Member

mstv commented Jan 7, 2024

just wait for @RussKie to approve, his request has been addressed

@mstv
Copy link
Member

mstv commented Jan 7, 2024

Reviews from others are welcome, too - as always.

If it affects the architecture, we want to get @RussKie's advice. Though he is pretty busy in RL.

@mstv mstv modified the milestones: vNext, 4.3 Jan 27, 2024
@mstv
Copy link
Member

mstv commented Feb 18, 2024

friendly nudge, @RussKie

@RussKie
Copy link
Member

RussKie commented Feb 18, 2024 via email

@mstv
Copy link
Member

mstv commented Apr 7, 2024

I am going to squash-merge this PR next weekend.

@SlugFiller
Copy link
Contributor Author

Cool. Finally. This has been slowing down my workflow for so long.

@mstv mstv merged commit 53e31e0 into gitextensions:master Apr 14, 2024
3 of 4 checks passed
@RussKie RussKie modified the milestones: 4.3, vNext May 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add script actions to file tree menu Add alter texteditor
6 participants