diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj
index 92d428eade..b3197b2dc3 100644
--- a/src/chocolatey.tests/chocolatey.tests.csproj
+++ b/src/chocolatey.tests/chocolatey.tests.csproj
@@ -88,6 +88,7 @@
+
diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyExportCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyExportCommandSpecs.cs
new file mode 100644
index 0000000000..d195c0c3f7
--- /dev/null
+++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyExportCommandSpecs.cs
@@ -0,0 +1,220 @@
+// Copyright © 2017 - 2018 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 RealDimensions Software, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.tests.infrastructure.app.commands
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using chocolatey.infrastructure.app.attributes;
+ using chocolatey.infrastructure.app.commands;
+ using chocolatey.infrastructure.app.configuration;
+ using chocolatey.infrastructure.app.services;
+ using chocolatey.infrastructure.commandline;
+ using chocolatey.infrastructure.filesystem;
+ using Moq;
+ using Should;
+
+ public class ChocolateyExportCommandSpecs
+ {
+ public abstract class ChocolateyExportCommandSpecsBase : TinySpec
+ {
+ protected ChocolateyExportCommand command;
+ protected Mock nugetService = new Mock();
+ protected Mock fileSystem = new Mock();
+ protected ChocolateyConfiguration configuration = new ChocolateyConfiguration();
+
+ public override void Context()
+ {
+ command = new ChocolateyExportCommand(nugetService.Object, fileSystem.Object);
+ }
+
+ public void reset()
+ {
+ nugetService.ResetCalls();
+ fileSystem.ResetCalls();
+ }
+ }
+
+ public class when_implementing_command_for : ChocolateyExportCommandSpecsBase
+ {
+ private List results;
+
+ public override void Because()
+ {
+ results = command.GetType().GetCustomAttributes(typeof(CommandForAttribute), false).Cast().Select(a => a.CommandName).ToList();
+ }
+
+ [Fact]
+ public void should_implement_help()
+ {
+ results.ShouldContain("export");
+ }
+ }
+
+ public class when_configurating_the_argument_parser : ChocolateyExportCommandSpecsBase
+ {
+ private OptionSet optionSet;
+
+ public override void Context()
+ {
+ base.Context();
+ optionSet = new OptionSet();
+ }
+
+ public override void Because()
+ {
+ command.configure_argument_parser(optionSet, configuration);
+ }
+
+ [Fact]
+ public void should_add_output_file_path_to_the_option_set()
+ {
+ optionSet.Contains("output-file-path").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void should_add_short_version_of_output_file_path_to_the_option_set()
+ {
+ optionSet.Contains("o").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void should_add_include_version_numbers_to_the_option_set()
+ {
+ optionSet.Contains("include-version-numbers").ShouldBeTrue();
+ }
+
+ [Fact]
+ public void should_add_include_version_to_the_option_set()
+ {
+ optionSet.Contains("include-version").ShouldBeTrue();
+ }
+ }
+
+ public class when_handling_additional_argument_parsing : ChocolateyExportCommandSpecsBase
+ {
+ private readonly IList unparsedArgs = new List();
+ private Action because;
+
+ public override void Because()
+ {
+ because = () => command.handle_additional_argument_parsing(unparsedArgs, configuration);
+ }
+
+ public new void reset()
+ {
+ configuration.ExportCommand.OutputFilePath = string.Empty;
+ unparsedArgs.Clear();
+ base.reset();
+ }
+
+ [Fact]
+ public void should_handle_passing_in_an_empty_string_for_output_file_path()
+ {
+ reset();
+ unparsedArgs.Add(" ");
+ because();
+
+ configuration.ExportCommand.OutputFilePath.ShouldEqual("packages.config");
+ }
+
+ [Fact]
+ public void should_handle_passing_in_a_string_for_output_file_path()
+ {
+ reset();
+ unparsedArgs.Add("custompackages.config");
+ because();
+
+ configuration.ExportCommand.OutputFilePath.ShouldEqual("custompackages.config");
+ }
+ }
+
+ public class when_noop_is_called : ChocolateyExportCommandSpecsBase
+ {
+ public override void Because()
+ {
+ command.noop(configuration);
+ }
+
+ [Fact]
+ public void should_log_a_message()
+ {
+ MockLogger.Verify(l => l.Info(It.IsAny()), Times.AtLeastOnce);
+ }
+
+ [Fact]
+ public void should_log_the_message_we_expect()
+ {
+ var messages = MockLogger.MessagesFor(LogLevel.Info);
+ messages.ShouldNotBeEmpty();
+ messages.Count.ShouldEqual(1);
+ messages[0].ShouldContain("Export would have been with options");
+ }
+ }
+
+ public class when_run_is_called : ChocolateyExportCommandSpecsBase
+ {
+ public new void reset()
+ {
+ Context();
+ base.reset();
+ }
+
+ public override void AfterEachSpec()
+ {
+ base.AfterEachSpec();
+ MockLogger.Messages.Clear();
+ }
+
+ public override void Because()
+ {
+ // because = () => command.run(configuration);
+ }
+
+ [Fact]
+ public void should_call_nuget_service_get_all_installed_packages()
+ {
+ reset();
+ command.run(configuration);
+
+ nugetService.Verify(n => n.get_all_installed_packages(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public void should_call_replace_file_when_file_already_exists()
+ {
+ fileSystem.Setup(f => f.file_exists(It.IsAny())).Returns(true);
+
+ reset();
+ command.run(configuration);
+
+ fileSystem.Verify(n => n.replace_file(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public void should_not_call_replace_file_when_file_doesnt_exist()
+ {
+ fileSystem.Setup(f => f.file_exists(It.IsAny())).Returns(false);
+
+ reset();
+ command.run(configuration);
+
+ fileSystem.Verify(n => n.replace_file(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+ }
+ }
+ }
+}
diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj
index 5c7a16224c..6635f7dd4e 100644
--- a/src/chocolatey/chocolatey.csproj
+++ b/src/chocolatey/chocolatey.csproj
@@ -113,6 +113,7 @@
Properties\SolutionVersion.cs
+
diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyExportCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyExportCommand.cs
new file mode 100644
index 0000000000..177f68d221
--- /dev/null
+++ b/src/chocolatey/infrastructure.app/commands/ChocolateyExportCommand.cs
@@ -0,0 +1,197 @@
+// Copyright © 2017 - 2018 Chocolatey Software, Inc
+// Copyright © 2011 - 2017 RealDimensions Software, LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace chocolatey.infrastructure.app.commands
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Text;
+ using System.Xml;
+ using attributes;
+ using commandline;
+ using configuration;
+ using filesystem;
+ using infrastructure.commands;
+ using logging;
+ using services;
+ using tolerance;
+
+ [CommandFor("export", "exports list of currently installed packages")]
+ public class ChocolateyExportCommand : ICommand
+ {
+ private readonly INugetService _nugetService;
+ private readonly IFileSystem _fileSystem;
+
+ public ChocolateyExportCommand(INugetService nugetService, IFileSystem fileSystem)
+ {
+ _nugetService = nugetService;
+ _fileSystem = fileSystem;
+ }
+
+ public void configure_argument_parser(OptionSet optionSet, ChocolateyConfiguration configuration)
+ {
+ optionSet
+ .Add("o=|output-file-path=",
+ "Output File Path - the path to where the list of currently installed packages should be saved. Defaults to packages.config.",
+ option => configuration.ExportCommand.OutputFilePath = option.remove_surrounding_quotes())
+ .Add("include-version-numbers|include-version",
+ "Include Version Numbers - controls whether or not version numbers for each package appear in generated file. Defaults to false.",
+ option => configuration.ExportCommand.IncludeVersionNumbers = option != null)
+ ;
+ }
+
+ public void handle_additional_argument_parsing(IList unparsedArguments, ChocolateyConfiguration configuration)
+ {
+ configuration.Input = string.Join(" ", unparsedArguments);
+
+ if (string.IsNullOrWhiteSpace(configuration.ExportCommand.OutputFilePath) && unparsedArguments.Count >=1)
+ {
+ configuration.ExportCommand.OutputFilePath = unparsedArguments[0];
+ }
+
+ // If no value has been provided for the OutputFilePath, default to packages.config
+ if (string.IsNullOrWhiteSpace(configuration.ExportCommand.OutputFilePath))
+ {
+ configuration.ExportCommand.OutputFilePath = "packages.config";
+ }
+ }
+
+ public void handle_validation(ChocolateyConfiguration configuration)
+ {
+ // Currently, no additional validation is required.
+ }
+
+ public void help_message(ChocolateyConfiguration configuration)
+ {
+ this.Log().Info(ChocolateyLoggers.Important, "Export Command");
+ this.Log().Info(@"
+Export all currently installed packages to a file.
+
+This is especially helpful when re-building a machine that was created
+using Chocolatey. Export all packages to a file, and then re-install
+those packages onto new machine using `choco install packages.config`.
+");
+ "chocolatey".Log().Info(ChocolateyLoggers.Important, "Usage");
+ "chocolatey".Log().Info(@"
+ choco export []
+");
+
+ "chocolatey".Log().Info(ChocolateyLoggers.Important, "Examples");
+ "chocolatey".Log().Info(@"
+ choco export
+ choco export --include-version-numbers
+ choco export ""'c:\temp\packages.config'""
+ choco export ""'c:\temp\packages.config'"" --include-version-numbers
+ choco export -o=""'c:\temp\packages.config'""
+ choco export -o=""'c:\temp\packages.config'"" --include-version-numbers
+ choco export --output-file-path=""'c:\temp\packages.config'""
+ choco export --output-file-path=""'c:\temp\packages.config'"" --include-version-numbers
+
+NOTE: See scripting in the command reference (`choco -?`) for how to
+ write proper scripts and integrations.
+
+");
+
+ "chocolatey".Log().Info(ChocolateyLoggers.Important, "Exit Codes");
+ "chocolatey".Log().Info(@"
+Exit codes that normally result from running this command.
+
+Normal:
+ - 0: operation was successful, no issues detected
+ - -1 or 1: an error has occurred
+
+If you find other exit codes that we have not yet documented, please
+ file a ticket so we can document it at
+ https://github.com/chocolatey/choco/issues/new/choose.
+
+");
+
+ "chocolatey".Log().Info(ChocolateyLoggers.Important, "Options and Switches");
+ }
+
+ public bool may_require_admin_access()
+ {
+ return false;
+ }
+
+ public void noop(ChocolateyConfiguration configuration)
+ {
+ this.Log().Info("Export would have been with options: {0} Output File Path={1}{0} Include Version Numbers:{2}".format_with(Environment.NewLine, configuration.ExportCommand.OutputFilePath, configuration.ExportCommand.IncludeVersionNumbers));
+ }
+
+ public void run(ChocolateyConfiguration configuration)
+ {
+ var packageResults = _nugetService.get_all_installed_packages(configuration);
+ var settings = new XmlWriterSettings { Indent = true, Encoding = new UTF8Encoding(false) };
+
+ FaultTolerance.try_catch_with_logging_exception(
+ () =>
+ {
+ using (var stringWriter = new StringWriter())
+ {
+ using (var xw = XmlWriter.Create(stringWriter, settings))
+ {
+ xw.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\"");
+ xw.WriteStartElement("packages");
+
+ foreach (var packageResult in packageResults)
+ {
+ xw.WriteStartElement("package");
+ xw.WriteAttributeString("id", packageResult.Package.Id);
+
+ if (configuration.ExportCommand.IncludeVersionNumbers)
+ {
+ xw.WriteAttributeString("version", packageResult.Package.Version.ToString());
+ }
+
+ xw.WriteEndElement();
+ }
+
+ xw.WriteEndElement();
+ xw.Flush();
+ }
+
+ var fileExists = _fileSystem.file_exists(configuration.ExportCommand.OutputFilePath);
+
+ // If the file doesn't already exist, just write the new one out directly
+ if (!fileExists)
+ {
+ _fileSystem.write_file(
+ configuration.ExportCommand.OutputFilePath,
+ stringWriter.GetStringBuilder().ToString(),
+ new UTF8Encoding(false));
+
+ return;
+ }
+
+
+ // Otherwise, create an update file, and resiliently move it into place.
+ var tempUpdateFile = configuration.ExportCommand.OutputFilePath + "." + Process.GetCurrentProcess().Id + ".update";
+ _fileSystem.write_file(tempUpdateFile,
+ stringWriter.GetStringBuilder().ToString(),
+ new UTF8Encoding(false));
+
+ _fileSystem.replace_file(tempUpdateFile, configuration.ExportCommand.OutputFilePath, configuration.ExportCommand.OutputFilePath + ".backup");
+ }
+ },
+ errorMessage: "Error exporting currently installed packages",
+ throwError: true
+ );
+ }
+ }
+}
diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs
index c5cc33f73f..4f8c8679d3 100644
--- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs
+++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs
@@ -50,6 +50,7 @@ public ChocolateyConfiguration()
PinCommand = new PinCommandConfiguration();
OutdatedCommand = new OutdatedCommandConfiguration();
Proxy = new ProxyConfiguration();
+ ExportCommand = new ExportCommandConfiguration();
#if DEBUG
AllowUnofficialBuild = true;
#endif
@@ -334,6 +335,8 @@ private void append_output(StringBuilder propertyValues, string append)
///
public OutdatedCommandConfiguration OutdatedCommand { get; set; }
+ public ExportCommandConfiguration ExportCommand { get; set; }
+
///
/// Configuration related specifically to proxies.
///
@@ -383,7 +386,7 @@ public sealed class FeaturesConfiguration
public bool VirusCheck { get; set; }
public bool FailOnInvalidOrMissingLicense { get; set; }
public bool IgnoreInvalidOptionsSwitches { get; set; }
- public bool UsePackageExitCodes { get; set; }
+ public bool UsePackageExitCodes { get; set; }
public bool UseEnhancedExitCodes { get; set; }
public bool UseFipsCompliantChecksums { get; set; }
public bool ShowNonElevatedWarnings { get; set; }
@@ -545,4 +548,12 @@ public sealed class ProxyConfiguration
public string BypassList { get; set; }
public bool BypassOnLocal { get; set; }
}
+
+ [Serializable]
+ public sealed class ExportCommandConfiguration
+ {
+ public bool IncludeVersionNumbers { get; set; }
+
+ public string OutputFilePath { get; set; }
+ }
}
\ No newline at end of file
diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs
index 86d4953099..f069d964f8 100644
--- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs
+++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs
@@ -96,7 +96,8 @@ public void RegisterComponents(Container container)
new ChocolateyApiKeyCommand(container.GetInstance()),
new ChocolateyUnpackSelfCommand(container.GetInstance()),
new ChocolateyVersionCommand(container.GetInstance()),
- new ChocolateyUpdateCommand(container.GetInstance())
+ new ChocolateyUpdateCommand(container.GetInstance()),
+ new ChocolateyExportCommand(container.GetInstance(), container.GetInstance())
};
return list.AsReadOnly();
}, Lifestyle.Singleton);
diff --git a/src/chocolatey/infrastructure.app/services/INugetService.cs b/src/chocolatey/infrastructure.app/services/INugetService.cs
index bcb7179a4c..950ba408ac 100644
--- a/src/chocolatey/infrastructure.app/services/INugetService.cs
+++ b/src/chocolatey/infrastructure.app/services/INugetService.cs
@@ -1,13 +1,13 @@
// Copyright © 2017 - 2019 Chocolatey Software, Inc
// Copyright © 2011 - 2017 RealDimensions Software, LLC
-//
+//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
-//
+//
// You may obtain a copy of the License at
-//
+//
// http://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,6 +17,7 @@
namespace chocolatey.infrastructure.app.services
{
using System.Collections.Concurrent;
+ using System.Collections.Generic;
using configuration;
using results;
@@ -58,5 +59,11 @@ public interface INugetService : ISourceRunner
/// Name of the package.
void remove_rollback_directory_if_exists(string packageName);
+
+ ///
+ /// Get all installed packages
+ ///
+ /// The configuration
+ IEnumerable get_all_installed_packages(ChocolateyConfiguration config);
}
}
diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs
index 0a749a7c3b..7676852284 100644
--- a/src/chocolatey/infrastructure.app/services/NugetService.cs
+++ b/src/chocolatey/infrastructure.app/services/NugetService.cs
@@ -1551,7 +1551,7 @@ public virtual void remove_installation_files(IPackage removedPackage, Chocolate
}
}
- private IEnumerable get_all_installed_packages(ChocolateyConfiguration config)
+ public IEnumerable get_all_installed_packages(ChocolateyConfiguration config)
{
//todo : move to deep copy for get all installed
//var listConfig = config.deep_copy();