refactor(cmd): centralize output formatting with printf helper function

This commit is contained in:
Yar Kravtsov
2025-05-24 11:30:55 +03:00
parent fc0b567e9f
commit 3a34e4fb37
8 changed files with 367 additions and 39 deletions

View File

@@ -24,9 +24,9 @@ func newAddCmd() *cobra.Command {
} }
basename := filepath.Base(filePath) basename := filepath.Base(filePath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✨ \033[1mAdded %s to lnk\033[0m\n", basename) printf(cmd, "✨ \033[1mAdded %s to lnk\033[0m\n", basename)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s\033[0m\n", filePath, basename) printf(cmd, " 🔗 \033[90m%s\033[0m → \033[36m~/.config/lnk/%s\033[0m\n", filePath, basename)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📝 Use \033[1mlnk push\033[0m to sync to remote\n") printf(cmd, " 📝 Use \033[1mlnk push\033[0m to sync to remote\n")
return nil return nil
}, },
} }

View File

@@ -22,18 +22,18 @@ func newInitCmd() *cobra.Command {
} }
if remote != "" { if remote != "" {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🎯 \033[1mInitialized lnk repository\033[0m\n") printf(cmd, "🎯 \033[1mInitialized lnk repository\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📦 Cloned from: \033[36m%s\033[0m\n", remote) printf(cmd, " 📦 Cloned from: \033[36m%s\033[0m\n", remote)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📁 Location: \033[90m~/.config/lnk\033[0m\n") printf(cmd, " 📁 Location: \033[90m~/.config/lnk\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 \033[33mNext steps:\033[0m\n") printf(cmd, "\n💡 \033[33mNext steps:\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Run \033[1mlnk pull\033[0m to restore symlinks\n") printf(cmd, " • Run \033[1mlnk pull\033[0m to restore symlinks\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Use \033[1mlnk add <file>\033[0m to manage new files\n") printf(cmd, " • Use \033[1mlnk add <file>\033[0m to manage new files\n")
} else { } else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🎯 \033[1mInitialized empty lnk repository\033[0m\n") printf(cmd, "🎯 \033[1mInitialized empty lnk repository\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📁 Location: \033[90m~/.config/lnk\033[0m\n") printf(cmd, " 📁 Location: \033[90m~/.config/lnk\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 \033[33mNext steps:\033[0m\n") printf(cmd, "\n💡 \033[33mNext steps:\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Run \033[1mlnk add <file>\033[0m to start managing dotfiles\n") printf(cmd, " • Run \033[1mlnk add <file>\033[0m to start managing dotfiles\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " • Add a remote with: \033[1mgit remote add origin <url>\033[0m\n") printf(cmd, " • Add a remote with: \033[1mgit remote add origin <url>\033[0m\n")
} }
return nil return nil

View File

@@ -21,20 +21,20 @@ func newPullCmd() *cobra.Command {
} }
if len(restored) > 0 { if len(restored) > 0 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n") printf(cmd, "⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 🔗 Restored \033[1m%d symlink", len(restored)) printf(cmd, " 🔗 Restored \033[1m%d symlink", len(restored))
if len(restored) > 1 { if len(restored) > 1 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "s") printf(cmd, "s")
} }
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\033[0m:\n") printf(cmd, "\033[0m:\n")
for _, file := range restored { for _, file := range restored {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ✨ \033[36m%s\033[0m\n", file) printf(cmd, " ✨ \033[36m%s\033[0m\n", file)
} }
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n 🎉 Your dotfiles are synced and ready!\n") printf(cmd, "\n 🎉 Your dotfiles are synced and ready!\n")
} else { } else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n") printf(cmd, "⬇️ \033[1;32mSuccessfully pulled changes\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ✅ All symlinks already in place\n") printf(cmd, " ✅ All symlinks already in place\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 🎉 Everything is up to date!\n") printf(cmd, " 🎉 Everything is up to date!\n")
} }
return nil return nil

View File

@@ -25,10 +25,10 @@ func newPushCmd() *cobra.Command {
return fmt.Errorf("failed to push changes: %w", err) return fmt.Errorf("failed to push changes: %w", err)
} }
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🚀 \033[1;32mSuccessfully pushed changes\033[0m\n") printf(cmd, "🚀 \033[1;32mSuccessfully pushed changes\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 💾 Commit: \033[90m%s\033[0m\n", message) printf(cmd, " 💾 Commit: \033[90m%s\033[0m\n", message)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📡 Synced to remote\n") printf(cmd, " 📡 Synced to remote\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ✨ Your dotfiles are up to date!\n") printf(cmd, " ✨ Your dotfiles are up to date!\n")
return nil return nil
}, },
} }

View File

@@ -24,9 +24,9 @@ func newRemoveCmd() *cobra.Command {
} }
basename := filepath.Base(filePath) basename := filepath.Base(filePath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "🗑️ \033[1mRemoved %s from lnk\033[0m\n", basename) printf(cmd, "🗑️ \033[1mRemoved %s from lnk\033[0m\n", basename)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ↩️ \033[90m~/.config/lnk/%s\033[0m → \033[36m%s\033[0m\n", basename, filePath) printf(cmd, " ↩️ \033[90m~/.config/lnk/%s\033[0m → \033[36m%s\033[0m\n", basename, filePath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📄 Original file restored\n") printf(cmd, " 📄 Original file restored\n")
return nil return nil
}, },
} }

View File

@@ -21,32 +21,32 @@ func newStatusCmd() *cobra.Command {
} }
if status.Ahead == 0 && status.Behind == 0 { if status.Ahead == 0 && status.Behind == 0 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "✅ \033[1;32mRepository is up to date\033[0m\n") printf(cmd, "✅ \033[1;32mRepository is up to date\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📡 Synced with \033[36m%s\033[0m\n", status.Remote) printf(cmd, " 📡 Synced with \033[36m%s\033[0m\n", status.Remote)
} else { } else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "📊 \033[1mRepository Status\033[0m\n") printf(cmd, "📊 \033[1mRepository Status\033[0m\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " 📡 Remote: \033[36m%s\033[0m\n", status.Remote) printf(cmd, " 📡 Remote: \033[36m%s\033[0m\n", status.Remote)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n") printf(cmd, "\n")
if status.Ahead > 0 { if status.Ahead > 0 {
commitText := "commit" commitText := "commit"
if status.Ahead > 1 { if status.Ahead > 1 {
commitText = "commits" commitText = "commits"
} }
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText) printf(cmd, " ⬆️ \033[1;33m%d %s ahead\033[0m - ready to push\n", status.Ahead, commitText)
} }
if status.Behind > 0 { if status.Behind > 0 {
commitText := "commit" commitText := "commit"
if status.Behind > 1 { if status.Behind > 1 {
commitText = "commits" commitText = "commits"
} }
_, _ = fmt.Fprintf(cmd.OutOrStdout(), " ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText) printf(cmd, " ⬇️ \033[1;31m%d %s behind\033[0m - run \033[1mlnk pull\033[0m\n", status.Behind, commitText)
} }
if status.Ahead > 0 && status.Behind == 0 { if status.Ahead > 0 && status.Behind == 0 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 Run \033[1mlnk push\033[0m to sync your changes") printf(cmd, "\n💡 Run \033[1mlnk push\033[0m to sync your changes")
} else if status.Behind > 0 { } else if status.Behind > 0 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\n💡 Run \033[1mlnk pull\033[0m to get latest changes") printf(cmd, "\n💡 Run \033[1mlnk pull\033[0m to get latest changes")
} }
} }

12
cmd/utils.go Normal file
View File

@@ -0,0 +1,12 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// printf is a helper function to simplify output formatting in commands
func printf(cmd *cobra.Command, format string, args ...interface{}) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), format, args...)
}

316
test/integration_test.go Normal file
View File

@@ -0,0 +1,316 @@
package test
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/yarlson/lnk/internal/core"
)
type LnkIntegrationTestSuite struct {
suite.Suite
tempDir string
originalDir string
lnk *core.Lnk
}
func (suite *LnkIntegrationTestSuite) SetupTest() {
// Create temporary directory for each test
tempDir, err := os.MkdirTemp("", "lnk-test-*")
suite.Require().NoError(err)
suite.tempDir = tempDir
// Change to temp directory
originalDir, err := os.Getwd()
suite.Require().NoError(err)
suite.originalDir = originalDir
err = os.Chdir(tempDir)
suite.Require().NoError(err)
// Set XDG_CONFIG_HOME to temp directory
suite.T().Setenv("XDG_CONFIG_HOME", tempDir)
// Initialize Lnk instance
suite.lnk = core.NewLnk()
}
func (suite *LnkIntegrationTestSuite) TearDownTest() {
// Return to original directory
err := os.Chdir(suite.originalDir)
suite.Require().NoError(err)
// Clean up temp directory
err = os.RemoveAll(suite.tempDir)
suite.Require().NoError(err)
}
// Test core initialization functionality
func (suite *LnkIntegrationTestSuite) TestCoreInit() {
err := suite.lnk.Init()
suite.Require().NoError(err)
// Check that the lnk directory was created
lnkDir := filepath.Join(suite.tempDir, "lnk")
suite.DirExists(lnkDir)
// Check that Git repo was initialized
gitDir := filepath.Join(lnkDir, ".git")
suite.DirExists(gitDir)
}
// Test core add/remove functionality with files
func (suite *LnkIntegrationTestSuite) TestCoreFileOperations() {
// Initialize first
err := suite.lnk.Init()
suite.Require().NoError(err)
// Create a test file
testFile := filepath.Join(suite.tempDir, ".bashrc")
content := "export PATH=$PATH:/usr/local/bin"
err = os.WriteFile(testFile, []byte(content), 0644)
suite.Require().NoError(err)
// Add the file
err = suite.lnk.Add(testFile)
suite.Require().NoError(err)
// Verify symlink and repo file
info, err := os.Lstat(testFile)
suite.Require().NoError(err)
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
suite.FileExists(repoFile)
// Verify content is preserved
repoContent, err := os.ReadFile(repoFile)
suite.Require().NoError(err)
suite.Equal(content, string(repoContent))
// Test remove
err = suite.lnk.Remove(testFile)
suite.Require().NoError(err)
// Verify symlink is gone and regular file is restored
info, err = os.Lstat(testFile)
suite.Require().NoError(err)
suite.Equal(os.FileMode(0), info.Mode()&os.ModeSymlink) // Not a symlink
// Verify content is preserved
restoredContent, err := os.ReadFile(testFile)
suite.Require().NoError(err)
suite.Equal(content, string(restoredContent))
}
// Test core add/remove functionality with directories
func (suite *LnkIntegrationTestSuite) TestCoreDirectoryOperations() {
err := suite.lnk.Init()
suite.Require().NoError(err)
// Create a directory with files
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)
// Add the directory
err = suite.lnk.Add(testDir)
suite.Require().NoError(err)
// Verify directory is now a symlink
info, err := os.Lstat(testDir)
suite.Require().NoError(err)
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
// Verify directory exists in repo
repoDir := filepath.Join(suite.tempDir, "lnk", "testdir")
suite.DirExists(repoDir)
// Remove the directory
err = suite.lnk.Remove(testDir)
suite.Require().NoError(err)
// Verify 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
// Verify content is preserved
restoredContent, err := os.ReadFile(testFile)
suite.Require().NoError(err)
suite.Equal(content, string(restoredContent))
}
// Test .lnk file tracking functionality
func (suite *LnkIntegrationTestSuite) TestLnkFileTracking() {
err := suite.lnk.Init()
suite.Require().NoError(err)
// Add multiple items
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)
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 one item and verify tracking 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")
}
// Test XDG_CONFIG_HOME fallback
func (suite *LnkIntegrationTestSuite) TestXDGConfigHomeFallback() {
// Test fallback to ~/.config/lnk when XDG_CONFIG_HOME is not set
suite.T().Setenv("XDG_CONFIG_HOME", "")
homeDir := filepath.Join(suite.tempDir, "home")
err := os.MkdirAll(homeDir, 0755)
suite.Require().NoError(err)
suite.T().Setenv("HOME", homeDir)
lnk := core.NewLnk()
err = lnk.Init()
suite.Require().NoError(err)
// Check that the lnk directory was created under ~/.config/lnk
expectedDir := filepath.Join(homeDir, ".config", "lnk")
suite.DirExists(expectedDir)
}
// Test symlink restoration (pull functionality)
func (suite *LnkIntegrationTestSuite) TestSymlinkRestoration() {
suite.lnk.Init()
// Create a file in the repo directly (simulating a pull)
repoFile := filepath.Join(suite.tempDir, "lnk", ".bashrc")
content := "export PATH=$PATH:/usr/local/bin"
err := os.WriteFile(repoFile, []byte(content), 0644)
suite.Require().NoError(err)
// Create .lnk file to track it
lnkFile := filepath.Join(suite.tempDir, "lnk", ".lnk")
err = os.WriteFile(lnkFile, []byte(".bashrc\n"), 0644)
suite.Require().NoError(err)
// Get home directory for the test
homeDir, err := os.UserHomeDir()
suite.Require().NoError(err)
targetFile := filepath.Join(homeDir, ".bashrc")
// Clean up the test file after the test
defer func() {
_ = os.Remove(targetFile)
}()
// Test symlink restoration
restored, err := suite.lnk.RestoreSymlinks()
suite.Require().NoError(err)
// Should have restored the symlink
suite.Len(restored, 1)
suite.Equal(".bashrc", restored[0])
// Check that file is now a symlink
info, err := os.Lstat(targetFile)
suite.NoError(err)
suite.Equal(os.ModeSymlink, info.Mode()&os.ModeSymlink)
}
// Test error conditions
func (suite *LnkIntegrationTestSuite) TestErrorConditions() {
// Test add nonexistent file
err := suite.lnk.Init()
suite.Require().NoError(err)
err = suite.lnk.Add("/nonexistent/file")
suite.Error(err)
suite.Contains(err.Error(), "File does not exist")
// Test remove unmanaged file
testFile := filepath.Join(suite.tempDir, ".regularfile")
err = os.WriteFile(testFile, []byte("content"), 0644)
suite.Require().NoError(err)
err = suite.lnk.Remove(testFile)
suite.Error(err)
suite.Contains(err.Error(), "File is not managed by lnk")
// Test status without remote
_, err = suite.lnk.Status()
suite.Error(err)
suite.Contains(err.Error(), "no remote configured")
}
// Test git operations
func (suite *LnkIntegrationTestSuite) TestGitOperations() {
err := suite.lnk.Init()
suite.Require().NoError(err)
// Add a file to create a commit
testFile := filepath.Join(suite.tempDir, ".bashrc")
content := "export PATH=$PATH:/usr/local/bin"
err = os.WriteFile(testFile, []byte(content), 0644)
suite.Require().NoError(err)
err = suite.lnk.Add(testFile)
suite.Require().NoError(err)
// 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 .bashrc")
// Test add remote
err = suite.lnk.AddRemote("origin", "https://github.com/test/dotfiles.git")
suite.Require().NoError(err)
// Test status with remote
status, err := suite.lnk.Status()
suite.Require().NoError(err)
suite.Equal(1, status.Ahead)
suite.Equal(0, status.Behind)
}
func TestLnkIntegrationSuite(t *testing.T) {
suite.Run(t, new(LnkIntegrationTestSuite))
}