diff --git a/README.md b/README.md index 85709c0..3481b22 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Files and folders will be recursively add/removed to match the filesystem. `SolutionItems` folder will be created and populated if missing. The name can be customized with `--folder`. +If there is only one `.sln` file in the current folder then the `--solution` argument can be omitted and it will automatically be found. + ## License - [A-GPL v3](LICENSE) diff --git a/src/CLI.cs b/src/CLI.cs index 9f30a46..0ff8157 100644 --- a/src/CLI.cs +++ b/src/CLI.cs @@ -18,8 +18,8 @@ public CLI(IGuidGenerator guidGenerator) private class Options { - [Option('s', "solution", Required = true, HelpText = "path to .sln file to modify")] - public required string SlnPath { get; set; } + [Option('s', "solution", HelpText = "path to .sln file to modify")] + public string? SlnPath { get; set; } [Option('f', "folder", Required = false, HelpText = "Solution folder to target")] public string SlnFolder { get; set; } = "SolutionItems"; @@ -61,6 +61,9 @@ public int Run(string[] args) Console.Error.WriteLine(); WriteUsage(); return 5; + } catch(MultipleSlnFilesFoundException ex) { + Console.Error.WriteLine(ex.Message); + return 6; } Console.Out.WriteLine("Sync completed."); diff --git a/src/SlnSync.cs b/src/SlnSync.cs index 42dacff..e01f088 100644 --- a/src/SlnSync.cs +++ b/src/SlnSync.cs @@ -18,8 +18,12 @@ public SlnSync() : this(new DefaultGuidGenerator()) /// relative path to sln file to modify /// /// list of paths to recursively add/update SolutionItems virtual folders with - public void SyncSlnFile(string slnPath, string slnFolder, IEnumerable paths) + public void SyncSlnFile(string? slnPath, string slnFolder, IEnumerable paths) { + if (slnPath is null) + { + slnPath = FindSlnFile(); + } if (!slnPath.EndsWith(".sln")) { throw new InvalidSlnPathException(slnPath); @@ -106,6 +110,23 @@ private void SyncFolder(SolutionFolder parentFolder, DirectoryInfo directory, st } } + /// + /// search current folder for single .sln file and return it + /// throw if not found or more than one found + /// + /// sln file if found + /// + private static string FindSlnFile() + { + var slnFiles = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.sln"); + return slnFiles.Length switch + { + 0 => throw new SlnFileNotFoundException("No .sln files found in current directory."), + > 1 => throw new MultipleSlnFilesFoundException("Multiple .sln files found in current directory. Specify one with --solution"), + _ => slnFiles[0], + }; + } + private SolutionFolder FindOrCreateSolutionFolder(ICollection solutionProjects, string solutionFolderName) { @@ -126,6 +147,7 @@ private SolutionFolder FindOrCreateSolutionFolder(ICollection solution } public class PathNotFoundException(string path) : Exception($"Path not found: '{path}'"); +public class MultipleSlnFilesFoundException(string message) : Exception(message: message); public class SlnFileNotFoundException(string path) : Exception($"Invalid .sln file '{path}' - File not found."); public class InvalidSlnPathException(string path) : Exception($"Invalid .sln file '{path}' - File must have .sln extension"); public class EmptyPathListException() : Exception(); diff --git a/tests/IntegrationTests.cs b/tests/IntegrationTests.cs index f9ccbf4..3f5fe5a 100644 --- a/tests/IntegrationTests.cs +++ b/tests/IntegrationTests.cs @@ -9,6 +9,7 @@ public class IntegrationTests private const string TargetSlnFile = "target.sln"; private readonly CLI _cli; private readonly string _testFolder; + private const int SuccessExitCode = 0; private static readonly string[] GuidsToReturn = [ @@ -58,7 +59,8 @@ public void AddsToBlankSln() }); // Act - _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder"]); + _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); // Assert const string expected = @" @@ -143,7 +145,8 @@ public void IgnoresExistingItems() }); // Act - _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder"]); + _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); // Assert const string expected = @" @@ -229,7 +232,8 @@ public void RemovesMissingFiles() }); // Act - _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder"]); + _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); // Assert const string expected = @" @@ -298,7 +302,8 @@ public void CustomFolderName() }); // Act - _cli.Run(["-s", TargetSlnFile, "-f", "My Items", "root-file.txt", "subfolder"]); + _cli.Run(["-s", TargetSlnFile, "-f", "My Items", "root-file.txt", "subfolder"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); // Assert const string expected = @" @@ -366,7 +371,8 @@ public void PathSanitization() }); // Act - _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder/"]); + _cli.Run(["-s", TargetSlnFile, "root-file.txt", "subfolder/"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); // Assert const string expected = @" @@ -431,7 +437,8 @@ public void AddsBOM() }); // Act - _cli.Run(["-s", TargetSlnFile, "some-file.txt"]); + _cli.Run(["-s", TargetSlnFile, "some-file.txt"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); File.ReadAllText(Path.Combine(_testFolder, TargetSlnFile)); @@ -489,7 +496,8 @@ public void RemovesEmptyFolder() }); // Act - _cli.Run(["-s", TargetSlnFile, "subfolder"]); + _cli.Run(["-s", TargetSlnFile, "subfolder"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); // Assert const string expected = @" @@ -526,6 +534,75 @@ public void RemovesEmptyFolder() ModifiedSln().Should().Be(expected); } + + [Fact] + public void FindsSlnFile() + { + // Arrange + SetupSln(@" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal +"); + + SetupFilesystem(new[] + { + "a-file.txt", + }); + + // Act + _cli.Run(["a-file.txt"]) + .Should().Be(SuccessExitCode, because: "command should succeed"); + + // Assert + const string expected = @" +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""SolutionItems"", ""SolutionItems"", ""{17591C35-3F90-4F4A-AA13-45CF8D824066}"" + ProjectSection(SolutionItems) = preProject + a-file.txt = a-file.txt + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal +"; + + File.WriteAllText(Path.Combine(_testFolder, "expected.sln"), expected); // for kdiff debugging + + ModifiedSln().Should().Be(expected); + } + + [Fact] + public void FindsSlnFileInSameFolder() + { + File.WriteAllText(Path.Combine(_testFolder, "solution1.sln"), "blank"); + File.WriteAllText(Path.Combine(_testFolder, "solution2.sln"), "blank"); + _cli.Run(["some-file.txt"]) + .Should().Be(6, because: "two solution files exist and arguments didn't specify which to use"); + // todo: assert message written to stderr + } + private string ModifiedSln() => File.ReadAllText(Path.Combine(_testFolder, TargetSlnFile)); private void SetupFilesystem(IEnumerable paths) @@ -549,7 +626,9 @@ private void SetupFile(string path) private void SetupSln(string slnContents) { - File.WriteAllText(Path.Combine(_testFolder, "original.sln"), slnContents); + var originalSlnFolder = Path.Combine(_testFolder, "original"); // put in subfolder to avoid sln detection finding it + Directory.CreateDirectory(originalSlnFolder); + File.WriteAllText(Path.Combine(originalSlnFolder, "original.sln"), slnContents); File.WriteAllText(Path.Combine(_testFolder, TargetSlnFile), slnContents); } }