feat: add directory support and .lnk tracking

- Add directory management support alongside individual files

- Implement .lnk tracking file to manage files and directories

- Update filesystem validation to allow directories

- Add MoveDirectory method for handling directory operations

- Use git rm --cached to properly handle directory removal

- Add comprehensive tests for directory operations

- Update README with directory support documentation

- All 20 integration tests passing

Breaking: None (backward compatible)

Resolves: Git limitation of not tracking directories
This commit is contained in:
Yar Kravtsov
2025-05-24 08:25:34 +03:00
parent 88b3fbd238
commit d730007fb3
5 changed files with 491 additions and 65 deletions

View File

@@ -9,7 +9,7 @@ Git-native dotfiles management that won't break your setup. Zero config, zero bl
lnk init && lnk add ~/.vimrc && lnk push
```
[![Tests](https://img.shields.io/badge/tests-17%20passing-green)](./test) [![Go](https://img.shields.io/badge/go-1.21+-blue)](https://golang.org) [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-20%20passing-green)](./test) [![Go](https://img.shields.io/badge/go-1.21+-blue)](https://golang.org) [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
## Why Lnk?
@@ -22,8 +22,9 @@ While chezmoi offers 100+ features and Home Manager requires learning Nix, **Lnk
-**Zero friction**: No YAML configs, no templates, no learning curve
- 🔧 **Git-native**: Clean commits, standard workflow, no abstractions
- 📦 **Zero dependencies**: Single binary vs Python/Node/Ruby runtimes
- 🚀 **Production ready**: 17 integration tests, proper error handling
- 🚀 **Production ready**: 20 integration tests, proper error handling
- 🔄 **Smart sync**: Built-in status tracking and seamless multi-machine workflow
- 📁 **Directory support**: Manage entire config directories or individual files
**The market gap**: Tools are either too simple (and unsafe) or too complex (and overwhelming). Lnk is the **Goldilocks solution** just right for developers who want reliability without complexity.
@@ -98,10 +99,11 @@ lnk init -r git@github.com:username/dotfiles.git # With remote
- ✅ Protects existing repositories from overwrite
- ✅ Validates remote conflicts before changes
### Manage Files
### Manage Files & Directories
```bash
lnk add ~/.bashrc ~/.vimrc ~/.tmux.conf # Add multiple files
lnk add ~/.config/nvim ~/.ssh # Add entire directories
lnk rm ~/.bashrc # Remove from management
```
@@ -246,7 +248,7 @@ internal/
### What Makes It Robust
- **17 integration tests** covering edge cases and error conditions
- **20 integration tests** covering edge cases and error conditions
- **Zero external dependencies** at runtime
- **Atomic operations** with automatic rollback on failure
- **Relative symlinks** for cross-platform compatibility

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/yarlson/lnk/internal/fs"
"github.com/yarlson/lnk/internal/git"
@@ -76,9 +78,9 @@ func (l *Lnk) AddRemote(name, url string) error {
return nil
}
// Add moves a file to the repository and creates a symlink
// Add moves a file or directory to the repository and creates a symlink
func (l *Lnk) Add(filePath string) error {
// Validate the file
// Validate the file or directory
if err := l.fs.ValidateFileForAdd(filePath); err != nil {
return err
}
@@ -93,30 +95,89 @@ func (l *Lnk) Add(filePath string) error {
basename := filepath.Base(absPath)
destPath := filepath.Join(l.repoPath, basename)
// Move file to repository
if err := l.fs.MoveFile(absPath, destPath); err != nil {
return fmt.Errorf("failed to move file to repository: %w", err)
// Check if it's a directory or file
info, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("failed to stat path: %w", err)
}
// Move to repository (handles both files and directories)
if info.IsDir() {
if err := l.fs.MoveDirectory(absPath, destPath); err != nil {
return fmt.Errorf("failed to move directory to repository: %w", err)
}
} else {
if err := l.fs.MoveFile(absPath, destPath); err != nil {
return fmt.Errorf("failed to move file to repository: %w", err)
}
}
// Create symlink
if err := l.fs.CreateSymlink(destPath, absPath); err != nil {
// Try to restore the file if symlink creation fails
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
// Try to restore the original if symlink creation fails
if info.IsDir() {
_ = l.fs.MoveDirectory(destPath, absPath) // Ignore error in cleanup
} else {
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
}
return fmt.Errorf("failed to create symlink: %w", err)
}
// Stage and commit the file
if err := l.git.AddAndCommit(basename, fmt.Sprintf("lnk: added %s", basename)); err != nil {
// Add to .lnk tracking file
if err := l.addManagedItem(absPath); err != nil {
// Try to restore the original state if tracking fails
_ = os.Remove(absPath) // Ignore error in cleanup
if info.IsDir() {
_ = l.fs.MoveDirectory(destPath, absPath) // Ignore error in cleanup
} else {
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
}
return fmt.Errorf("failed to update tracking file: %w", err)
}
// Add both the item and .lnk file to git in a single commit
if err := l.git.Add(basename); err != nil {
// Try to restore the original state if git add fails
_ = os.Remove(absPath) // Ignore error in cleanup
_ = l.removeManagedItem(absPath) // Ignore error in cleanup
if info.IsDir() {
_ = l.fs.MoveDirectory(destPath, absPath) // Ignore error in cleanup
} else {
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
}
return fmt.Errorf("failed to add item to git: %w", err)
}
// Add .lnk file to the same commit
if err := l.git.Add(".lnk"); err != nil {
// Try to restore the original state if git add fails
_ = os.Remove(absPath) // Ignore error in cleanup
_ = l.removeManagedItem(absPath) // Ignore error in cleanup
if info.IsDir() {
_ = l.fs.MoveDirectory(destPath, absPath) // Ignore error in cleanup
} else {
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
}
return fmt.Errorf("failed to add .lnk file to git: %w", err)
}
// Commit both changes together
if err := l.git.Commit(fmt.Sprintf("lnk: added %s", basename)); err != nil {
// Try to restore the original state if commit fails
_ = os.Remove(absPath) // Ignore error in cleanup
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
_ = os.Remove(absPath) // Ignore error in cleanup
_ = l.removeManagedItem(absPath) // Ignore error in cleanup
if info.IsDir() {
_ = l.fs.MoveDirectory(destPath, absPath) // Ignore error in cleanup
} else {
_ = l.fs.MoveFile(destPath, absPath) // Ignore error in cleanup
}
return fmt.Errorf("failed to commit changes: %w", err)
}
return nil
}
// Remove removes a symlink and restores the original file
// Remove removes a symlink and restores the original file or directory
func (l *Lnk) Remove(filePath string) error {
// Get absolute path
absPath, err := filepath.Abs(filePath)
@@ -142,24 +203,48 @@ func (l *Lnk) Remove(filePath string) error {
basename := filepath.Base(target)
// Check if target is a directory or file
info, err := os.Stat(target)
if err != nil {
return fmt.Errorf("failed to stat target: %w", err)
}
// Remove the symlink
if err := os.Remove(absPath); err != nil {
return fmt.Errorf("failed to remove symlink: %w", err)
}
// Move file back from repository
if err := l.fs.MoveFile(target, absPath); err != nil {
return fmt.Errorf("failed to restore file: %w", err)
// Remove from .lnk tracking file
if err := l.removeManagedItem(absPath); err != nil {
return fmt.Errorf("failed to update tracking file: %w", err)
}
// Remove from Git and commit
if err := l.git.RemoveAndCommit(basename, fmt.Sprintf("lnk: removed %s", basename)); err != nil {
// Try to restore the symlink if commit fails
_ = l.fs.MoveFile(absPath, target) // Ignore error in cleanup
_ = l.fs.CreateSymlink(target, absPath) // Ignore error in cleanup
// Remove from Git first (while the item is still in the repository)
if err := l.git.Remove(basename); err != nil {
return fmt.Errorf("failed to remove from git: %w", err)
}
// Add .lnk file to the same commit
if err := l.git.Add(".lnk"); err != nil {
return fmt.Errorf("failed to add .lnk file to git: %w", err)
}
// Commit both changes together
if err := l.git.Commit(fmt.Sprintf("lnk: removed %s", basename)); err != nil {
return fmt.Errorf("failed to commit changes: %w", err)
}
// Move back from repository (handles both files and directories)
if info.IsDir() {
if err := l.fs.MoveDirectory(target, absPath); err != nil {
return fmt.Errorf("failed to restore directory: %w", err)
}
} else {
if err := l.fs.MoveFile(target, absPath); err != nil {
return fmt.Errorf("failed to restore file: %w", err)
}
}
return nil
}
@@ -249,30 +334,24 @@ func (l *Lnk) Pull() ([]string, error) {
return restored, nil
}
// RestoreSymlinks finds all files in the repository and ensures they have proper symlinks
// RestoreSymlinks finds all managed items from .lnk file and ensures they have proper symlinks
func (l *Lnk) RestoreSymlinks() ([]string, error) {
var restored []string
// Read all files in the repository
entries, err := os.ReadDir(l.repoPath)
// Get managed items from .lnk file
managedItems, err := l.getManagedItems()
if err != nil {
return nil, fmt.Errorf("failed to read repository directory: %w", err)
return nil, fmt.Errorf("failed to get managed items: %w", err)
}
for _, entry := range entries {
// Skip hidden files and directories (like .git)
if entry.Name()[0] == '.' {
continue
}
for _, itemName := range managedItems {
repoItem := filepath.Join(l.repoPath, itemName)
// Skip directories
if entry.IsDir() {
continue
// Check if item exists in repository
if _, err := os.Stat(repoItem); os.IsNotExist(err) {
continue // Skip missing items
}
filename := entry.Name()
repoFile := filepath.Join(l.repoPath, filename)
// Determine where the symlink should be
// For config files, we'll place them in the user's home directory
homeDir, err := os.UserHomeDir()
@@ -280,26 +359,26 @@ func (l *Lnk) RestoreSymlinks() ([]string, error) {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
symlinkPath := filepath.Join(homeDir, filename)
symlinkPath := filepath.Join(homeDir, itemName)
// Check if symlink already exists and is correct
if l.isValidSymlink(symlinkPath, repoFile) {
if l.isValidSymlink(symlinkPath, repoItem) {
continue
}
// Remove existing file/symlink if it exists
if _, err := os.Lstat(symlinkPath); err == nil {
if err := os.Remove(symlinkPath); err != nil {
return nil, fmt.Errorf("failed to remove existing file %s: %w", symlinkPath, err)
if err := os.RemoveAll(symlinkPath); err != nil {
return nil, fmt.Errorf("failed to remove existing item %s: %w", symlinkPath, err)
}
}
// Create symlink
if err := l.fs.CreateSymlink(repoFile, symlinkPath); err != nil {
return nil, fmt.Errorf("failed to create symlink for %s: %w", filename, err)
if err := l.fs.CreateSymlink(repoItem, symlinkPath); err != nil {
return nil, fmt.Errorf("failed to create symlink for %s: %w", itemName, err)
}
restored = append(restored, filename)
restored = append(restored, itemName)
}
return restored, nil
@@ -341,3 +420,99 @@ func (l *Lnk) isValidSymlink(symlinkPath, expectedTarget string) bool {
return targetAbs == expectedAbs
}
// getManagedItems returns the list of managed files and directories from .lnk file
func (l *Lnk) getManagedItems() ([]string, error) {
lnkFile := filepath.Join(l.repoPath, ".lnk")
// If .lnk file doesn't exist, return empty list
if _, err := os.Stat(lnkFile); os.IsNotExist(err) {
return []string{}, nil
}
content, err := os.ReadFile(lnkFile)
if err != nil {
return nil, fmt.Errorf("failed to read .lnk file: %w", err)
}
if len(content) == 0 {
return []string{}, nil
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
var items []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
items = append(items, line)
}
}
return items, nil
}
// addManagedItem adds an item to the .lnk tracking file
func (l *Lnk) addManagedItem(itemPath string) error {
// Get current items
items, err := l.getManagedItems()
if err != nil {
return fmt.Errorf("failed to get managed items: %w", err)
}
// Get the basename for storage
basename := filepath.Base(itemPath)
// Check if already exists
for _, item := range items {
if item == basename {
return nil // Already managed
}
}
// Add new item
items = append(items, basename)
// Sort for consistent ordering
sort.Strings(items)
return l.writeManagedItems(items)
}
// removeManagedItem removes an item from the .lnk tracking file
func (l *Lnk) removeManagedItem(itemPath string) error {
// Get current items
items, err := l.getManagedItems()
if err != nil {
return fmt.Errorf("failed to get managed items: %w", err)
}
// Get the basename for removal
basename := filepath.Base(itemPath)
// Remove item
var newItems []string
for _, item := range items {
if item != basename {
newItems = append(newItems, item)
}
}
return l.writeManagedItems(newItems)
}
// writeManagedItems writes the list of managed items to .lnk file
func (l *Lnk) writeManagedItems(items []string) error {
lnkFile := filepath.Join(l.repoPath, ".lnk")
content := strings.Join(items, "\n")
if len(items) > 0 {
content += "\n"
}
err := os.WriteFile(lnkFile, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write .lnk file: %w", err)
}
return nil
}

View File

@@ -15,7 +15,7 @@ func New() *FileSystem {
return &FileSystem{}
}
// ValidateFileForAdd validates that a file can be added to lnk
// ValidateFileForAdd validates that a file or directory can be added to lnk
func (fs *FileSystem) ValidateFileForAdd(filePath string) error {
// Check if file exists
info, err := os.Stat(filePath)
@@ -26,14 +26,9 @@ func (fs *FileSystem) ValidateFileForAdd(filePath string) error {
return fmt.Errorf("failed to stat file: %w", err)
}
// Check if it's a directory
if info.IsDir() {
return fmt.Errorf("directories are not supported: %s", filePath)
}
// Check if it's a regular file
if !info.Mode().IsRegular() {
return fmt.Errorf("only regular files are supported: %s", filePath)
// Allow both regular files and directories
if !info.Mode().IsRegular() && !info.IsDir() {
return fmt.Errorf("only regular files and directories are supported: %s", filePath)
}
return nil
@@ -110,3 +105,29 @@ func (fs *FileSystem) CreateSymlink(target, linkPath string) error {
return nil
}
// MoveDirectory moves a directory from source to destination recursively
func (fs *FileSystem) MoveDirectory(src, dst string) error {
// Check if source is a directory
info, err := os.Stat(src)
if err != nil {
return fmt.Errorf("failed to stat source: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("source is not a directory: %s", src)
}
// Ensure destination parent directory exists
dstParent := filepath.Dir(dst)
if err := os.MkdirAll(dstParent, 0755); err != nil {
return fmt.Errorf("failed to create destination parent directory: %w", err)
}
// Use os.Rename which works for directories
if err := os.Rename(src, dst); err != nil {
return fmt.Errorf("failed to move directory from %s to %s: %w", src, dst, err)
}
return nil
}

View File

@@ -130,7 +130,7 @@ func (g *Git) IsLnkRepository() bool {
// AddAndCommit stages a file and commits it
func (g *Git) AddAndCommit(filename, message string) error {
// Stage the file
if err := g.add(filename); err != nil {
if err := g.Add(filename); err != nil {
return err
}
@@ -145,7 +145,7 @@ func (g *Git) AddAndCommit(filename, message string) error {
// RemoveAndCommit removes a file from Git and commits the change
func (g *Git) RemoveAndCommit(filename, message string) error {
// Remove the file from Git
if err := g.remove(filename); err != nil {
if err := g.Remove(filename); err != nil {
return err
}
@@ -157,8 +157,8 @@ func (g *Git) RemoveAndCommit(filename, message string) error {
return nil
}
// add stages a file
func (g *Git) add(filename string) error {
// Add stages a file
func (g *Git) Add(filename string) error {
cmd := exec.Command("git", "add", filename)
cmd.Dir = g.repoPath
@@ -170,9 +170,21 @@ func (g *Git) add(filename string) error {
return nil
}
// remove removes a file from Git tracking
func (g *Git) remove(filename string) error {
cmd := exec.Command("git", "rm", filename)
// Remove removes a file from Git tracking
func (g *Git) Remove(filename string) error {
// Check if it's a directory that needs -r flag
fullPath := filepath.Join(g.repoPath, filename)
info, err := os.Stat(fullPath)
var cmd *exec.Cmd
if err == nil && info.IsDir() {
// Use -r and --cached flags for directories (only remove from git, not filesystem)
cmd = exec.Command("git", "rm", "-r", "--cached", filename)
} else {
// Regular file (only remove from git, not filesystem)
cmd = exec.Command("git", "rm", "--cached", filename)
}
cmd.Dir = g.repoPath
output, err := cmd.CombinedOutput()

View File

@@ -169,14 +169,230 @@ func (suite *LnkIntegrationTestSuite) TestAddDirectory() {
err := suite.lnk.Init()
suite.Require().NoError(err)
// Create a directory
// Create a directory with files
testDir := filepath.Join(suite.tempDir, "testdir")
err = os.MkdirAll(testDir, 0755)
suite.Require().NoError(err)
// Add files to the directory
testFile1 := filepath.Join(testDir, "file1.txt")
err = os.WriteFile(testFile1, []byte("content1"), 0644)
suite.Require().NoError(err)
testFile2 := filepath.Join(testDir, "file2.txt")
err = os.WriteFile(testFile2, []byte("content2"), 0644)
suite.Require().NoError(err)
// Add the directory - should now succeed
err = suite.lnk.Add(testDir)
suite.Error(err)
suite.Contains(err.Error(), "directories are not supported")
suite.Require().NoError(err)
// Check that the directory is now a symlink
info, err := os.Lstat(testDir)
suite.Require().NoError(err)
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
// Check that the directory exists in the repo
repoDir := filepath.Join(suite.tempDir, "lnk", "testdir")
suite.DirExists(repoDir)
// Check that files are preserved
repoFile1 := filepath.Join(repoDir, "file1.txt")
repoFile2 := filepath.Join(repoDir, "file2.txt")
suite.FileExists(repoFile1)
suite.FileExists(repoFile2)
content1, err := os.ReadFile(repoFile1)
suite.Require().NoError(err)
suite.Equal("content1", string(content1))
content2, err := os.ReadFile(repoFile2)
suite.Require().NoError(err)
suite.Equal("content2", string(content2))
// Check that .lnk file was created and contains the directory
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
suite.FileExists(lnkFile)
lnkContent, err := os.ReadFile(lnkFile)
suite.Require().NoError(err)
suite.Contains(string(lnkContent), "testdir")
// Check that Git commit was made
commits, err := suite.lnk.GetCommits()
suite.Require().NoError(err)
suite.Len(commits, 1)
suite.Contains(commits[0], "lnk: added testdir")
}
func (suite *LnkIntegrationTestSuite) TestRemoveDirectory() {
// Initialize and add a directory first
err := suite.lnk.Init()
suite.Require().NoError(err)
testDir := filepath.Join(suite.tempDir, "testdir")
err = os.MkdirAll(testDir, 0755)
suite.Require().NoError(err)
testFile := filepath.Join(testDir, "config.txt")
content := "test config"
err = os.WriteFile(testFile, []byte(content), 0644)
suite.Require().NoError(err)
err = suite.lnk.Add(testDir)
suite.Require().NoError(err)
// Now remove the directory
err = suite.lnk.Remove(testDir)
suite.Require().NoError(err)
// Check that the symlink is gone and regular directory is restored
info, err := os.Lstat(testDir)
suite.Require().NoError(err)
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
suite.True(info.IsDir()) // Is a directory
// Check that content is preserved
restoredContent, err := os.ReadFile(testFile)
suite.Require().NoError(err)
suite.Equal(content, string(restoredContent))
// Check that directory is removed from repo
repoDir := filepath.Join(suite.tempDir, "lnk", "testdir")
suite.NoDirExists(repoDir)
// Check that .lnk file no longer contains the directory
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
if suite.FileExists(lnkFile) {
lnkContent, err := os.ReadFile(lnkFile)
suite.Require().NoError(err)
suite.NotContains(string(lnkContent), "testdir")
}
// Check that Git commit was made
commits, err := suite.lnk.GetCommits()
suite.Require().NoError(err)
suite.Len(commits, 2) // add + remove
suite.Contains(commits[0], "lnk: removed testdir")
suite.Contains(commits[1], "lnk: added testdir")
}
func (suite *LnkIntegrationTestSuite) TestLnkFileTracking() {
err := suite.lnk.Init()
suite.Require().NoError(err)
// Add a file
testFile := filepath.Join(suite.tempDir, ".bashrc")
err = os.WriteFile(testFile, []byte("export PATH=/usr/local/bin:$PATH"), 0644)
suite.Require().NoError(err)
err = suite.lnk.Add(testFile)
suite.Require().NoError(err)
// Add a directory
testDir := filepath.Join(suite.tempDir, ".ssh")
err = os.MkdirAll(testDir, 0700)
suite.Require().NoError(err)
configFile := filepath.Join(testDir, "config")
err = os.WriteFile(configFile, []byte("Host example.com"), 0600)
suite.Require().NoError(err)
err = suite.lnk.Add(testDir)
suite.Require().NoError(err)
// Check .lnk file contains both entries
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
suite.FileExists(lnkFile)
lnkContent, err := os.ReadFile(lnkFile)
suite.Require().NoError(err)
lines := strings.Split(strings.TrimSpace(string(lnkContent)), "\n")
suite.Len(lines, 2)
suite.Contains(lines, ".bashrc")
suite.Contains(lines, ".ssh")
// Remove a file and check .lnk is updated
err = suite.lnk.Remove(testFile)
suite.Require().NoError(err)
lnkContent, err = os.ReadFile(lnkFile)
suite.Require().NoError(err)
lines = strings.Split(strings.TrimSpace(string(lnkContent)), "\n")
suite.Len(lines, 1)
suite.Contains(lines, ".ssh")
suite.NotContains(lines, ".bashrc")
}
func (suite *LnkIntegrationTestSuite) TestPullWithDirectories() {
// Initialize repo
err := suite.lnk.Init()
suite.Require().NoError(err)
// Add remote for pull to work
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
suite.Require().NoError(err)
// Create a directory and .lnk file in the repo directly to simulate a pull
repoDir := filepath.Join(suite.tempDir, "lnk", ".config")
err = os.MkdirAll(repoDir, 0755)
suite.Require().NoError(err)
configFile := filepath.Join(repoDir, "app.conf")
content := "setting=value"
err = os.WriteFile(configFile, []byte(content), 0644)
suite.Require().NoError(err)
// Create .lnk file
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
err = os.WriteFile(lnkFile, []byte(".config\n"), 0644)
suite.Require().NoError(err)
// Get home directory for the test
homeDir, err := os.UserHomeDir()
suite.Require().NoError(err)
targetDir := filepath.Join(homeDir, ".config")
// Clean up the test directory after the test
defer func() {
_ = os.RemoveAll(targetDir)
}()
// Create a regular directory in home to simulate conflict scenario
err = os.MkdirAll(targetDir, 0755)
suite.Require().NoError(err)
err = os.WriteFile(filepath.Join(targetDir, "different.conf"), []byte("different"), 0644)
suite.Require().NoError(err)
// Pull should restore symlinks and handle conflicts
restored, err := suite.lnk.Pull()
// In tests, pull will fail because we don't have real remotes, but that's expected
// We can still test the symlink restoration part
if err != nil {
suite.Contains(err.Error(), "git pull failed")
// Test symlink restoration directly
restored, err = suite.lnk.RestoreSymlinks()
suite.Require().NoError(err)
}
// Should have restored the symlink
suite.GreaterOrEqual(len(restored), 1)
if len(restored) > 0 {
suite.Equal(".config", restored[0])
}
// Check that directory is back to being a symlink
info, err := os.Lstat(targetDir)
suite.Require().NoError(err)
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
// Check content is preserved from repo
repoContent, err := os.ReadFile(configFile)
suite.Require().NoError(err)
suite.Equal(content, string(repoContent))
}
func (suite *LnkIntegrationTestSuite) TestRemoveNonSymlink() {