refactor(errors): implement structured error handling for improved debugging

This commit is contained in:
Yar Kravtsov
2025-06-03 07:58:21 +03:00
parent 3cba309c05
commit 1e2c9704f3
15 changed files with 482 additions and 213 deletions

218
internal/git/errors.go Normal file
View File

@@ -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
}

View File

@@ -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