From 1b8bb93c7121cd291a17303edfdd313c8edb9f88 Mon Sep 17 00:00:00 2001 From: Fernsehkind Date: Thu, 27 Dec 2018 14:51:44 +0100 Subject: [PATCH] Fix #5999: Add option to qoute arguments in script parser - Use {option} for old/existing way - Use {{option}} for quoted arguments. When replaced options ends with backslash, this backslash is escaped. --- .../Pages/ScriptsSettingsPage.cs | 5 +- GitUI/Script/ScriptOptionsParser.cs | 344 +++++++++++------- .../Script/ScriptOptionsParserTests.cs | 14 +- 3 files changed, 221 insertions(+), 142 deletions(-) diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs index 7abbd744b1e..4bce4a25b18 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/ScriptsSettingsPage.cs @@ -17,7 +17,10 @@ public partial class ScriptsSettingsPage : SettingsPageWithHeader #region translation private readonly TranslationString _scriptSettingsPageHelpDisplayArgumentsHelp = new TranslationString("Arguments help"); - private readonly TranslationString _scriptSettingsPageHelpDisplayContent = new TranslationString(@"User Input: + private readonly TranslationString _scriptSettingsPageHelpDisplayContent = new TranslationString(@"Use {option} for normal replacement. +Use {{option}} for qouted replacement. + +User Input: {UserInput} {UserFiles} diff --git a/GitUI/Script/ScriptOptionsParser.cs b/GitUI/Script/ScriptOptionsParser.cs index 62db17fea27..33182c4a44f 100644 --- a/GitUI/Script/ScriptOptionsParser.cs +++ b/GitUI/Script/ScriptOptionsParser.cs @@ -14,38 +14,50 @@ public sealed class ScriptOptionsParser { public static readonly IReadOnlyList Options = new[] { - "{sHashes}", - "{sTag}", - "{sBranch}", - "{sLocalBranch}", - "{sRemoteBranch}", - "{sRemote}", - "{sRemoteUrl}", - "{sRemotePathFromUrl}", - "{sHash}", - "{sMessage}", - "{sAuthor}", - "{sCommitter}", - "{sAuthorDate}", - "{sCommitDate}", - "{cTag}", - "{cBranch}", - "{cLocalBranch}", - "{cRemoteBranch}", - "{cHash}", - "{cMessage}", - "{cAuthor}", - "{cCommitter}", - "{cAuthorDate}", - "{cCommitDate}", - "{cDefaultRemote}", - "{cDefaultRemoteUrl}", - "{cDefaultRemotePathFromUrl}", - "{UserInput}", - "{UserFiles}", - "{WorkingDir}" + "sHashes", + "sTag", + "sBranch", + "sLocalBranch", + "sRemoteBranch", + "sRemote", + "sRemoteUrl", + "sRemotePathFromUrl", + "sHash", + "sMessage", + "sAuthor", + "sCommitter", + "sAuthorDate", + "sCommitDate", + "cTag", + "cBranch", + "cLocalBranch", + "cRemoteBranch", + "cHash", + "cMessage", + "cAuthor", + "cCommitter", + "cAuthorDate", + "cCommitDate", + "cDefaultRemote", + "cDefaultRemoteUrl", + "cDefaultRemotePathFromUrl", + "UserInput", + "UserFiles", + "WorkingDir" }; + private static string CreateOption(string option, bool quoted) + { + var result = "{" + option + "}"; + + if (quoted) + { + result = "{" + result + "}"; + } + + return result; + } + public static (string argument, bool abort) Parse(string argument, IGitModule module, IWin32Window owner, RevisionGridControl revisionGrid) { GitRevision selectedRevision = null; @@ -65,12 +77,20 @@ public static (string argument, bool abort) Parse(string argument, IGitModule mo foreach (string option in Options) { - if (string.IsNullOrEmpty(argument) || !argument.Contains(option)) + if (string.IsNullOrEmpty(argument)) { continue; } - if (option.StartsWith("{c") && currentRevision == null) + string regularOption = CreateOption(option, false); + string quotedOption = CreateOption(option, true); + + if (!argument.Contains(regularOption) && (!argument.Contains(quotedOption))) + { + continue; + } + + if (option.StartsWith("c") && currentRevision == null) { currentRevision = GetCurrentRevision(module, revisionGrid, currentTags, currentLocalBranches, currentRemoteBranches, currentBranches, currentRevision); @@ -88,7 +108,7 @@ public static (string argument, bool abort) Parse(string argument, IGitModule mo } } } - else if (option.StartsWith("{s") && selectedRevision == null && revisionGrid != null) + else if (option.StartsWith("s") && selectedRevision == null && revisionGrid != null) { allSelectedRevisions = revisionGrid.GetSelectedRevisions(); selectedRevision = CalculateSelectedRevision(revisionGrid, selectedRemoteBranches, selectedRemotes, selectedLocalBranches, selectedBranches, selectedTags); @@ -215,251 +235,281 @@ private static string GetRemotePath(string url) private static string ParseScriptArguments(string argument, string option, IWin32Window owner, RevisionGridControl revisionGrid, IGitModule module, IReadOnlyList allSelectedRevisions, in IList selectedTags, in IList selectedBranches, in IList selectedLocalBranches, in IList selectedRemoteBranches, in IList selectedRemotes, GitRevision selectedRevision, in IList currentTags, in IList currentBranches, in IList currentLocalBranches, in IList currentRemoteBranches, GitRevision currentRevision, string currentRemote) { + string newString = null; string remote; string url; switch (option) { - case "{sHashes}": - argument = argument.Replace(option, - string.Join(" ", allSelectedRevisions.Select(revision => revision.Guid).ToArray())); + case "sHashes": + newString = string.Join(" ", allSelectedRevisions.Select(revision => revision.Guid).ToArray()); break; - case "{sTag}": + case "sTag": if (selectedTags.Count == 1) { - argument = argument.Replace(option, selectedTags[0].Name); + newString = selectedTags[0].Name; } else if (selectedTags.Count != 0) { - argument = argument.Replace(option, AskToSpecify(selectedTags, revisionGrid)); + newString = AskToSpecify(selectedTags, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{sBranch}": + + case "sBranch": if (selectedBranches.Count == 1) { - argument = argument.Replace(option, selectedBranches[0].Name); + newString = selectedBranches[0].Name; } else if (selectedBranches.Count != 0) { - argument = argument.Replace(option, - AskToSpecify(selectedBranches, revisionGrid)); + newString = AskToSpecify(selectedBranches, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{sLocalBranch}": + + case "sLocalBranch": if (selectedLocalBranches.Count == 1) { - argument = argument.Replace(option, selectedLocalBranches[0].Name); + newString = selectedLocalBranches[0].Name; } else if (selectedLocalBranches.Count != 0) { - argument = argument.Replace(option, AskToSpecify(selectedLocalBranches, revisionGrid)); + newString = AskToSpecify(selectedLocalBranches, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{sRemoteBranch}": + + case "sRemoteBranch": if (selectedRemoteBranches.Count == 1) { - argument = argument.Replace(option, selectedRemoteBranches[0].Name); + newString = selectedRemoteBranches[0].Name; } else if (selectedRemoteBranches.Count != 0) { - argument = argument.Replace(option, AskToSpecify(selectedRemoteBranches, revisionGrid)); + newString = AskToSpecify(selectedRemoteBranches, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{sRemote}": + + case "sRemote": if (selectedRemotes.Count == 0) { - argument = argument.Replace(option, ""); - break; + newString = ""; + } + else + { + newString = selectedRemotes.Count == 1 + ? selectedRemotes[0] + : AskToSpecify(selectedRemotes, revisionGrid); } - remote = selectedRemotes.Count == 1 - ? selectedRemotes[0] - : AskToSpecify(selectedRemotes, revisionGrid); - - argument = argument.Replace(option, remote); break; - case "{sRemoteUrl}": + + case "sRemoteUrl": if (selectedRemotes.Count == 0) { - argument = argument.Replace(option, ""); - break; + newString = ""; + } + else + { + remote = selectedRemotes.Count == 1 + ? selectedRemotes[0] + : AskToSpecify(selectedRemotes, revisionGrid); + newString = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, remote)); } - remote = selectedRemotes.Count == 1 - ? selectedRemotes[0] - : AskToSpecify(selectedRemotes, revisionGrid); - - url = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, remote)); - argument = argument.Replace(option, url); break; - case "{sRemotePathFromUrl}": + + case "sRemotePathFromUrl": if (selectedRemotes.Count == 0) { - argument = argument.Replace(option, ""); - break; + newString = ""; + } + else + { + remote = selectedRemotes.Count == 1 + ? selectedRemotes[0] + : AskToSpecify(selectedRemotes, revisionGrid); + newString = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, remote)); } - remote = selectedRemotes.Count == 1 - ? selectedRemotes[0] - : AskToSpecify(selectedRemotes, revisionGrid); - - url = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, remote)); - argument = argument.Replace(option, GetRemotePath(url)); break; - case "{sHash}": - argument = argument.Replace(option, selectedRevision.Guid); + + case "sHash": + newString = selectedRevision.Guid; break; - case "{sMessage}": - argument = argument.Replace(option, selectedRevision.Subject); + + case "sMessage": + newString = selectedRevision.Subject; break; - case "{sAuthor}": - argument = argument.Replace(option, selectedRevision.Author); + + case "sAuthor": + newString = selectedRevision.Author; break; - case "{sCommitter}": - argument = argument.Replace(option, selectedRevision.Committer); + + case "sCommitter": + newString = selectedRevision.Committer; break; - case "{sAuthorDate}": - argument = argument.Replace(option, selectedRevision.AuthorDate.ToString()); + + case "sAuthorDate": + newString = selectedRevision.AuthorDate.ToString(); break; - case "{sCommitDate}": - argument = argument.Replace(option, selectedRevision.CommitDate.ToString()); + + case "sCommitDate": + newString = selectedRevision.CommitDate.ToString(); break; - case "{cTag}": + + case "cTag": if (currentTags.Count == 1) { - argument = argument.Replace(option, currentTags[0].Name); + newString = currentTags[0].Name; } else if (currentTags.Count != 0) { - argument = argument.Replace(option, AskToSpecify(currentTags, revisionGrid)); + newString = AskToSpecify(currentTags, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{cBranch}": + + case "cBranch": if (currentBranches.Count == 1) { - argument = argument.Replace(option, currentBranches[0].Name); + newString = currentBranches[0].Name; } else if (currentBranches.Count != 0) { - argument = argument.Replace(option, AskToSpecify(currentBranches, revisionGrid)); + newString = AskToSpecify(currentBranches, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{cLocalBranch}": + + case "cLocalBranch": if (currentLocalBranches.Count == 1) { - argument = argument.Replace(option, currentLocalBranches[0].Name); + newString = currentLocalBranches[0].Name; } else if (currentLocalBranches.Count != 0) { - argument = argument.Replace(option, AskToSpecify(currentLocalBranches, revisionGrid)); + newString = AskToSpecify(currentLocalBranches, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{cRemoteBranch}": + + case "cRemoteBranch": if (currentRemoteBranches.Count == 1) { - argument = argument.Replace(option, currentRemoteBranches[0].Name); + newString = currentRemoteBranches[0].Name; } else if (currentRemoteBranches.Count != 0) { - argument = argument.Replace(option, AskToSpecify(currentRemoteBranches, revisionGrid)); + newString = AskToSpecify(currentRemoteBranches, revisionGrid); } else { - argument = argument.Replace(option, ""); + newString = ""; } break; - case "{cHash}": - argument = argument.Replace(option, currentRevision.Guid); + case "cHash": + newString = currentRevision.Guid; break; - case "{cMessage}": - argument = argument.Replace(option, currentRevision.Subject); + + case "cMessage": + newString = currentRevision.Subject; break; - case "{cAuthor}": - argument = argument.Replace(option, currentRevision.Author); + + case "cAuthor": + newString = currentRevision.Author; break; - case "{cCommitter}": - argument = argument.Replace(option, currentRevision.Committer); + + case "cCommitter": + newString = currentRevision.Committer; break; - case "{cAuthorDate}": - argument = argument.Replace(option, currentRevision.AuthorDate.ToString()); + + case "cAuthorDate": + newString = currentRevision.AuthorDate.ToString(); break; - case "{cCommitDate}": - argument = argument.Replace(option, currentRevision.CommitDate.ToString()); + + case "cCommitDate": + newString = currentRevision.CommitDate.ToString(); break; - case "{cDefaultRemote}": + + case "cDefaultRemote": if (string.IsNullOrEmpty(currentRemote)) { - argument = argument.Replace(option, ""); - break; + newString = ""; + } + else + { + newString = currentRemote; } - argument = argument.Replace(option, currentRemote); break; - case "{cDefaultRemoteUrl}": + + case "cDefaultRemoteUrl": if (string.IsNullOrEmpty(currentRemote)) { - argument = argument.Replace(option, ""); - break; + newString = ""; + } + else + { + newString = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, currentRemote)); } - url = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, currentRemote)); - argument = argument.Replace(option, url); break; - case "{cDefaultRemotePathFromUrl}": + + case "cDefaultRemotePathFromUrl": if (string.IsNullOrEmpty(currentRemote)) { - argument = argument.Replace(option, ""); - break; + newString = ""; + } + else + { + url = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, currentRemote)); + newString = GetRemotePath(url); } - url = module.GetSetting(string.Format(SettingKeyString.RemoteUrl, currentRemote)); - argument = argument.Replace(option, GetRemotePath(url)); break; - case "{UserInput}": + + case "UserInput": using (var prompt = new SimplePrompt()) { prompt.ShowDialog(); - argument = argument.Replace(option, prompt.UserInput); + newString = prompt.UserInput; } break; - case "{UserFiles}": + + case "UserFiles": using (FormFilePrompt prompt = new FormFilePrompt()) { if (prompt.ShowDialog(owner) != DialogResult.OK) @@ -468,15 +518,31 @@ private static string ParseScriptArguments(string argument, string option, IWin3 return null; } - argument = argument.Replace(option, prompt.FileInput); + newString = prompt.FileInput; break; } - case "{WorkingDir}": - argument = argument.Replace(option, module.WorkingDir); + case "WorkingDir": + newString = module.WorkingDir; break; } + if (newString != null) + { + string newStringQuoted; + if (newString.EndsWith("\\")) + { + newStringQuoted = "\"" + newString + "\\\""; + } + else + { + newStringQuoted = "\"" + newString + "\""; + } + + argument = argument.Replace(CreateOption(option, true), newStringQuoted); + argument = argument.Replace(CreateOption(option, false), newString); + } + return argument; } diff --git a/UnitTests/GitUITests/Script/ScriptOptionsParserTests.cs b/UnitTests/GitUITests/Script/ScriptOptionsParserTests.cs index 8b1443477ed..c87b141871d 100644 --- a/UnitTests/GitUITests/Script/ScriptOptionsParserTests.cs +++ b/UnitTests/GitUITests/Script/ScriptOptionsParserTests.cs @@ -21,7 +21,7 @@ public void Setup() [Test] public void ScriptOptionsParser_resolve_cDefaultRemotePathFromUrl_currentRemote_unset() { - var result = ScriptOptionsParser.GetTestAccessor().ParseScriptArguments("{openUrl} https://gitlab.com{cDefaultRemotePathFromUrl}/tree/{sBranch}", "{cDefaultRemotePathFromUrl}", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + var result = ScriptOptionsParser.GetTestAccessor().ParseScriptArguments("{openUrl} https://gitlab.com{cDefaultRemotePathFromUrl}/tree/{sBranch}", "cDefaultRemotePathFromUrl", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); result.Should().Be("{openUrl} https://gitlab.com/tree/{sBranch}"); } @@ -32,9 +32,19 @@ public void ScriptOptionsParser_resolve_cDefaultRemotePathFromUrl_currentRemote_ var currentRemote = "myRemote"; _module.GetSetting(string.Format(SettingKeyString.RemoteUrl, currentRemote)).Returns("https://gitlab.com/gitlabhq/gitlabhq.git"); - var result = ScriptOptionsParser.GetTestAccessor().ParseScriptArguments("{openUrl} https://gitlab.com{cDefaultRemotePathFromUrl}/tree/{sBranch}", "{cDefaultRemotePathFromUrl}", null, null, _module, null, null, null, null, null, null, null, null, null, null, null, null, currentRemote); + var result = ScriptOptionsParser.GetTestAccessor().ParseScriptArguments("{openUrl} https://gitlab.com{cDefaultRemotePathFromUrl}/tree/{sBranch}", "cDefaultRemotePathFromUrl", null, null, _module, null, null, null, null, null, null, null, null, null, null, null, null, currentRemote); result.Should().Be("{openUrl} https://gitlab.com/gitlabhq/gitlabhq/tree/{sBranch}"); } + + [Test] + public void ScriptOptionsParser_resolve_QuotedWithBackslashAtEnd() + { + _module.WorkingDir.Returns("C:\\test path with whitespaces\\"); + + var result = ScriptOptionsParser.GetTestAccessor().ParseScriptArguments("{{WorkingDir}} \"{WorkingDir}\"", "WorkingDir", null, null, _module, null, null, null, null, null, null, null, null, null, null, null, null, null); + + result.Should().Be("\"C:\\test path with whitespaces\\\\\" \"C:\\test path with whitespaces\\\""); + } } }