diff --git a/FileDeduplicator.Test/DeduplicateConfirmationTests.cs b/FileDeduplicator.Test/DeduplicateConfirmationTests.cs
index 25a9bde..e3a4dd7 100644
--- a/FileDeduplicator.Test/DeduplicateConfirmationTests.cs
+++ b/FileDeduplicator.Test/DeduplicateConfirmationTests.cs
@@ -166,4 +166,29 @@ public void ConfirmingDeletesExactlyTheCopiesTheListingNamed()
Assert.Contains("Deleted 1 file(s).", output);
Assert.Contains("Reclaimed 5 B of disk space.", output);
}
+
+ ///
+ /// Empty files are never deleted, so a tree whose only duplicates are empty marker files has
+ /// nothing to approve: the verb says so, reads no answer, and succeeds.
+ ///
+ [TestMethod]
+ public void OnlyEmptyDuplicatesSkipTheConfirmationAndSucceed()
+ {
+ // Arrange -- the routine Python/git case: two empty __init__.py files and one unique file
+ using TempTree tree = new();
+ AbsoluteFilePath first = tree.Write("a/__init__.py", string.Empty);
+ AbsoluteFilePath second = tree.Write("b/__init__.py", string.Empty);
+ _ = tree.Write("u.txt", "unique");
+
+ // Act -- stdin is empty, as it is for a script with nothing piped in
+ string output = ConsoleCapture.Normalize(ConsoleCapture.Run(new Deduplicate { PathString = tree.Root.WeakString }, string.Empty, out int exitCode));
+
+ // Assert
+ Assert.AreEqual(0, exitCode, $"A run with nothing to delete reported failure. Output was:\n{output}");
+ Assert.Contains("Nothing to delete", output);
+ Assert.DoesNotContain("Proceed with deletion?", output, $"The verb asked to approve deleting nothing. Output was:\n{output}");
+ Assert.DoesNotContain("Aborted.", output);
+ Assert.IsTrue(TempTree.Exists(first), $"{first} is empty and should never have been touched.");
+ Assert.IsTrue(TempTree.Exists(second), $"{second} is empty and should never have been touched.");
+ }
}
diff --git a/FileDeduplicator.Test/VerbOutputTests.cs b/FileDeduplicator.Test/VerbOutputTests.cs
index 610eea7..48fa86f 100644
--- a/FileDeduplicator.Test/VerbOutputTests.cs
+++ b/FileDeduplicator.Test/VerbOutputTests.cs
@@ -439,4 +439,24 @@ private static (string Output, int ExitCode) RunWithExitCode(BaseVerb verb, stri
string output = ConsoleCapture.Run(verb, stdin, out int exitCode);
return (output, exitCode);
}
+
+ ///
+ /// When every duplicate is empty, and so kept, Scan does not point at a Deduplicate run that
+ /// would have nothing to do.
+ ///
+ [TestMethod]
+ public void ScanDoesNotSuggestDeduplicateWhenOnlyEmptyDuplicatesExist()
+ {
+ // Arrange
+ using TempTree tree = new();
+ AbsoluteFilePath first = tree.Write("a/.gitkeep", string.Empty);
+ _ = tree.Write("b/.gitkeep", string.Empty);
+
+ // Act
+ string output = ConsoleCapture.Normalize(ConsoleCapture.Run(new Scan { PathString = tree.Root.WeakString }));
+
+ // Assert
+ Assert.Contains($"{first} [KEEP, empty]", output);
+ Assert.DoesNotContain("Run the 'Deduplicate' command", output, $"Scan suggested a Deduplicate run with nothing to delete. Output was:\n{output}");
+ }
}
diff --git a/FileDeduplicator/Verbs/Deduplicate.cs b/FileDeduplicator/Verbs/Deduplicate.cs
index 2ee0f0a..6cd50ba 100644
--- a/FileDeduplicator/Verbs/Deduplicate.cs
+++ b/FileDeduplicator/Verbs/Deduplicate.cs
@@ -52,6 +52,15 @@ internal override void Run(Deduplicate options)
// and DryRun already print this; the verb that actually deletes is the one that needs it.
DeletionPlan plan = DuplicateReport.PlanDeletions(duplicates);
+ // Empty files are never deleted, so a tree whose only duplicates are empty marker files
+ // (__init__.py, .gitkeep) has nothing to do. Asking to approve deleting zero files would
+ // read stdin for nothing, and declining it would report failure to a script.
+ if (plan.FileCount == 0)
+ {
+ Console.WriteLine("Nothing to delete (only empty-file duplicates found).");
+ return;
+ }
+
Console.WriteLine($"Found {duplicates.Count} group(s) of duplicate files.");
Console.WriteLine("Keeping the copy with the shortest filename in each group.");
Console.WriteLine();
diff --git a/FileDeduplicator/Verbs/Scan.cs b/FileDeduplicator/Verbs/Scan.cs
index 4b6fb15..9766cf7 100644
--- a/FileDeduplicator/Verbs/Scan.cs
+++ b/FileDeduplicator/Verbs/Scan.cs
@@ -50,6 +50,7 @@ internal override void Run(Scan options)
// Step 4: Display results
long totalWastedBytes = 0;
+ bool anyDeletable = false;
Console.WriteLine($"Found {duplicates.Count} group(s) of duplicate files:");
Console.WriteLine();
@@ -57,6 +58,7 @@ internal override void Run(Scan options)
{
AbsoluteFilePath keeper = Deduplicator.SelectFileToKeep(group.Files);
bool deletable = Deduplicator.IsDeletable(group);
+ anyDeletable |= deletable;
long wastedBytes = group.FileSize * (group.Files.Count - 1);
totalWastedBytes += wastedBytes;
@@ -78,7 +80,13 @@ internal override void Run(Scan options)
Console.WriteLine($"Total duplicate groups: {duplicates.Count}");
Console.WriteLine($"Total wasted space: {DuplicateReport.FormatBytes(totalWastedBytes)}");
- Console.WriteLine();
- Console.WriteLine("Run the 'Deduplicate' command to remove duplicates.");
+
+ // Empty duplicates are kept, so pointing at Deduplicate when they are all there is would
+ // send the user to a verb with nothing to do.
+ if (anyDeletable)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Run the 'Deduplicate' command to remove duplicates.");
+ }
}
}