diff --git a/docs/usage.md b/docs/usage.md index fb0b260f2..567457b4e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -11,11 +11,11 @@ git credential-manager-core [ []] ## Commands -### help / --help +### --help / -h / -? Displays a list of available commands. -### version / --version +### --version Displays the current version. diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/ConfigureCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/ConfigureCommandTests.cs index 35dbbae5a..314f3a2a0 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/ConfigureCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/ConfigureCommandTests.cs @@ -10,24 +10,6 @@ namespace Microsoft.Git.CredentialManager.Tests.Commands { public class ConfigureCommandTests { - [Theory] - [InlineData("configure", true)] - [InlineData("CONFIGURE", true)] - [InlineData("cOnFiGuRe", true)] - [InlineData("get", false)] - [InlineData("store", false)] - [InlineData("unconfigure", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void ConfigureCommand_CanExecuteAsync(string argString, bool expected) - { - var command = new ConfigureCommand(Mock.Of()); - - bool result = command.CanExecute(argString?.Split(null)); - - Assert.Equal(expected, result); - } - [Fact] public async Task ConfigureCommand_ExecuteAsync_User_InvokesConfigurationServiceConfigureUser() { @@ -37,11 +19,9 @@ public async Task ConfigureCommand_ExecuteAsync_User_InvokesConfigurationService .Verifiable(); var context = new TestCommandContext(); + var command = new ConfigureCommand(context, configService.Object); - string[] cmdArgs = {"configure"}; - var command = new ConfigureCommand(configService.Object); - - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(false); configService.Verify(x => x.ConfigureAsync(ConfigurationTarget.User), Times.Once); } @@ -55,11 +35,9 @@ public async Task ConfigureCommand_ExecuteAsync_System_InvokesConfigurationServi .Verifiable(); var context = new TestCommandContext(); + var command = new ConfigureCommand(context, configService.Object); - string[] cmdArgs = {"configure", "--system"}; - var command = new ConfigureCommand(configService.Object); - - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(true); configService.Verify(x => x.ConfigureAsync(ConfigurationTarget.System), Times.Once); } diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs index 951fdabb2..e2ea7e386 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs @@ -11,24 +11,6 @@ namespace Microsoft.Git.CredentialManager.Tests.Commands { public class EraseCommandTests { - [Theory] - [InlineData("erase", true)] - [InlineData("ERASE", true)] - [InlineData("eRaSe", true)] - [InlineData("get", false)] - [InlineData("store", false)] - [InlineData("foobar", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void EraseCommand_CanExecuteAsync(string argString, bool expected) - { - var command = new EraseCommand(Mock.Of()); - - bool result = command.CanExecute(argString?.Split(null)); - - Assert.Equal(expected, result); - } - [Fact] public async Task EraseCommand_ExecuteAsync_CallsHostProvider() { @@ -50,10 +32,9 @@ public async Task EraseCommand_ExecuteAsync_CallsHostProvider() Streams = {In = stdin} }; - string[] cmdArgs = {"erase"}; - var command = new EraseCommand(providerRegistry); + var command = new EraseCommand(context, providerRegistry); - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(); providerMock.Verify( x => x.EraseCredentialAsync(It.Is(y => AreInputArgumentsEquivalent(expectedInput, y))), diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GetCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GetCommandTests.cs index cbe388b68..0f984e76c 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GetCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GetCommandTests.cs @@ -13,31 +13,6 @@ namespace Microsoft.Git.CredentialManager.Tests.Commands { public class GetCommandTests { - [Theory] - [InlineData("get", true)] - [InlineData("GET", true)] - [InlineData("gEt", true)] - [InlineData("erase", false)] - [InlineData("store", false)] - [InlineData("foobar", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void GetCommand_CanExecuteAsync(string argString, bool expected) - { - var command = new GetCommand(Mock.Of()); - - bool result = command.CanExecute(argString?.Split(null)); - - if (expected) - { - Assert.True(result); - } - else - { - Assert.False(result); - } - } - [Fact] public async Task GetCommand_ExecuteAsync_CallsHostProviderAndWritesCredential() { @@ -56,10 +31,9 @@ public async Task GetCommand_ExecuteAsync_CallsHostProviderAndWritesCredential() var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object}; var context = new TestCommandContext(); - string[] cmdArgs = {"get"}; - var command = new GetCommand(providerRegistry); + var command = new GetCommand(context, providerRegistry); - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(); IDictionary actualStdOutDict = ParseDictionary(context.Streams.Out); diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/HostProviderCommandBaseTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GitCommandBaseTests.cs similarity index 69% rename from src/shared/Microsoft.Git.CredentialManager.Tests/Commands/HostProviderCommandBaseTests.cs rename to src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GitCommandBaseTests.cs index b1700a1b7..d078297d8 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/HostProviderCommandBaseTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GitCommandBaseTests.cs @@ -9,10 +9,10 @@ namespace Microsoft.Git.CredentialManager.Tests.Commands { - public class HostProviderCommandBaseTests + public class GitCommandBaseTests { [Fact] - public async Task HostProviderCommandBase_ExecuteAsync_CallsExecuteInternalAsyncWithCorrectArgs() + public async Task GitCommandBase_ExecuteAsync_CallsExecuteInternalAsyncWithCorrectArgs() { var mockContext = new Mock(); var mockStreams = new Mock(); @@ -34,11 +34,10 @@ public async Task HostProviderCommandBase_ExecuteAsync_CallsExecuteInternalAsync mockContext.Setup(x => x.Trace).Returns(Mock.Of()); mockContext.Setup(x => x.Settings).Returns(Mock.Of()); - HostProviderCommandBase testCommand = new TestCommand(mockHostRegistry.Object) + GitCommandBase testCommand = new TestCommand(mockContext.Object, mockHostRegistry.Object) { - VerifyExecuteInternalAsync = (context, input, provider) => + VerifyExecuteInternalAsync = (input, provider) => { - Assert.Same(mockContext.Object, context); Assert.Same(mockProvider.Object, provider); Assert.Equal("test", input.Protocol); Assert.Equal("example.com", input.Host); @@ -46,11 +45,11 @@ public async Task HostProviderCommandBase_ExecuteAsync_CallsExecuteInternalAsync } }; - await testCommand.ExecuteAsync(mockContext.Object, new string[0]); + await testCommand.ExecuteAsync(); } [Fact] - public async Task HostProviderCommandBase_ExecuteAsync_ConfiguresSettingsRemoteUri() + public async Task GitCommandBase_ExecuteAsync_ConfiguresSettingsRemoteUri() { var mockContext = new Mock(); var mockStreams = new Mock(); @@ -73,29 +72,27 @@ public async Task HostProviderCommandBase_ExecuteAsync_ConfiguresSettingsRemoteU mockContext.Setup(x => x.Trace).Returns(Mock.Of()); mockContext.Setup(x => x.Settings).Returns(mockSettings.Object); - HostProviderCommandBase testCommand = new TestCommand(mockHostRegistry.Object); + GitCommandBase testCommand = new TestCommand(mockContext.Object, mockHostRegistry.Object); - await testCommand.ExecuteAsync(mockContext.Object, new string[0]); + await testCommand.ExecuteAsync(); Assert.Equal(remoteUri, mockSettings.Object.RemoteUri); } - private class TestCommand : HostProviderCommandBase + private class TestCommand : GitCommandBase { - public TestCommand(IHostProviderRegistry hostProviderRegistry) - : base(hostProviderRegistry) + public TestCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry) + : base(context, "test", null, hostProviderRegistry) { } - protected override string Name { get; } - - protected override Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider) + protected override Task ExecuteInternalAsync(InputArguments input, IHostProvider provider) { - VerifyExecuteInternalAsync?.Invoke(context, input, provider); + VerifyExecuteInternalAsync?.Invoke(input, provider); return Task.CompletedTask; } - public Action VerifyExecuteInternalAsync { get; set; } + public Action VerifyExecuteInternalAsync { get; set; } } } } diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs index 793bda26f..1bfc42339 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs @@ -10,33 +10,7 @@ namespace Microsoft.Git.CredentialManager.Tests.Commands { public class StoreCommandTests - { - [Theory] - [InlineData("store", true)] - [InlineData("STORE", true)] - [InlineData("sToRe", true)] - [InlineData("get", false)] - [InlineData("erase", false)] - [InlineData("foobar", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void StoreCommand_CanExecuteAsync(string argString, bool expected) - { - var command = new StoreCommand(Mock.Of()); - - bool result = command.CanExecute(argString?.Split(null)); - - if (expected) - { - Assert.True(result); - } - else - { - Assert.False(result); - } - } - - [Fact] + {[Fact] public async Task StoreCommand_ExecuteAsync_CallsHostProvider() { const string testUserName = "john.doe"; @@ -57,10 +31,9 @@ public async Task StoreCommand_ExecuteAsync_CallsHostProvider() Streams = {In = stdin} }; - string[] cmdArgs = {"store"}; - var command = new StoreCommand(providerRegistry); + var command = new StoreCommand(context, providerRegistry); - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(); providerMock.Verify( x => x.StoreCredentialAsync(It.Is(y => AreInputArgumentsEquivalent(expectedInput, y))), diff --git a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/UnconfigureCommandTests.cs b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/UnconfigureCommandTests.cs index 40873ca73..5d09c78a0 100644 --- a/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/UnconfigureCommandTests.cs +++ b/src/shared/Microsoft.Git.CredentialManager.Tests/Commands/UnconfigureCommandTests.cs @@ -10,24 +10,6 @@ namespace Microsoft.Git.CredentialManager.Tests.Commands { public class UnconfigureCommandTests { - [Theory] - [InlineData("unconfigure", true)] - [InlineData("UNCONFIGURE", true)] - [InlineData("uNcOnFiGuRe", true)] - [InlineData("get", false)] - [InlineData("store", false)] - [InlineData("configure", false)] - [InlineData("", false)] - [InlineData(null, false)] - public void UnconfigureCommand_CanExecuteAsync(string argString, bool expected) - { - var command = new UnconfigureCommand(Mock.Of()); - - bool result = command.CanExecute(argString?.Split(null)); - - Assert.Equal(expected, result); - } - [Fact] public async Task UnconfigureCommand_ExecuteAsync_User_InvokesConfigurationServiceUnconfigureUser() { @@ -37,11 +19,9 @@ public async Task UnconfigureCommand_ExecuteAsync_User_InvokesConfigurationServi .Verifiable(); var context = new TestCommandContext(); + var command = new UnconfigureCommand(context, configService.Object); - string[] cmdArgs = {"unconfigure"}; - var command = new UnconfigureCommand(configService.Object); - - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(false); configService.Verify(x => x.UnconfigureAsync(ConfigurationTarget.User), Times.Once); } @@ -55,11 +35,9 @@ public async Task UnconfigureCommand_ExecuteAsync_System_InvokesConfigurationSer .Verifiable(); var context = new TestCommandContext(); + var command = new UnconfigureCommand(context, configService.Object); - string[] cmdArgs = {"unconfigure", "--system"}; - var command = new UnconfigureCommand(configService.Object); - - await command.ExecuteAsync(context, cmdArgs); + await command.ExecuteAsync(true); configService.Verify(x => x.UnconfigureAsync(ConfigurationTarget.System), Times.Once); } diff --git a/src/shared/Microsoft.Git.CredentialManager/Application.cs b/src/shared/Microsoft.Git.CredentialManager/Application.cs index 9b6d54ee2..21ac00b23 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Application.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Application.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; +using System.Collections.Generic; +using System.CommandLine; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -15,6 +17,7 @@ public class Application : ApplicationBase, IConfigurableComponent private readonly string _appPath; private readonly IHostProviderRegistry _providerRegistry; private readonly IConfigurationService _configurationService; + private readonly IList _providerCommands = new List(); public Application(ICommandContext context, string appPath) : this(context, new HostProviderRegistry(context), new ConfigurationService(context), appPath) @@ -47,62 +50,52 @@ public void RegisterProvider(IHostProvider provider, HostProviderPriority priori { _configurationService.AddComponent(configurableProvider); } + + // If the provider has custom commands to offer then create them here + if (provider is ICommandProvider cmdProvider) + { + ProviderCommand providerCommand = cmdProvider.CreateCommand(); + _providerCommands.Add(providerCommand); + } } protected override async Task RunInternalAsync(string[] args) { - string appName = Path.GetFileNameWithoutExtension(_appPath); + var rootCommand = new RootCommand(); - // Construct all supported commands - var commands = new CommandBase[] + // Add standard commands + rootCommand.AddCommand(new GetCommand(Context, _providerRegistry)); + rootCommand.AddCommand(new StoreCommand(Context, _providerRegistry)); + rootCommand.AddCommand(new EraseCommand(Context, _providerRegistry)); + rootCommand.AddCommand(new ConfigureCommand(Context, _configurationService)); + rootCommand.AddCommand(new UnconfigureCommand(Context, _configurationService)); + + // Add any custom provider commands + foreach (ProviderCommand providerCommand in _providerCommands) { - new GetCommand(_providerRegistry), - new StoreCommand(_providerRegistry), - new EraseCommand(_providerRegistry), - new ConfigureCommand(_configurationService), - new UnconfigureCommand(_configurationService), - new VersionCommand(), - new HelpCommand(appName), - }; + rootCommand.AddCommand(providerCommand); + } // Trace the current version and program arguments Context.Trace.WriteLine($"{Constants.GetProgramHeader()} '{string.Join(" ", args)}'"); - if (args.Length == 0) + try { - Context.Streams.Error.WriteLine("Missing command."); - HelpCommand.PrintUsage(Context.Streams.Error, appName); - return -1; + return await rootCommand.InvokeAsync(args); } - - foreach (var cmd in commands) + catch (Exception e) { - if (cmd.CanExecute(args)) + if (e is AggregateException ae) { - try - { - await cmd.ExecuteAsync(Context, args); - return 0; - } - catch (Exception e) - { - if (e is AggregateException ae) - { - ae.Handle(WriteException); - } - else - { - WriteException(e); - } - - return -1; - } + ae.Handle(WriteException); + } + else + { + WriteException(e); } - } - Context.Streams.Error.WriteLine("Unrecognized command '{0}'.", args[0]); - HelpCommand.PrintUsage(Context.Streams.Error, appName); - return -1; + return -1; + } } protected override void Dispose(bool disposing) diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/Command.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/Command.cs deleted file mode 100644 index 4f08adc1f..000000000 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/Command.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.Git.CredentialManager.Commands -{ - /// - /// Represents a Git Credential Manager command. - /// - public abstract class CommandBase - { - /// - /// Check if this command should be executed for the given arguments. - /// - /// Application command-line arguments. - /// True if the command should be executed, false otherwise. - public abstract bool CanExecute(string[] args); - - /// - /// Execute the command. - /// - /// The current command execution context. - /// Application command-line arguments. - /// Awaitable task for the command execution. - public abstract Task ExecuteAsync(ICommandContext context, string[] args); - } - - /// - /// Represents a simple Git Credential Manager command that takes a single named verb. - /// - public abstract class VerbCommandBase : CommandBase - { - /// - /// Name of the command verb. - /// - protected abstract string Name { get; } - - public override bool CanExecute(string[] args) - { - return args != null && args.Length != 0 && StringComparer.OrdinalIgnoreCase.Equals(args[0], Name); - } - } - - /// - /// Represents a command which selects a from a - /// based on the from standard input, and interacts with a . - /// - public abstract class HostProviderCommandBase : VerbCommandBase - { - private readonly IHostProviderRegistry _hostProviderRegistry; - - protected HostProviderCommandBase(IHostProviderRegistry hostProviderRegistry) - { - EnsureArgument.NotNull(hostProviderRegistry, nameof(hostProviderRegistry)); - - _hostProviderRegistry = hostProviderRegistry; - } - - public override async Task ExecuteAsync(ICommandContext context, string[] args) - { - context.Trace.WriteLine($"Start '{Name}' command..."); - - // Parse standard input arguments - // git-credential treats the keys as case-sensitive; so should we. - IDictionary inputDict = await context.Streams.In.ReadDictionaryAsync(StringComparer.Ordinal); - var input = new InputArguments(inputDict); - - // Set the remote URI to scope settings to throughout the process from now on - context.Settings.RemoteUri = input.GetRemoteUri(); - - // Determine the host provider - context.Trace.WriteLine("Detecting host provider for input:"); - context.Trace.WriteDictionarySecrets(inputDict, new []{ "password" }, StringComparer.OrdinalIgnoreCase); - IHostProvider provider = await _hostProviderRegistry.GetProviderAsync(input); - context.Trace.WriteLine($"Host provider '{provider.Name}' was selected."); - - await ExecuteInternalAsync(context, input, provider); - - context.Trace.WriteLine($"End '{Name}' command..."); - } - - /// - /// Execute the command using the given and . - /// - /// The current command execution context. - /// Input arguments of the current Git credential query. - /// Host provider for the current . - /// Awaitable task for the command execution. - protected abstract Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider); - } -} diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/ConfigurationCommands.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/ConfigurationCommands.cs index 4d55d483b..641f13dea 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/ConfigurationCommands.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/ConfigurationCommands.cs @@ -1,25 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; -using System.Linq; +using System.CommandLine; +using System.CommandLine.Invocation; using System.Threading.Tasks; namespace Microsoft.Git.CredentialManager.Commands { - public abstract class ConfigurationCommandBase : VerbCommandBase + public abstract class ConfigurationCommandBase : Command { - protected ConfigurationCommandBase(IConfigurationService configurationService) + protected ConfigurationCommandBase(ICommandContext context, string name, string description, IConfigurationService configurationService) + : base(name, description) { + EnsureArgument.NotNull(context, nameof(context)); EnsureArgument.NotNull(configurationService, nameof(configurationService)); + Context = context; ConfigurationService = configurationService; + + AddOption(new Option("--system", "Modify the system-wide Git configuration instead of the current user")); + + Handler = CommandHandler.Create(ExecuteAsync); } + protected ICommandContext Context { get; } + protected IConfigurationService ConfigurationService { get; } - public override Task ExecuteAsync(ICommandContext context, string[] args) + internal Task ExecuteAsync(bool system) { - var target = args.Any(x => StringComparer.OrdinalIgnoreCase.Equals("--system", x)) + var target = system ? ConfigurationTarget.System : ConfigurationTarget.User; @@ -31,10 +40,8 @@ public override Task ExecuteAsync(ICommandContext context, string[] args) public class ConfigureCommand : ConfigurationCommandBase { - public ConfigureCommand(IConfigurationService configurationService) - : base(configurationService) { } - - protected override string Name => "configure"; + public ConfigureCommand(ICommandContext context, IConfigurationService configurationService) + : base(context, "configure", "Configure Git Credential Manager as the Git credential helper", configurationService) { } protected override Task ExecuteInternalAsync(ConfigurationTarget target) { @@ -44,10 +51,8 @@ protected override Task ExecuteInternalAsync(ConfigurationTarget target) public class UnconfigureCommand : ConfigurationCommandBase { - public UnconfigureCommand(IConfigurationService configurationService) - : base(configurationService) { } - - protected override string Name => "unconfigure"; + public UnconfigureCommand(ICommandContext context, IConfigurationService configurationService) + : base(context, "unconfigure", "Unconfigure Git Credential Manager as the Git credential helper", configurationService) { } protected override Task ExecuteInternalAsync(ConfigurationTarget target) { diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs index fdeebc458..47227c837 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/EraseCommand.cs @@ -7,14 +7,12 @@ namespace Microsoft.Git.CredentialManager.Commands /// /// Erase a previously stored from the OS secure credential store. /// - public class EraseCommand : HostProviderCommandBase + public class EraseCommand : GitCommandBase { - public EraseCommand(IHostProviderRegistry hostProviderRegistry) - : base(hostProviderRegistry) { } + public EraseCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry) + : base(context, "erase", "[Git] Erase a stored credential", hostProviderRegistry) { } - protected override string Name => "erase"; - - protected override Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider) + protected override Task ExecuteInternalAsync(InputArguments input, IHostProvider provider) { return provider.EraseCredentialAsync(input); } diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs index 94b236cd4..18323b1ed 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/GetCommand.cs @@ -8,14 +8,12 @@ namespace Microsoft.Git.CredentialManager.Commands /// /// Acquire a new from a . /// - public class GetCommand : HostProviderCommandBase + public class GetCommand : GitCommandBase { - public GetCommand(IHostProviderRegistry hostProviderRegistry) - : base(hostProviderRegistry) { } + public GetCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry) + : base(context, "get", "[Git] Return a stored credential", hostProviderRegistry) { } - protected override string Name => "get"; - - protected override async Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider) + protected override async Task ExecuteInternalAsync(InputArguments input, IHostProvider provider) { ICredential credential = await provider.GetCredentialAsync(input); @@ -40,7 +38,7 @@ protected override async Task ExecuteInternalAsync(ICommandContext context, Inpu output["password"] = credential.Password; // Write the values to standard out - context.Streams.Out.WriteDictionary(output); + Context.Streams.Out.WriteDictionary(output); } } } diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/GitCommandBase.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/GitCommandBase.cs new file mode 100644 index 000000000..062f3561f --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/GitCommandBase.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading.Tasks; + +namespace Microsoft.Git.CredentialManager.Commands +{ + /// + /// Represents a command which selects a from a + /// based on the from standard input, and interacts with a . + /// + public abstract class GitCommandBase : Command + { + private readonly IHostProviderRegistry _hostProviderRegistry; + + protected GitCommandBase(ICommandContext context, string name, string description, IHostProviderRegistry hostProviderRegistry) + : base(name, description) + { + EnsureArgument.NotNull(hostProviderRegistry, nameof(hostProviderRegistry)); + EnsureArgument.NotNull(context, nameof(context)); + + Context = context; + _hostProviderRegistry = hostProviderRegistry; + + Handler = CommandHandler.Create(ExecuteAsync); + } + + protected ICommandContext Context { get; } + + internal async Task ExecuteAsync() + { + Context.Trace.WriteLine($"Start '{Name}' command..."); + + // Parse standard input arguments + // git-credential treats the keys as case-sensitive; so should we. + IDictionary inputDict = await Context.Streams.In.ReadDictionaryAsync(StringComparer.Ordinal); + var input = new InputArguments(inputDict); + + // Set the remote URI to scope settings to throughout the process from now on + Context.Settings.RemoteUri = input.GetRemoteUri(); + + // Determine the host provider + Context.Trace.WriteLine("Detecting host provider for input:"); + Context.Trace.WriteDictionarySecrets(inputDict, new []{ "password" }, StringComparer.OrdinalIgnoreCase); + IHostProvider provider = await _hostProviderRegistry.GetProviderAsync(input); + Context.Trace.WriteLine($"Host provider '{provider.Name}' was selected."); + + await ExecuteInternalAsync(input, provider); + + Context.Trace.WriteLine($"End '{Name}' command..."); + } + + /// + /// Execute the command using the given and . + /// + /// Input arguments of the current Git credential query. + /// Host provider for the current . + /// Awaitable task for the command execution. + protected abstract Task ExecuteInternalAsync(InputArguments input, IHostProvider provider); + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/HelpCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/HelpCommand.cs deleted file mode 100644 index 1dbda5298..000000000 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/HelpCommand.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.Git.CredentialManager.Commands -{ - /// - /// Print usage information and basic help for Git Credential Manager. - /// - public class HelpCommand : CommandBase - { - private readonly string _appName; - - public HelpCommand(string appName) - { - EnsureArgument.NotNullOrWhiteSpace(appName, nameof(appName)); - - _appName = appName; - } - - public override bool CanExecute(string[] args) - { - return args.Any(x => StringComparer.OrdinalIgnoreCase.Equals(x, "--help") || - StringComparer.OrdinalIgnoreCase.Equals(x, "-h") || - StringComparer.OrdinalIgnoreCase.Equals(x, "help") || - (x != null && x.Contains('?'))); - } - - public override Task ExecuteAsync(ICommandContext context, string[] args) - { - context.Streams.Out.WriteLine(Constants.GetProgramHeader()); - - PrintUsage(context.Streams.Out, _appName); - - return Task.CompletedTask; - } - - /// - /// Print the standard usage documentation for Git Credential Manager to the given . - /// - /// Text writer to write usage information to. - /// Application name. - public static void PrintUsage(TextWriter writer, string appName) - { - writer.WriteLine(); - writer.WriteLine("usage: {0} ", appName); - writer.WriteLine(); - writer.WriteLine(" Available commands:"); - writer.WriteLine(" erase"); - writer.WriteLine(" get"); - writer.WriteLine(" store"); - writer.WriteLine(); - writer.WriteLine(" configure [--system]"); - writer.WriteLine(" unconfigure [--system]"); - writer.WriteLine(); - writer.WriteLine(" --version, version"); - writer.WriteLine(" --help, -h, -?"); - writer.WriteLine(); - } - } -} diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/ProviderCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/ProviderCommand.cs new file mode 100644 index 000000000..88f8ec105 --- /dev/null +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/ProviderCommand.cs @@ -0,0 +1,20 @@ +using System.CommandLine; + +namespace Microsoft.Git.CredentialManager.Commands +{ + public interface ICommandProvider + { + /// + /// Create a custom provider command. + /// + ProviderCommand CreateCommand(); + } + + public class ProviderCommand : Command + { + public ProviderCommand(IHostProvider provider) + : base(provider.Id, $"Commands for interacting with the {provider.Name} host provider") + { + } + } +} diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/StoreCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/StoreCommand.cs index c3e6cef74..4bb580a6e 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/StoreCommand.cs +++ b/src/shared/Microsoft.Git.CredentialManager/Commands/StoreCommand.cs @@ -7,14 +7,12 @@ namespace Microsoft.Git.CredentialManager.Commands /// /// Store a previously created in the OS secure credential store. /// - public class StoreCommand : HostProviderCommandBase + public class StoreCommand : GitCommandBase { - public StoreCommand(IHostProviderRegistry hostProviderRegistry) - : base(hostProviderRegistry) { } + public StoreCommand(ICommandContext context, IHostProviderRegistry hostProviderRegistry) + : base(context, "store", "[Git] Store a credential", hostProviderRegistry) { } - protected override string Name => "store"; - - protected override Task ExecuteInternalAsync(ICommandContext context, InputArguments input, IHostProvider provider) + protected override Task ExecuteInternalAsync(InputArguments input, IHostProvider provider) { return provider.StoreCredentialAsync(input); } diff --git a/src/shared/Microsoft.Git.CredentialManager/Commands/VersionCommand.cs b/src/shared/Microsoft.Git.CredentialManager/Commands/VersionCommand.cs deleted file mode 100644 index da2be0668..000000000 --- a/src/shared/Microsoft.Git.CredentialManager/Commands/VersionCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.Git.CredentialManager.Commands -{ - /// - /// Print version information for Git Credential Manager. - /// - public class VersionCommand : CommandBase - { - public override bool CanExecute(string[] args) - { - return args.Any(x => StringComparer.OrdinalIgnoreCase.Equals(x, "--version")) - || args.Any(x => StringComparer.OrdinalIgnoreCase.Equals(x, "version")); - } - - public override Task ExecuteAsync(ICommandContext context, string[] args) - { - string appHeader = Constants.GetProgramHeader(); - - context.Streams.Out.WriteLine(appHeader); - - return Task.CompletedTask; - } - } -} diff --git a/src/shared/Microsoft.Git.CredentialManager/Microsoft.Git.CredentialManager.csproj b/src/shared/Microsoft.Git.CredentialManager/Microsoft.Git.CredentialManager.csproj index 7d93864f6..009d66469 100644 --- a/src/shared/Microsoft.Git.CredentialManager/Microsoft.Git.CredentialManager.csproj +++ b/src/shared/Microsoft.Git.CredentialManager/Microsoft.Git.CredentialManager.csproj @@ -19,6 +19,7 @@ +