mirror of
https://github.com/yarlson/lnk.git
synced 2025-09-01 18:02:34 +02:00
feat(add): implement dry-run mode and enhance output formatting
This commit is contained in:
75
cmd/add.go
75
cmd/add.go
@@ -11,17 +11,61 @@ func newAddCmd() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "add <file>...",
|
Use: "add <file>...",
|
||||||
Short: "✨ Add files to lnk management",
|
Short: "✨ Add files to lnk management",
|
||||||
Long: "Moves files to the lnk repository and creates symlinks in their place. Supports multiple files.",
|
Long: `Moves files to the lnk repository and creates symlinks in their place. Supports multiple files.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
lnk add ~/.bashrc ~/.vimrc # Add multiple files at once
|
||||||
|
lnk add --recursive ~/.config/nvim # Add directory contents individually
|
||||||
|
lnk add --dry-run ~/.gitconfig # Preview what would be added
|
||||||
|
lnk add --host work ~/.ssh/config # Add host-specific configuration
|
||||||
|
|
||||||
|
The --recursive flag processes directory contents individually instead of treating
|
||||||
|
the directory as a single unit. This is useful for configuration directories where
|
||||||
|
you want each file managed separately.
|
||||||
|
|
||||||
|
The --dry-run flag shows you exactly what files would be added without making any
|
||||||
|
changes to your system - perfect for verification before bulk operations.`,
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
host, _ := cmd.Flags().GetString("host")
|
host, _ := cmd.Flags().GetString("host")
|
||||||
recursive, _ := cmd.Flags().GetBool("recursive")
|
recursive, _ := cmd.Flags().GetBool("recursive")
|
||||||
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
lnk := core.NewLnk(core.WithHost(host))
|
lnk := core.NewLnk(core.WithHost(host))
|
||||||
|
|
||||||
|
// Handle dry-run mode
|
||||||
|
if dryRun {
|
||||||
|
files, err := lnk.PreviewAdd(args, recursive)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display preview output
|
||||||
|
if recursive {
|
||||||
|
printf(cmd, "🔍 \033[1mWould add %d files recursively:\033[0m\n", len(files))
|
||||||
|
} else {
|
||||||
|
printf(cmd, "🔍 \033[1mWould add %d files:\033[0m\n", len(files))
|
||||||
|
}
|
||||||
|
|
||||||
|
// List files that would be added
|
||||||
|
for _, file := range files {
|
||||||
|
basename := filepath.Base(file)
|
||||||
|
printf(cmd, " 📄 \033[90m%s\033[0m\n", basename)
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(cmd, "\n💡 \033[33mTo proceed:\033[0m run without --dry-run flag\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Handle recursive mode
|
// Handle recursive mode
|
||||||
if recursive {
|
if recursive {
|
||||||
|
// Get preview to count files first for better output
|
||||||
|
previewFiles, err := lnk.PreviewAdd(args, recursive)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Create progress callback for CLI display
|
// Create progress callback for CLI display
|
||||||
progressCallback := func(current, total int, currentFile string) {
|
progressCallback := func(current, total int, currentFile string) {
|
||||||
printf(cmd, "\r⏳ Processing %d/%d: %s", current, total, currentFile)
|
printf(cmd, "\r⏳ Processing %d/%d: %s", current, total, currentFile)
|
||||||
@@ -33,6 +77,9 @@ func newAddCmd() *cobra.Command {
|
|||||||
|
|
||||||
// Clear progress line and show completion
|
// Clear progress line and show completion
|
||||||
printf(cmd, "\r")
|
printf(cmd, "\r")
|
||||||
|
|
||||||
|
// Store processed file count for display
|
||||||
|
args = previewFiles // Replace args with actual files for display
|
||||||
} else {
|
} else {
|
||||||
// Use appropriate method based on number of files
|
// Use appropriate method based on number of files
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
@@ -50,11 +97,30 @@ func newAddCmd() *cobra.Command {
|
|||||||
|
|
||||||
// Display results
|
// Display results
|
||||||
if recursive {
|
if recursive {
|
||||||
// Recursive mode - show different message
|
// Recursive mode - show enhanced message with count
|
||||||
if host != "" {
|
if host != "" {
|
||||||
printf(cmd, "✨ \033[1mAdded files recursively to lnk (host: %s)\033[0m\n", host)
|
printf(cmd, "✨ \033[1mAdded %d files recursively to lnk (host: %s)\033[0m\n", len(args), host)
|
||||||
} else {
|
} else {
|
||||||
printf(cmd, "✨ \033[1mAdded files recursively to lnk\033[0m\n")
|
printf(cmd, "✨ \033[1mAdded %d files recursively to lnk\033[0m\n", len(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show some of the files that were added (limit to first few for readability)
|
||||||
|
filesToShow := len(args)
|
||||||
|
if filesToShow > 5 {
|
||||||
|
filesToShow = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < filesToShow; i++ {
|
||||||
|
basename := filepath.Base(args[i])
|
||||||
|
if host != "" {
|
||||||
|
printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s.lnk/...\033[0m\n", basename, host)
|
||||||
|
} else {
|
||||||
|
printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/...\033[0m\n", basename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 5 {
|
||||||
|
printf(cmd, " \033[90m... and %d more files\033[0m\n", len(args)-5)
|
||||||
}
|
}
|
||||||
} else if len(args) == 1 {
|
} else if len(args) == 1 {
|
||||||
// Single file - maintain existing output format for backward compatibility
|
// Single file - maintain existing output format for backward compatibility
|
||||||
@@ -93,5 +159,6 @@ func newAddCmd() *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().StringP("host", "H", "", "Manage file for specific host (default: common configuration)")
|
cmd.Flags().StringP("host", "H", "", "Manage file for specific host (default: common configuration)")
|
||||||
cmd.Flags().BoolP("recursive", "r", false, "Add directory contents individually instead of the directory as a whole")
|
cmd.Flags().BoolP("recursive", "r", false, "Add directory contents individually instead of the directory as a whole")
|
||||||
|
cmd.Flags().BoolP("dry-run", "n", false, "Show what would be added without making changes")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
20
cmd/root.go
20
cmd/root.go
@@ -20,17 +20,19 @@ func NewRootCommand() *cobra.Command {
|
|||||||
Long: `🔗 Lnk - Git-native dotfiles management that doesn't suck.
|
Long: `🔗 Lnk - Git-native dotfiles management that doesn't suck.
|
||||||
|
|
||||||
Move your dotfiles to ~/.config/lnk, symlink them back, and use Git like normal.
|
Move your dotfiles to ~/.config/lnk, symlink them back, and use Git like normal.
|
||||||
Supports both common configurations and host-specific setups.
|
Supports both common configurations, host-specific setups, and bulk operations for multiple files.
|
||||||
|
|
||||||
✨ Examples:
|
✨ Examples:
|
||||||
lnk init # Fresh start
|
lnk init # Fresh start
|
||||||
lnk init -r <repo-url> # Clone existing dotfiles (runs bootstrap automatically)
|
lnk init -r <repo-url> # Clone existing dotfiles (runs bootstrap automatically)
|
||||||
lnk add ~/.vimrc ~/.bashrc # Start managing common files
|
lnk add ~/.vimrc ~/.bashrc # Start managing common files
|
||||||
lnk add --host work ~/.ssh/config # Manage host-specific files
|
lnk add --recursive ~/.config/nvim # Add directory contents individually
|
||||||
lnk list --all # Show all configurations
|
lnk add --dry-run ~/.gitconfig # Preview changes without applying
|
||||||
lnk pull --host work # Pull host-specific changes
|
lnk add --host work ~/.ssh/config # Manage host-specific files
|
||||||
lnk push "setup complete" # Sync to remote
|
lnk list --all # Show all configurations
|
||||||
lnk bootstrap # Run bootstrap script manually
|
lnk pull --host work # Pull host-specific changes
|
||||||
|
lnk push "setup complete" # Sync to remote
|
||||||
|
lnk bootstrap # Run bootstrap script manually
|
||||||
|
|
||||||
🚀 Bootstrap Support:
|
🚀 Bootstrap Support:
|
||||||
Automatically runs bootstrap.sh when cloning a repository.
|
Automatically runs bootstrap.sh when cloning a repository.
|
||||||
|
219
cmd/root_test.go
219
cmd/root_test.go
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -1119,6 +1120,224 @@ func (suite *CLITestSuite) TestAddCommandRecursiveMultipleDirs() {
|
|||||||
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "dir2 should not be a symlink")
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "dir2 should not be a symlink")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Task 3.1: Dry-Run Mode Tests
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestDryRunFlag() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.NoError(err)
|
||||||
|
initOutput := suite.stdout.String()
|
||||||
|
suite.Contains(initOutput, "Initialized")
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFile1 := filepath.Join(suite.tempDir, "test1.txt")
|
||||||
|
testFile2 := filepath.Join(suite.tempDir, "test2.txt")
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile1, []byte("content1"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile2, []byte("content2"), 0644))
|
||||||
|
|
||||||
|
// Run add with dry-run flag (should not exist yet)
|
||||||
|
err = suite.runCommand("add", "--dry-run", testFile1, testFile2)
|
||||||
|
suite.NoError(err, "Dry-run command should succeed")
|
||||||
|
output := suite.stdout.String()
|
||||||
|
|
||||||
|
// Basic check that some output was produced (flag exists but behavior TBD)
|
||||||
|
suite.NotEmpty(output, "Should produce some output")
|
||||||
|
|
||||||
|
// Verify files were NOT actually added (no symlinks created)
|
||||||
|
info, err := os.Lstat(testFile1)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be a symlink in dry-run")
|
||||||
|
|
||||||
|
info, err = os.Lstat(testFile2)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be a symlink in dry-run")
|
||||||
|
|
||||||
|
// Verify lnk list shows no managed files
|
||||||
|
suite.stdout.Reset()
|
||||||
|
err = suite.runCommand("list")
|
||||||
|
suite.NoError(err)
|
||||||
|
listOutput := suite.stdout.String()
|
||||||
|
suite.NotContains(listOutput, "test1.txt", "Files should not be managed after dry-run")
|
||||||
|
suite.NotContains(listOutput, "test2.txt", "Files should not be managed after dry-run")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestDryRunOutput() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.NoError(err)
|
||||||
|
initOutput := suite.stdout.String()
|
||||||
|
suite.Contains(initOutput, "Initialized")
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFile1 := filepath.Join(suite.tempDir, "test1.txt")
|
||||||
|
testFile2 := filepath.Join(suite.tempDir, "test2.txt")
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile1, []byte("content1"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile2, []byte("content2"), 0644))
|
||||||
|
|
||||||
|
// Run add with dry-run flag
|
||||||
|
err = suite.runCommand("add", "--dry-run", testFile1, testFile2)
|
||||||
|
suite.NoError(err, "Dry-run command should succeed")
|
||||||
|
output := suite.stdout.String()
|
||||||
|
|
||||||
|
// Verify dry-run shows preview of what would be added
|
||||||
|
suite.Contains(output, "Would add", "Should show dry-run preview")
|
||||||
|
suite.Contains(output, "test1.txt", "Should show first file")
|
||||||
|
suite.Contains(output, "test2.txt", "Should show second file")
|
||||||
|
suite.Contains(output, "2 files", "Should show file count")
|
||||||
|
|
||||||
|
// Should contain helpful instructions
|
||||||
|
suite.Contains(output, "run without --dry-run", "Should provide next steps")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestDryRunRecursive() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.NoError(err)
|
||||||
|
initOutput := suite.stdout.String()
|
||||||
|
suite.Contains(initOutput, "Initialized")
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create directory structure with multiple files
|
||||||
|
configDir := filepath.Join(suite.tempDir, ".config", "test-app")
|
||||||
|
err = os.MkdirAll(configDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create files in directory
|
||||||
|
for i := 1; i <= 15; i++ {
|
||||||
|
file := filepath.Join(configDir, fmt.Sprintf("config%d.json", i))
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("config %d", i)), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run recursive add with dry-run
|
||||||
|
err = suite.runCommand("add", "--dry-run", "--recursive", configDir)
|
||||||
|
suite.NoError(err, "Dry-run recursive command should succeed")
|
||||||
|
output := suite.stdout.String()
|
||||||
|
|
||||||
|
// Verify dry-run shows all files that would be added
|
||||||
|
suite.Contains(output, "Would add", "Should show dry-run preview")
|
||||||
|
suite.Contains(output, "15 files", "Should show correct file count")
|
||||||
|
suite.Contains(output, "recursively", "Should indicate recursive mode")
|
||||||
|
|
||||||
|
// Should show some of the files
|
||||||
|
suite.Contains(output, "config1.json", "Should show first file")
|
||||||
|
suite.Contains(output, "config15.json", "Should show last file")
|
||||||
|
|
||||||
|
// Verify no actual changes were made
|
||||||
|
for i := 1; i <= 15; i++ {
|
||||||
|
file := filepath.Join(configDir, fmt.Sprintf("config%d.json", i))
|
||||||
|
info, err := os.Lstat(file)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after dry-run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 3.2: Enhanced Output and Messaging Tests
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestEnhancedSuccessOutput() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create multiple test files
|
||||||
|
testFiles := []string{
|
||||||
|
filepath.Join(suite.tempDir, "config1.txt"),
|
||||||
|
filepath.Join(suite.tempDir, "config2.txt"),
|
||||||
|
filepath.Join(suite.tempDir, "config3.txt"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, file := range testFiles {
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i+1)), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add multiple files
|
||||||
|
args := append([]string{"add"}, testFiles...)
|
||||||
|
err = suite.runCommand(args...)
|
||||||
|
suite.NoError(err)
|
||||||
|
output := suite.stdout.String()
|
||||||
|
|
||||||
|
// Should have enhanced formatting with consistent indentation
|
||||||
|
suite.Contains(output, "🔗", "Should use link icons")
|
||||||
|
suite.Contains(output, "config1.txt", "Should show first file")
|
||||||
|
suite.Contains(output, "config2.txt", "Should show second file")
|
||||||
|
suite.Contains(output, "config3.txt", "Should show third file")
|
||||||
|
|
||||||
|
// Should show organized file list
|
||||||
|
suite.Contains(output, " ", "Should have consistent indentation")
|
||||||
|
|
||||||
|
// Should include summary information
|
||||||
|
suite.Contains(output, "3 items", "Should show total count")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestOperationSummary() {
|
||||||
|
// Initialize repository
|
||||||
|
err := suite.runCommand("init")
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Create directory with files for recursive operation
|
||||||
|
configDir := filepath.Join(suite.tempDir, ".config", "test-app")
|
||||||
|
err = os.MkdirAll(configDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create files in directory
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
file := filepath.Join(configDir, fmt.Sprintf("file%d.json", i))
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("content %d", i)), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add recursively
|
||||||
|
err = suite.runCommand("add", "--recursive", configDir)
|
||||||
|
suite.NoError(err)
|
||||||
|
output := suite.stdout.String()
|
||||||
|
|
||||||
|
// Should show operation summary
|
||||||
|
suite.Contains(output, "recursively", "Should indicate operation type")
|
||||||
|
suite.Contains(output, "5", "Should show correct file count")
|
||||||
|
|
||||||
|
// Should include contextual help message
|
||||||
|
suite.Contains(output, "lnk push", "Should suggest next steps")
|
||||||
|
suite.Contains(output, "sync to remote", "Should explain next step purpose")
|
||||||
|
|
||||||
|
// Should show operation completion confirmation
|
||||||
|
suite.Contains(output, "✨", "Should use success emoji")
|
||||||
|
suite.Contains(output, "Added", "Should confirm operation completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 3.3: Documentation and Help Updates Tests
|
||||||
|
|
||||||
|
func (suite *CLITestSuite) TestUpdatedHelpText() {
|
||||||
|
// Test main help
|
||||||
|
err := suite.runCommand("help")
|
||||||
|
suite.NoError(err)
|
||||||
|
helpOutput := suite.stdout.String()
|
||||||
|
suite.stdout.Reset()
|
||||||
|
|
||||||
|
// Should mention bulk operations
|
||||||
|
suite.Contains(helpOutput, "multiple files", "Help should mention multiple file support")
|
||||||
|
|
||||||
|
// Test add command help
|
||||||
|
err = suite.runCommand("add", "--help")
|
||||||
|
suite.NoError(err)
|
||||||
|
addHelpOutput := suite.stdout.String()
|
||||||
|
|
||||||
|
// Should include new flags
|
||||||
|
suite.Contains(addHelpOutput, "--recursive", "Help should include recursive flag")
|
||||||
|
suite.Contains(addHelpOutput, "--dry-run", "Help should include dry-run flag")
|
||||||
|
|
||||||
|
// Should include examples
|
||||||
|
suite.Contains(addHelpOutput, "Examples:", "Help should include usage examples")
|
||||||
|
suite.Contains(addHelpOutput, "lnk add ~/.bashrc ~/.vimrc", "Help should show multiple file example")
|
||||||
|
suite.Contains(addHelpOutput, "lnk add --recursive ~/.config", "Help should show recursive example")
|
||||||
|
suite.Contains(addHelpOutput, "lnk add --dry-run", "Help should show dry-run example")
|
||||||
|
|
||||||
|
// Should describe what each flag does
|
||||||
|
suite.Contains(addHelpOutput, "directory contents individually", "Should explain recursive flag")
|
||||||
|
suite.Contains(addHelpOutput, "without making changes", "Should explain dry-run flag")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCLISuite(t *testing.T) {
|
func TestCLISuite(t *testing.T) {
|
||||||
suite.Run(t, new(CLITestSuite))
|
suite.Run(t, new(CLITestSuite))
|
||||||
}
|
}
|
||||||
|
@@ -1087,3 +1087,64 @@ func (l *Lnk) AddRecursive(paths []string) error {
|
|||||||
|
|
||||||
return l.AddMultiple(allFiles)
|
return l.AddMultiple(allFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreviewAdd simulates an add operation and returns files that would be affected
|
||||||
|
func (l *Lnk) PreviewAdd(paths []string, recursive bool) ([]string, error) {
|
||||||
|
var allFiles []string
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
// Get absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get absolute path for %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory
|
||||||
|
info, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to stat %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() && recursive {
|
||||||
|
// Walk directory to get all files (same logic as AddRecursive)
|
||||||
|
files, err := l.walkDirectory(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to walk directory %s: %w", path, err)
|
||||||
|
}
|
||||||
|
allFiles = append(allFiles, files...)
|
||||||
|
} else {
|
||||||
|
// It's a regular file or non-recursive directory, add it directly
|
||||||
|
allFiles = append(allFiles, absPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate files (same validation as AddMultiple but without making changes)
|
||||||
|
var validFiles []string
|
||||||
|
for _, filePath := range allFiles {
|
||||||
|
// Validate the file or directory
|
||||||
|
if err := l.fs.ValidateFileForAdd(filePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation failed for %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get relative path for tracking
|
||||||
|
relativePath, err := getRelativePath(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get relative path for %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this relative path is already managed
|
||||||
|
managedItems, err := l.getManagedItems()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get managed items: %w", err)
|
||||||
|
}
|
||||||
|
for _, item := range managedItems {
|
||||||
|
if item == relativePath {
|
||||||
|
return nil, fmt.Errorf("❌ File is already managed by lnk: \033[31m%s\033[0m", relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validFiles = append(validFiles, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return validFiles, nil
|
||||||
|
}
|
||||||
|
@@ -1338,6 +1338,100 @@ func (suite *CoreTestSuite) TestProgressThreshold() {
|
|||||||
suite.Equal(15, largeProgressCalls, "Progress should be called for operations over threshold")
|
suite.Equal(15, largeProgressCalls, "Progress should be called for operations over threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Task 3.1: Dry-Run Mode Core Tests
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestPreviewAdd() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFile1 := filepath.Join(suite.tempDir, "test1.txt")
|
||||||
|
testFile2 := filepath.Join(suite.tempDir, "test2.txt")
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile1, []byte("content1"), 0644))
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile2, []byte("content2"), 0644))
|
||||||
|
|
||||||
|
// Test PreviewAdd for multiple files
|
||||||
|
files, err := suite.lnk.PreviewAdd([]string{testFile1, testFile2}, false)
|
||||||
|
suite.Require().NoError(err, "PreviewAdd should succeed")
|
||||||
|
|
||||||
|
// Should return both files
|
||||||
|
suite.Len(files, 2, "Should preview both files")
|
||||||
|
suite.Contains(files, testFile1, "Should include first file")
|
||||||
|
suite.Contains(files, testFile2, "Should include second file")
|
||||||
|
|
||||||
|
// Verify no actual changes were made (files should still be regular files)
|
||||||
|
info, err := os.Lstat(testFile1)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after preview")
|
||||||
|
|
||||||
|
info, err = os.Lstat(testFile2)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after preview")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestPreviewAddRecursive() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create directory structure
|
||||||
|
configDir := filepath.Join(suite.tempDir, ".config", "test-app")
|
||||||
|
err = os.MkdirAll(configDir, 0755)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Create files in directory
|
||||||
|
expectedFiles := 5
|
||||||
|
var createdFiles []string
|
||||||
|
for i := 1; i <= expectedFiles; i++ {
|
||||||
|
file := filepath.Join(configDir, fmt.Sprintf("config%d.json", i))
|
||||||
|
suite.Require().NoError(os.WriteFile(file, []byte(fmt.Sprintf("config %d", i)), 0644))
|
||||||
|
createdFiles = append(createdFiles, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test PreviewAdd with recursive
|
||||||
|
files, err := suite.lnk.PreviewAdd([]string{configDir}, true)
|
||||||
|
suite.Require().NoError(err, "PreviewAdd recursive should succeed")
|
||||||
|
|
||||||
|
// Should return all files in directory
|
||||||
|
suite.Len(files, expectedFiles, "Should preview all files in directory")
|
||||||
|
|
||||||
|
// Check that all created files are included
|
||||||
|
for _, createdFile := range createdFiles {
|
||||||
|
suite.Contains(files, createdFile, "Should include file %s", createdFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no actual changes were made
|
||||||
|
for _, createdFile := range createdFiles {
|
||||||
|
info, err := os.Lstat(createdFile)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink, "File should not be symlink after preview")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CoreTestSuite) TestPreviewAddValidation() {
|
||||||
|
// Initialize lnk repository
|
||||||
|
err := suite.lnk.Init()
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test with nonexistent file
|
||||||
|
nonexistentFile := filepath.Join(suite.tempDir, "nonexistent.txt")
|
||||||
|
_, err = suite.lnk.PreviewAdd([]string{nonexistentFile}, false)
|
||||||
|
suite.Error(err, "PreviewAdd should fail for nonexistent file")
|
||||||
|
suite.Contains(err.Error(), "failed to stat", "Error should mention stat failure")
|
||||||
|
|
||||||
|
// Create and add a file first
|
||||||
|
testFile := filepath.Join(suite.tempDir, "test.txt")
|
||||||
|
suite.Require().NoError(os.WriteFile(testFile, []byte("content"), 0644))
|
||||||
|
err = suite.lnk.Add(testFile)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
// Test preview with already managed file
|
||||||
|
_, err = suite.lnk.PreviewAdd([]string{testFile}, false)
|
||||||
|
suite.Error(err, "PreviewAdd should fail for already managed file")
|
||||||
|
suite.Contains(err.Error(), "already managed", "Error should mention already managed")
|
||||||
|
}
|
||||||
|
|
||||||
func TestCoreSuite(t *testing.T) {
|
func TestCoreSuite(t *testing.T) {
|
||||||
suite.Run(t, new(CoreTestSuite))
|
suite.Run(t, new(CoreTestSuite))
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user