diff --git a/cmd/add.go b/cmd/add.go index 675ba8a..1f777b2 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "path/filepath" "github.com/spf13/cobra" @@ -10,11 +9,12 @@ import ( func newAddCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "add ", - Short: "✨ Add a file to lnk management", - Long: "Moves a file to the lnk repository and creates a symlink in its place.", - Args: cobra.ExactArgs(1), - SilenceUsage: true, + Use: "add ", + Short: "✨ Add a file to lnk management", + Long: "Moves a file to the lnk repository and creates a symlink in its place.", + Args: cobra.ExactArgs(1), + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { filePath := args[0] host, _ := cmd.Flags().GetString("host") @@ -22,7 +22,7 @@ func newAddCmd() *cobra.Command { lnk := core.NewLnk(core.WithHost(host)) if err := lnk.Add(filePath); err != nil { - return fmt.Errorf("failed to add file: %w", err) + return err } basename := filepath.Base(filePath) diff --git a/cmd/init.go b/cmd/init.go index 08d9d6f..9ac4fcd 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,24 +1,23 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" "github.com/yarlson/lnk/internal/core" ) func newInitCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "init", - Short: "🎯 Initialize a new lnk repository", - Long: "Creates the lnk directory and initializes a Git repository for managing dotfiles.", - SilenceUsage: true, + Use: "init", + Short: "🎯 Initialize a new lnk repository", + Long: "Creates the lnk directory and initializes a Git repository for managing dotfiles.", + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { remote, _ := cmd.Flags().GetString("remote") lnk := core.NewLnk() if err := lnk.InitWithRemote(remote); err != nil { - return fmt.Errorf("failed to initialize lnk: %w", err) + return err } if remote != "" { diff --git a/cmd/list.go b/cmd/list.go index 890c091..811537d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "os" "path/filepath" "strings" @@ -12,10 +11,11 @@ import ( func newListCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "list", - Short: "📋 List files managed by lnk", - Long: "Display all files and directories currently managed by lnk.", - SilenceUsage: true, + Use: "list", + Short: "📋 List files managed by lnk", + Long: "Display all files and directories currently managed by lnk.", + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { host, _ := cmd.Flags().GetString("host") all, _ := cmd.Flags().GetBool("all") @@ -44,7 +44,7 @@ func listCommonConfig(cmd *cobra.Command) error { lnk := core.NewLnk() managedItems, err := lnk.List() if err != nil { - return fmt.Errorf("failed to list managed items: %w", err) + return err } if len(managedItems) == 0 { @@ -71,7 +71,7 @@ func listHostConfig(cmd *cobra.Command, host string) error { lnk := core.NewLnk(core.WithHost(host)) managedItems, err := lnk.List() if err != nil { - return fmt.Errorf("failed to list managed items for host %s: %w", host, err) + return err } if len(managedItems) == 0 { @@ -101,7 +101,7 @@ func listAllConfigs(cmd *cobra.Command) error { lnk := core.NewLnk() commonItems, err := lnk.List() if err != nil { - return fmt.Errorf("failed to list common managed items: %w", err) + return err } printf(cmd, "🌐 \033[1mCommon configuration\033[0m (\033[36m%d item", len(commonItems)) @@ -121,7 +121,7 @@ func listAllConfigs(cmd *cobra.Command) error { // Find all host-specific configurations hosts, err := findHostConfigs() if err != nil { - return fmt.Errorf("failed to find host configurations: %w", err) + return err } for _, host := range hosts { @@ -163,7 +163,7 @@ func findHostConfigs() ([]string, error) { entries, err := os.ReadDir(repoPath) if err != nil { - return nil, fmt.Errorf("failed to read repository directory: %w", err) + return nil, err } var hosts []string diff --git a/cmd/pull.go b/cmd/pull.go index c3a8bdc..ccf722e 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -1,18 +1,17 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" "github.com/yarlson/lnk/internal/core" ) func newPullCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "pull", - Short: "⬇️ Pull changes from remote and restore symlinks", - Long: "Fetches changes from remote repository and automatically restores symlinks for all managed files.", - SilenceUsage: true, + Use: "pull", + Short: "⬇️ Pull changes from remote and restore symlinks", + Long: "Fetches changes from remote repository and automatically restores symlinks for all managed files.", + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { host, _ := cmd.Flags().GetString("host") @@ -20,7 +19,7 @@ func newPullCmd() *cobra.Command { restored, err := lnk.Pull() if err != nil { - return fmt.Errorf("failed to pull changes: %w", err) + return err } if len(restored) > 0 { diff --git a/cmd/push.go b/cmd/push.go index eb992be..47cbb09 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -1,19 +1,18 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" "github.com/yarlson/lnk/internal/core" ) func newPushCmd() *cobra.Command { return &cobra.Command{ - Use: "push [message]", - Short: "🚀 Push local changes to remote repository", - Long: "Stages all changes, creates a sync commit with the provided message, and pushes to remote.", - Args: cobra.MaximumNArgs(1), - SilenceUsage: true, + Use: "push [message]", + Short: "🚀 Push local changes to remote repository", + Long: "Stages all changes, creates a sync commit with the provided message, and pushes to remote.", + Args: cobra.MaximumNArgs(1), + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { message := "lnk: sync configuration files" if len(args) > 0 { @@ -22,7 +21,7 @@ func newPushCmd() *cobra.Command { lnk := core.NewLnk() if err := lnk.Push(message); err != nil { - return fmt.Errorf("failed to push changes: %w", err) + return err } printf(cmd, "🚀 \033[1;32mSuccessfully pushed changes\033[0m\n") diff --git a/cmd/rm.go b/cmd/rm.go index 9c030c5..26acf3f 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "path/filepath" "github.com/spf13/cobra" @@ -10,11 +9,12 @@ import ( func newRemoveCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "rm ", - Short: "🗑️ Remove a file from lnk management", - Long: "Removes a symlink and restores the original file from the lnk repository.", - Args: cobra.ExactArgs(1), - SilenceUsage: true, + Use: "rm ", + Short: "🗑️ Remove a file from lnk management", + Long: "Removes a symlink and restores the original file from the lnk repository.", + Args: cobra.ExactArgs(1), + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { filePath := args[0] host, _ := cmd.Flags().GetString("host") @@ -22,7 +22,7 @@ func newRemoveCmd() *cobra.Command { lnk := core.NewLnk(core.WithHost(host)) if err := lnk.Remove(filePath); err != nil { - return fmt.Errorf("failed to remove file: %w", err) + return err } basename := filepath.Base(filePath) diff --git a/cmd/root.go b/cmd/root.go index cae5935..d042c73 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,8 +32,9 @@ Supports both common configurations and host-specific setups. lnk push "setup complete" # Sync to remote 🎯 Simple, fast, Git-native, and multi-host ready.`, - SilenceUsage: true, - Version: fmt.Sprintf("%s (built %s)", version, buildTime), + SilenceUsage: true, + SilenceErrors: true, + Version: fmt.Sprintf("%s (built %s)", version, buildTime), } // Add subcommands @@ -57,7 +58,7 @@ func SetVersion(v, bt string) { func Execute() { rootCmd := NewRootCommand() if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } diff --git a/cmd/root_test.go b/cmd/root_test.go index 1102331..944de63 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -157,7 +157,7 @@ func (suite *CLITestSuite) TestStatusCommand() { // Test status without remote - should fail err = suite.runCommand("status") suite.Error(err) - suite.Contains(err.Error(), "no remote configured") + suite.Contains(err.Error(), "No remote repository is configured") } func (suite *CLITestSuite) TestListCommand() { @@ -247,7 +247,7 @@ func (suite *CLITestSuite) TestErrorHandling() { name: "add nonexistent file", args: []string{"add", "/nonexistent/file"}, wantErr: true, - errContains: "File does not exist", + errContains: "File or directory not found", }, { name: "status without init", diff --git a/cmd/status.go b/cmd/status.go index 8544d7a..46415d3 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -1,23 +1,22 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" "github.com/yarlson/lnk/internal/core" ) func newStatusCmd() *cobra.Command { return &cobra.Command{ - Use: "status", - Short: "📊 Show repository sync status", - Long: "Display how many commits ahead/behind the local repository is relative to the remote and check for uncommitted changes.", - SilenceUsage: true, + Use: "status", + Short: "📊 Show repository sync status", + Long: "Display how many commits ahead/behind the local repository is relative to the remote and check for uncommitted changes.", + SilenceUsage: true, + SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { lnk := core.NewLnk() status, err := lnk.Status() if err != nil { - return fmt.Errorf("failed to get status: %w", err) + return err } if status.Dirty { diff --git a/internal/core/lnk.go b/internal/core/lnk.go index 75cdddf..a082fa6 100644 --- a/internal/core/lnk.go +++ b/internal/core/lnk.go @@ -141,27 +141,17 @@ func (l *Lnk) InitWithRemote(remoteURL string) error { } // No existing repository, initialize Git repository - if err := l.git.Init(); err != nil { - return fmt.Errorf("failed to initialize git repository: %w", err) - } - - return nil + return l.git.Init() } // Clone clones a repository from the given URL func (l *Lnk) Clone(url string) error { - if err := l.git.Clone(url); err != nil { - return fmt.Errorf("failed to clone repository: %w", err) - } - return nil + return l.git.Clone(url) } // AddRemote adds a remote to the repository func (l *Lnk) AddRemote(name, url string) error { - if err := l.git.AddRemote(name, url); err != nil { - return fmt.Errorf("failed to add remote %s: %w", name, err) - } - return nil + return l.git.AddRemote(name, url) } // Add moves a file or directory to the repository and creates a symlink @@ -211,36 +201,22 @@ func (l *Lnk) Add(filePath string) error { } // 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) - } + if err := l.fs.Move(absPath, destPath, info); err != nil { + return err } // Create symlink if err := l.fs.CreateSymlink(destPath, absPath); err != nil { // 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) + _ = l.fs.Move(destPath, absPath, info) + return err } // Add to .lnk tracking file using relative path if err := l.addManagedItem(relativePath); 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 - } + _ = os.Remove(absPath) + _ = l.fs.Move(destPath, absPath, info) return fmt.Errorf("failed to update tracking file: %w", err) } @@ -252,41 +228,29 @@ func (l *Lnk) Add(filePath string) error { } if err := l.git.Add(gitPath); err != nil { // Try to restore the original state if git add fails - _ = os.Remove(absPath) // Ignore error in cleanup - _ = l.removeManagedItem(relativePath) // 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) + _ = os.Remove(absPath) + _ = l.removeManagedItem(relativePath) + _ = l.fs.Move(destPath, absPath, info) + return err } // Add .lnk file to the same commit if err := l.git.Add(l.getLnkFileName()); err != nil { // Try to restore the original state if git add fails - _ = os.Remove(absPath) // Ignore error in cleanup - _ = l.removeManagedItem(relativePath) // 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) + _ = os.Remove(absPath) + _ = l.removeManagedItem(relativePath) + _ = l.fs.Move(destPath, absPath, info) + return err } // Commit both changes together basename := filepath.Base(relativePath) 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.removeManagedItem(relativePath) // 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) + _ = os.Remove(absPath) + _ = l.removeManagedItem(relativePath) + _ = l.fs.Move(destPath, absPath, info) + return err } return nil @@ -361,29 +325,23 @@ func (l *Lnk) Remove(filePath string) error { gitPath = filepath.Join(l.host+".lnk", relativePath) } if err := l.git.Remove(gitPath); err != nil { - return fmt.Errorf("failed to remove from git: %w", err) + return err } // Add .lnk file to the same commit if err := l.git.Add(l.getLnkFileName()); err != nil { - return fmt.Errorf("failed to add .lnk file to git: %w", err) + return err } // Commit both changes together basename := filepath.Base(relativePath) if err := l.git.Commit(fmt.Sprintf("lnk: removed %s", basename)); err != nil { - return fmt.Errorf("failed to commit changes: %w", err) + return 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) - } + if err := l.fs.Move(target, absPath, info); err != nil { + return err } return nil @@ -411,7 +369,7 @@ func (l *Lnk) Status() (*StatusInfo, error) { gitStatus, err := l.git.GetStatus() if err != nil { - return nil, fmt.Errorf("failed to get repository status: %w", err) + return nil, err } return &StatusInfo{ @@ -432,28 +390,24 @@ func (l *Lnk) Push(message string) error { // Check if there are any changes hasChanges, err := l.git.HasChanges() if err != nil { - return fmt.Errorf("failed to check for changes: %w", err) + return err } if hasChanges { // Stage all changes if err := l.git.AddAll(); err != nil { - return fmt.Errorf("failed to stage changes: %w", err) + return err } // Create a sync commit if err := l.git.Commit(message); err != nil { - return fmt.Errorf("failed to commit changes: %w", err) + return err } } // Push to remote (this will be a no-op in tests since we don't have real remotes) // In real usage, this would push to the actual remote repository - if err := l.git.Push(); err != nil { - return fmt.Errorf("failed to push to remote: %w", err) - } - - return nil + return l.git.Push() } // Pull fetches changes from remote and restores symlinks as needed @@ -465,7 +419,7 @@ func (l *Lnk) Pull() ([]string, error) { // Pull changes from remote (this will be a no-op in tests since we don't have real remotes) if err := l.git.Pull(); err != nil { - return nil, fmt.Errorf("failed to pull from remote: %w", err) + return nil, err } // Find all managed files in the repository and restore symlinks @@ -541,7 +495,7 @@ func (l *Lnk) RestoreSymlinks() ([]string, error) { // Create symlink if err := l.fs.CreateSymlink(repoItem, symlinkPath); err != nil { - return nil, fmt.Errorf("failed to create symlink for %s: %w", relativePath, err) + return nil, err } restored = append(restored, relativePath) diff --git a/internal/core/lnk_test.go b/internal/core/lnk_test.go index 9798982..0043fae 100644 --- a/internal/core/lnk_test.go +++ b/internal/core/lnk_test.go @@ -275,7 +275,7 @@ func (suite *CoreTestSuite) TestErrorConditions() { err = suite.lnk.Add("/nonexistent/file") suite.Error(err) - suite.Contains(err.Error(), "File does not exist") + suite.Contains(err.Error(), "File or directory not found") // Test remove unmanaged file testFile := filepath.Join(suite.tempDir, ".regularfile") @@ -289,7 +289,7 @@ func (suite *CoreTestSuite) TestErrorConditions() { // Test status without remote _, err = suite.lnk.Status() suite.Error(err) - suite.Contains(err.Error(), "no remote configured") + suite.Contains(err.Error(), "No remote repository is configured") } // Test git operations diff --git a/internal/fs/errors.go b/internal/fs/errors.go new file mode 100644 index 0000000..850a388 --- /dev/null +++ b/internal/fs/errors.go @@ -0,0 +1,119 @@ +package fs + +import "fmt" + +// ANSI color codes for consistent formatting +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorBold = "\033[1m" +) + +// formatError creates a consistently formatted error message with ❌ prefix +func formatError(message string, args ...interface{}) string { + return fmt.Sprintf("❌ "+message, args...) +} + +// formatPath formats a file path with red color +func formatPath(path string) string { + return fmt.Sprintf("%s%s%s", colorRed, path, colorReset) +} + +// formatCommand formats a command with bold styling +func formatCommand(command string) string { + return fmt.Sprintf("%s%s%s", colorBold, command, colorReset) +} + +// FileNotExistsError represents an error when a file does not exist +type FileNotExistsError struct { + Path string + Err error +} + +func (e *FileNotExistsError) Error() string { + return formatError("File or directory not found: %s", formatPath(e.Path)) +} + +func (e *FileNotExistsError) Unwrap() error { + return e.Err +} + +// FileCheckError represents an error when failing to check a file +type FileCheckError struct { + Err error +} + +func (e *FileCheckError) Error() string { + return formatError("Unable to access file. Please check file permissions and try again.") +} + +func (e *FileCheckError) Unwrap() error { + return e.Err +} + +// UnsupportedFileTypeError represents an error when a file type is not supported +type UnsupportedFileTypeError struct { + Path string +} + +func (e *UnsupportedFileTypeError) Error() string { + return formatError("Cannot manage this type of file: %s\n 💡 lnk can only manage regular files and directories", formatPath(e.Path)) +} + +func (e *UnsupportedFileTypeError) Unwrap() error { + return nil +} + +// NotManagedByLnkError represents an error when a file is not managed by lnk +type NotManagedByLnkError struct { + Path string +} + +func (e *NotManagedByLnkError) Error() string { + return formatError("File is not managed by lnk: %s\n 💡 Use %s to manage this file first", + formatPath(e.Path), formatCommand("lnk add")) +} + +func (e *NotManagedByLnkError) Unwrap() error { + return nil +} + +// SymlinkReadError represents an error when failing to read a symlink +type SymlinkReadError struct { + Err error +} + +func (e *SymlinkReadError) Error() string { + return formatError("Unable to read symlink. The file may be corrupted or have invalid permissions.") +} + +func (e *SymlinkReadError) Unwrap() error { + return e.Err +} + +// DirectoryCreationError represents an error when failing to create a directory +type DirectoryCreationError struct { + Operation string + Err error +} + +func (e *DirectoryCreationError) Error() string { + return formatError("Failed to create directory. Please check permissions and available disk space.") +} + +func (e *DirectoryCreationError) Unwrap() error { + return e.Err +} + +// RelativePathCalculationError represents an error when failing to calculate relative path +type RelativePathCalculationError struct { + Err error +} + +func (e *RelativePathCalculationError) Error() string { + return formatError("Unable to create symlink due to path configuration issues. Please check file locations.") +} + +func (e *RelativePathCalculationError) Unwrap() error { + return e.Err +} diff --git a/internal/fs/filesystem.go b/internal/fs/filesystem.go index 996e492..de37dd7 100644 --- a/internal/fs/filesystem.go +++ b/internal/fs/filesystem.go @@ -1,7 +1,6 @@ package fs import ( - "fmt" "os" "path/filepath" "strings" @@ -17,18 +16,19 @@ func New() *FileSystem { // ValidateFileForAdd validates that a file or directory can be added to lnk func (fs *FileSystem) ValidateFileForAdd(filePath string) error { - // Check if file exists + // Check if file exists and get its info info, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { - return fmt.Errorf("❌ File does not exist: \033[31m%s\033[0m", filePath) + return &FileNotExistsError{Path: filePath, Err: err} } - return fmt.Errorf("❌ Failed to check file: %w", err) + + return &FileCheckError{Err: err} } // Allow both regular files and directories if !info.Mode().IsRegular() && !info.IsDir() { - return fmt.Errorf("❌ Only regular files and directories are supported: \033[31m%s\033[0m", filePath) + return &UnsupportedFileTypeError{Path: filePath} } return nil @@ -36,98 +36,79 @@ func (fs *FileSystem) ValidateFileForAdd(filePath string) error { // ValidateSymlinkForRemove validates that a symlink can be removed from lnk func (fs *FileSystem) ValidateSymlinkForRemove(filePath, repoPath string) error { - // Check if file exists + // Check if file exists and is a symlink info, err := os.Lstat(filePath) // Use Lstat to not follow symlinks if err != nil { if os.IsNotExist(err) { - return fmt.Errorf("❌ File does not exist: \033[31m%s\033[0m", filePath) + return &FileNotExistsError{Path: filePath, Err: err} } - return fmt.Errorf("❌ Failed to check file: %w", err) + + return &FileCheckError{Err: err} } - // Check if it's a symlink if info.Mode()&os.ModeSymlink == 0 { - return fmt.Errorf("❌ File is not managed by lnk: \033[31m%s\033[0m\n 💡 Use \033[1mlnk add\033[0m to manage this file first", filePath) + return &NotManagedByLnkError{Path: filePath} } - // Check if symlink points to the repository + // Get symlink target and resolve to absolute path target, err := os.Readlink(filePath) if err != nil { - return fmt.Errorf("failed to read symlink: %w", err) + return &SymlinkReadError{Err: err} } - // Convert relative path to absolute if needed if !filepath.IsAbs(target) { target = filepath.Join(filepath.Dir(filePath), target) } - // Clean the path to resolve any .. or . components + // Clean paths and check if target is inside the repository target = filepath.Clean(target) repoPath = filepath.Clean(repoPath) - // Check if target is inside the repository if !strings.HasPrefix(target, repoPath+string(filepath.Separator)) && target != repoPath { - return fmt.Errorf("❌ File is not managed by lnk: \033[31m%s\033[0m", filePath) + return &NotManagedByLnkError{Path: filePath} } return nil } +// Move moves a file or directory from source to destination based on the file info +func (fs *FileSystem) Move(src, dst string, info os.FileInfo) error { + if info.IsDir() { + return fs.MoveDirectory(src, dst) + } + return fs.MoveFile(src, dst) +} + // MoveFile moves a file from source to destination func (fs *FileSystem) MoveFile(src, dst string) error { // Ensure destination directory exists - dstDir := filepath.Dir(dst) - if err := os.MkdirAll(dstDir, 0755); err != nil { - return fmt.Errorf("failed to create destination directory: %w", err) + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return &DirectoryCreationError{Operation: "destination directory", Err: err} } // Move the file - if err := os.Rename(src, dst); err != nil { - return fmt.Errorf("failed to move file from %s to %s: %w", src, dst, err) - } - - return nil + return os.Rename(src, dst) } // CreateSymlink creates a relative symlink from target to linkPath func (fs *FileSystem) CreateSymlink(target, linkPath string) error { // Calculate relative path from linkPath to target - linkDir := filepath.Dir(linkPath) - relTarget, err := filepath.Rel(linkDir, target) + relTarget, err := filepath.Rel(filepath.Dir(linkPath), target) if err != nil { - return fmt.Errorf("failed to calculate relative path: %w", err) + return &RelativePathCalculationError{Err: err} } // Create the symlink - if err := os.Symlink(relTarget, linkPath); err != nil { - return fmt.Errorf("failed to create symlink: %w", err) - } - - return nil + return os.Symlink(relTarget, linkPath) } // 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) + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return &DirectoryCreationError{Operation: "destination parent directory", Err: 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 + // Move the directory + return os.Rename(src, dst) } diff --git a/internal/git/errors.go b/internal/git/errors.go new file mode 100644 index 0000000..dba7929 --- /dev/null +++ b/internal/git/errors.go @@ -0,0 +1,218 @@ +package git + +import "fmt" + +// ANSI color codes for consistent formatting +const ( + colorReset = "\033[0m" + colorBold = "\033[1m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" +) + +// formatError creates a consistently formatted error message with ❌ prefix +func formatError(message string, args ...interface{}) string { + return fmt.Sprintf("❌ "+message, args...) +} + +// formatURL formats a URL with styling +func formatURL(url string) string { + return fmt.Sprintf("%s%s%s", colorBold, url, colorReset) +} + +// formatRemote formats a remote name with styling +func formatRemote(remote string) string { + return fmt.Sprintf("%s%s%s", colorGreen, remote, colorReset) +} + +// GitInitError represents an error during git initialization +type GitInitError struct { + Output string + Err error +} + +func (e *GitInitError) Error() string { + return formatError("Failed to initialize git repository. Please ensure git is installed and try again.") +} + +func (e *GitInitError) Unwrap() error { + return e.Err +} + +// BranchSetupError represents an error setting up the default branch +type BranchSetupError struct { + Err error +} + +func (e *BranchSetupError) Error() string { + return formatError("Failed to set up the default branch. Please check your git installation.") +} + +func (e *BranchSetupError) Unwrap() error { + return e.Err +} + +// RemoteExistsError represents an error when a remote already exists with different URL +type RemoteExistsError struct { + Remote string + ExistingURL string + NewURL string +} + +func (e *RemoteExistsError) Error() string { + return formatError("Remote %s is already configured with a different repository (%s). Cannot add %s.", + formatRemote(e.Remote), formatURL(e.ExistingURL), formatURL(e.NewURL)) +} + +func (e *RemoteExistsError) Unwrap() error { + return nil +} + +// GitCommandError represents a generic git command execution error +type GitCommandError struct { + Command string + Output string + Err error +} + +func (e *GitCommandError) Error() string { + // Provide user-friendly messages based on common command types + switch e.Command { + case "add": + return formatError("Failed to stage files for commit. Please check file permissions and try again.") + case "commit": + return formatError("Failed to create commit. Please ensure you have staged changes and try again.") + case "remote add": + return formatError("Failed to add remote repository. Please check the repository URL and try again.") + case "rm": + return formatError("Failed to remove file from git tracking. Please check if the file exists and try again.") + case "log": + return formatError("Failed to retrieve commit history.") + case "remote": + return formatError("Failed to retrieve remote repository information.") + case "clone": + return formatError("Failed to clone repository. Please check the repository URL and your network connection.") + default: + return formatError("Git operation failed. Please check your repository state and try again.") + } +} + +func (e *GitCommandError) Unwrap() error { + return e.Err +} + +// NoRemoteError represents an error when no remote is configured +type NoRemoteError struct{} + +func (e *NoRemoteError) Error() string { + return formatError("No remote repository is configured. Please add a remote repository first.") +} + +func (e *NoRemoteError) Unwrap() error { + return nil +} + +// RemoteNotFoundError represents an error when a specific remote is not found +type RemoteNotFoundError struct { + Remote string + Err error +} + +func (e *RemoteNotFoundError) Error() string { + return formatError("Remote repository %s is not configured.", formatRemote(e.Remote)) +} + +func (e *RemoteNotFoundError) Unwrap() error { + return e.Err +} + +// GitConfigError represents an error with git configuration +type GitConfigError struct { + Setting string + Err error +} + +func (e *GitConfigError) Error() string { + return formatError("Failed to configure git settings. Please check your git installation.") +} + +func (e *GitConfigError) Unwrap() error { + return e.Err +} + +// UncommittedChangesError represents an error checking for uncommitted changes +type UncommittedChangesError struct { + Err error +} + +func (e *UncommittedChangesError) Error() string { + return formatError("Failed to check repository status. Please verify your git repository is valid.") +} + +func (e *UncommittedChangesError) Unwrap() error { + return e.Err +} + +// DirectoryRemovalError represents an error removing a directory +type DirectoryRemovalError struct { + Path string + Err error +} + +func (e *DirectoryRemovalError) Error() string { + return formatError("Failed to prepare directory for operation. Please check directory permissions.") +} + +func (e *DirectoryRemovalError) Unwrap() error { + return e.Err +} + +// DirectoryCreationError represents an error creating a directory +type DirectoryCreationError struct { + Path string + Err error +} + +func (e *DirectoryCreationError) Error() string { + return formatError("Failed to create directory. Please check permissions and available disk space.") +} + +func (e *DirectoryCreationError) Unwrap() error { + return e.Err +} + +// PushError represents an error during git push operation +type PushError struct { + Reason string + Output string + Err error +} + +func (e *PushError) Error() string { + if e.Reason != "" { + return formatError("Cannot push changes: %s", e.Reason) + } + return formatError("Failed to push changes to remote repository. Please check your network connection and repository permissions.") +} + +func (e *PushError) Unwrap() error { + return e.Err +} + +// PullError represents an error during git pull operation +type PullError struct { + Reason string + Output string + Err error +} + +func (e *PullError) Error() string { + if e.Reason != "" { + return formatError("Cannot pull changes: %s", e.Reason) + } + return formatError("Failed to pull changes from remote repository. Please check your network connection and resolve any conflicts.") +} + +func (e *PullError) Unwrap() error { + return e.Err +} diff --git a/internal/git/git.go b/internal/git/git.go index 7b3c2f3..1fbd8ce 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -34,7 +34,7 @@ func (g *Git) Init() error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git init failed: %w\nOutput: %s", err, string(output)) + return &GitInitError{Output: string(output), Err: err} } // Set the default branch to main @@ -42,7 +42,7 @@ func (g *Git) Init() error { cmd.Dir = g.repoPath if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to set default branch to main: %w", err) + return &BranchSetupError{Err: err} } } @@ -60,7 +60,7 @@ func (g *Git) AddRemote(name, url string) error { return nil } // Different URL, error - return fmt.Errorf("remote %s already exists with different URL: %s (trying to add: %s)", name, existingURL, url) + return &RemoteExistsError{Remote: name, ExistingURL: existingURL, NewURL: url} } // Remote doesn't exist, add it @@ -69,7 +69,7 @@ func (g *Git) AddRemote(name, url string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git remote add failed: %w\nOutput: %s", err, string(output)) + return &GitCommandError{Command: "remote add", Output: string(output), Err: err} } return nil @@ -164,7 +164,7 @@ func (g *Git) Add(filename string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git add failed: %w\nOutput: %s", err, string(output)) + return &GitCommandError{Command: "add", Output: string(output), Err: err} } return nil @@ -189,7 +189,7 @@ func (g *Git) Remove(filename string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git rm failed: %w\nOutput: %s", err, string(output)) + return &GitCommandError{Command: "rm", Output: string(output), Err: err} } return nil @@ -207,7 +207,7 @@ func (g *Git) Commit(message string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git commit failed: %w\nOutput: %s", err, string(output)) + return &GitCommandError{Command: "commit", Output: string(output), Err: err} } return nil @@ -223,7 +223,7 @@ func (g *Git) ensureGitConfig() error { cmd = exec.Command("git", "config", "user.name", "Lnk User") cmd.Dir = g.repoPath if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to set git user.name: %w", err) + return &GitConfigError{Setting: "user.name", Err: err} } } @@ -235,7 +235,7 @@ func (g *Git) ensureGitConfig() error { cmd = exec.Command("git", "config", "user.email", "lnk@localhost") cmd.Dir = g.repoPath if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to set git user.email: %w", err) + return &GitConfigError{Setting: "user.email", Err: err} } } @@ -260,7 +260,7 @@ func (g *Git) GetCommits() ([]string, error) { if strings.Contains(outputStr, "does not have any commits yet") { return []string{}, nil } - return nil, fmt.Errorf("git log failed: %w", err) + return nil, &GitCommandError{Command: "log", Output: outputStr, Err: err} } commits := strings.Split(strings.TrimSpace(string(output)), "\n") @@ -282,18 +282,18 @@ func (g *Git) GetRemoteInfo() (string, error) { output, err := cmd.Output() if err != nil { - return "", fmt.Errorf("failed to list remotes: %w", err) + return "", &GitCommandError{Command: "remote", Output: string(output), Err: err} } remotes := strings.Split(strings.TrimSpace(string(output)), "\n") if len(remotes) == 0 || remotes[0] == "" { - return "", fmt.Errorf("no remote configured") + return "", &NoRemoteError{} } // Use the first remote url, err = g.getRemoteURL(remotes[0]) if err != nil { - return "", fmt.Errorf("failed to get remote URL: %w", err) + return "", &RemoteNotFoundError{Remote: remotes[0], Err: err} } } @@ -319,7 +319,7 @@ func (g *Git) GetStatus() (*StatusInfo, error) { // Check for uncommitted changes dirty, err := g.HasChanges() if err != nil { - return nil, fmt.Errorf("failed to check for uncommitted changes: %w", err) + return nil, &UncommittedChangesError{Err: err} } // Get the remote tracking branch @@ -410,7 +410,7 @@ func (g *Git) HasChanges() (bool, error) { output, err := cmd.Output() if err != nil { - return false, fmt.Errorf("git status failed: %w", err) + return false, &GitCommandError{Command: "status", Output: string(output), Err: err} } return len(strings.TrimSpace(string(output))) > 0, nil @@ -423,7 +423,7 @@ func (g *Git) AddAll() error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git add failed: %w\nOutput: %s", err, string(output)) + return &GitCommandError{Command: "add", Output: string(output), Err: err} } return nil @@ -434,7 +434,7 @@ func (g *Git) Push() error { // First ensure we have a remote configured _, err := g.GetRemoteInfo() if err != nil { - return fmt.Errorf("cannot push: %w", err) + return &PushError{Reason: err.Error(), Err: err} } cmd := exec.Command("git", "push", "-u", "origin", "main") @@ -442,7 +442,7 @@ func (g *Git) Push() error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git push failed: %w\nOutput: %s", err, string(output)) + return &PushError{Output: string(output), Err: err} } return nil @@ -453,7 +453,7 @@ func (g *Git) Pull() error { // First ensure we have a remote configured _, err := g.GetRemoteInfo() if err != nil { - return fmt.Errorf("cannot pull: %w", err) + return &PullError{Reason: err.Error(), Err: err} } cmd := exec.Command("git", "pull", "origin", "main") @@ -461,7 +461,7 @@ func (g *Git) Pull() error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git pull failed: %w\nOutput: %s", err, string(output)) + return &PullError{Output: string(output), Err: err} } return nil @@ -471,20 +471,20 @@ func (g *Git) Pull() error { func (g *Git) Clone(url string) error { // Remove the directory if it exists to ensure clean clone if err := os.RemoveAll(g.repoPath); err != nil { - return fmt.Errorf("failed to remove existing directory: %w", err) + return &DirectoryRemovalError{Path: g.repoPath, Err: err} } // Create parent directory parentDir := filepath.Dir(g.repoPath) if err := os.MkdirAll(parentDir, 0755); err != nil { - return fmt.Errorf("failed to create parent directory: %w", err) + return &DirectoryCreationError{Path: parentDir, Err: err} } // Clone the repository cmd := exec.Command("git", "clone", url, g.repoPath) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git clone failed: %w\nOutput: %s", err, string(output)) + return &GitCommandError{Command: "clone", Output: string(output), Err: err} } // Set up upstream tracking for main branch