-
Notifications
You must be signed in to change notification settings - Fork 15
Add ability to link multiple referenced packages at once #27
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
base: master
Are you sure you want to change the base?
Changes from all commits
2704dff
ac7ba42
1a46605
e4b92c2
eaf2fef
f4828a8
1eae899
c099b02
385cdc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection.Metadata.Ecma335; | ||
|
@@ -15,47 +16,86 @@ public LinkCommand(IUserInterface ui) | |
_ui = ui; | ||
} | ||
|
||
public int Execute(NuLinkCommandOptions options) | ||
public void Execute(NuLinkCommandOptions options) | ||
{ | ||
_ui.ReportMedium(() => | ||
$"Checking package references in {(options.ProjectIsSolution ? "solution" : "project")}: {options.ConsumerProjectPath}"); | ||
|
||
var requestedPackage = GetPackageInfo(); | ||
var allPackages = GetAllPackages(options); | ||
var localProjectPath = options.LocalProjectPath; | ||
|
||
if (options.Mode != NuLinkCommandOptions.LinkMode.AllToAll) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A styling hint: in order to keep functions short, I'd extract "then" and "else" into two nested functions. |
||
{ | ||
var requestedPackage = GetPackage(allPackages, options.PackageId); | ||
|
||
if (options.Mode == NuLinkCommandOptions.LinkMode.SingleToAll) | ||
{ | ||
localProjectPath = GetAllProjects(options.RootDirectory). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd avoid reassigning a local variable because it reduces readability. Instead, you can introduce a new variable named |
||
FirstOrDefault(proj => proj.Contains(requestedPackage.PackageId + ".csproj")); | ||
} | ||
|
||
LinkPackage(requestedPackage, localProjectPath, options.Mode == NuLinkCommandOptions.LinkMode.SingleToSingle); | ||
} | ||
else | ||
{ | ||
var allProjectsInRoot = GetAllProjects(options.RootDirectory).ToList(); | ||
|
||
foreach (var package in allPackages) | ||
{ | ||
localProjectPath = allProjectsInRoot.FirstOrDefault(proj => proj.Contains(package.PackageId + ".csproj")); | ||
LinkPackage(package, localProjectPath, false); | ||
} | ||
} | ||
} | ||
|
||
private void LinkPackage(PackageReferenceInfo requestedPackage, string localProjectPath, bool singleMode) | ||
{ | ||
if (localProjectPath == null) | ||
{ | ||
_ui.Report(singleMode ? VerbosityLevel.Error : VerbosityLevel.Low, () => | ||
$"{(singleMode ? string.Concat(VerbosityLevel.Error.ToString(), ": ") : string.Empty)}" + | ||
$"Cannot find corresponding project to package {requestedPackage.PackageId}"); | ||
|
||
return; | ||
} | ||
|
||
var linkTargetPath = Path.Combine(Path.GetDirectoryName(localProjectPath), "bin", "Debug"); | ||
var status = requestedPackage.CheckStatus(); | ||
var linkTargetPath = Path.Combine(Path.GetDirectoryName(options.LocalProjectPath), "bin", "Debug"); | ||
|
||
ValidateOperation(); | ||
if (!ValidateOperation()) | ||
{ | ||
return; | ||
} | ||
|
||
PerformOperation(); | ||
|
||
_ui.ReportSuccess(() => $"Linked {requestedPackage.LibFolderPath}"); | ||
_ui.ReportSuccess(() => $" -> {linkTargetPath}", ConsoleColor.Magenta); | ||
return 0; | ||
|
||
PackageReferenceInfo GetPackageInfo() | ||
{ | ||
var allProjects = new WorkspaceLoader().LoadProjects(options.ConsumerProjectPath, options.ProjectIsSolution); | ||
var referenceLoader = new PackageReferenceLoader(_ui); | ||
var allPackages = referenceLoader.LoadPackageReferences(allProjects); | ||
var package = allPackages.FirstOrDefault(p => p.PackageId == options.PackageId); | ||
return package ?? throw new Exception($"Error: Package not referenced: {options.PackageId}"); | ||
} | ||
|
||
void ValidateOperation() | ||
bool ValidateOperation() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TL;DR: let's keep throwing exceptions rather than returning a I was thinking quite a bit about this one. If we want to support both "ignore errors" and "fail fast" modes, the code can become complicated. I mean that using exception results in simpler code. On the other hand, we have "do not use exceptions to steer logic". In the bottom line, I think that throwing exceptions from |
||
{ | ||
if (!status.LibFolderExists) | ||
{ | ||
throw new Exception($"Error: Cannot link package {options.PackageId}: 'lib' folder not found, expected {requestedPackage.LibFolderPath}"); | ||
_ui.ReportError(() => $"Error: Cannot link package {requestedPackage.PackageId}: 'lib' folder not found, expected {requestedPackage.LibFolderPath}"); | ||
return false; | ||
} | ||
|
||
if (status.IsLibFolderLinked) | ||
{ | ||
throw new Exception($"Error: Package {requestedPackage.PackageId} is already linked to {status.LibFolderLinkTargetPath}"); | ||
_ui.Report(singleMode ? VerbosityLevel.Error : VerbosityLevel.Low, () => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I wrote above, this should be an error only if the link target path differs from the requested one. Maybe this check should be performed earlier. |
||
$"{(singleMode ? string.Concat(VerbosityLevel.Error.ToString(), ": ") : string.Empty)}" + | ||
$"Package {requestedPackage.PackageId} is already linked to {status.LibFolderLinkTargetPath}"); | ||
|
||
return false; | ||
} | ||
|
||
if (!Directory.Exists(linkTargetPath)) | ||
{ | ||
throw new Exception($"Error: Target link directory doesn't exist: {linkTargetPath}"); | ||
_ui.ReportError(() => $"Error: Target link directory doesn't exist: {linkTargetPath}"); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
void PerformOperation() | ||
|
@@ -66,13 +106,14 @@ void PerformOperation() | |
} | ||
else | ||
{ | ||
_ui.ReportWarning(() => $"Warning: backup folder was not expected to exist: {requestedPackage.LibBackupFolderPath}"); | ||
_ui.ReportWarning(() => | ||
$"Warning: backup folder was not expected to exist: {requestedPackage.LibBackupFolderPath}"); | ||
} | ||
|
||
try | ||
{ | ||
SymbolicLinkWithDiagnostics.Create( | ||
fromPath: requestedPackage.LibFolderPath, | ||
fromPath: requestedPackage.LibFolderPath, | ||
toPath: linkTargetPath); | ||
} | ||
catch | ||
|
@@ -96,10 +137,31 @@ void RevertOperation() | |
_ui.ReportError(() => "--- MANUAL RECOVERY INSTRUCTIONS ---"); | ||
_ui.ReportError(() => $"1. Go to {Path.GetDirectoryName(requestedPackage.LibFolderPath)}"); | ||
_ui.ReportError(() => $"2. Rename '{Path.GetFileName(requestedPackage.LibBackupFolderPath)}'" + | ||
$" to '{Path.GetFileName(requestedPackage.LibFolderPath)}'"); | ||
$" to '{Path.GetFileName(requestedPackage.LibFolderPath)}'"); | ||
_ui.ReportError(() => "--- END OF RECOVERY INSTRUCTIONS ---"); | ||
} | ||
} | ||
} | ||
|
||
private PackageReferenceInfo GetPackage(HashSet<PackageReferenceInfo> allPackages, string packageId) | ||
{ | ||
var package = allPackages.FirstOrDefault(p => p.PackageId == packageId); | ||
return package ?? throw new Exception($"Error: Package not referenced: {packageId}"); | ||
} | ||
|
||
private HashSet<PackageReferenceInfo> GetAllPackages(NuLinkCommandOptions options) | ||
{ | ||
var allProjects = new WorkspaceLoader().LoadProjects(options.ConsumerProjectPath, options.ProjectIsSolution); | ||
var referenceLoader = new PackageReferenceLoader(_ui); | ||
var allPackages = referenceLoader.LoadPackageReferences(allProjects); | ||
return allPackages; | ||
} | ||
|
||
private IEnumerable<string> GetAllProjects(string rootDir) | ||
{ | ||
var slnPaths = Directory.GetFiles(rootDir, "*.sln", SearchOption.AllDirectories); | ||
var allProjects = new WorkspaceLoader().LoadProjects(slnPaths).Select(proj => proj.ProjectFile.Path); | ||
return allProjects; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea behind the
int
return value is to return an exit code, which can be useful for scripting. I guessint
becomes tricky because of a multi-package operation, which can result in a mix of successes and failures. In such a case, I'd prefer to define the overall success and failure.This is what I suggest:
link
command, the "package already linked" situation, when the link target is the desired location, should be a success rather than an error (print a success message, do nothing).unlink
command, if the package is not linked, it's a success and not an error (print a success message, do nothing).--ignore-errors
flag is specified, we run all packages, report all errors as warnings, and always exit with code 0.(as for implementation, see comment below about exceptions)