diff --git a/.gitignore b/.gitignore index cd6b23f..0dc20ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -# Auto-generated .gitignore by gignore: github.com/onyx-and-iris/gignore +# Generated by ignr: github.com/onyx-and-iris/ignr -### Go ### +## Go ## # If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # @@ -10,18 +10,30 @@ *.dll *.so *.dylib -bin/ # Test binary, built with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE +# Code coverage profiles and other test artifacts *.out +coverage.* +*.coverprofile +profile.cov # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work +go.work.sum -# End of gignore: github.com/onyx-and-iris/gignore +# env file +.env + +# Editor/IDE +# .idea/ +# .vscode/ + +# End of ignr + +testdata/ \ No newline at end of file diff --git a/cmd/add.go b/cmd/add.go index 0a8b3e3..1ccaf8b 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "io" + "slices" "github.com/spf13/cobra" ) @@ -15,27 +16,37 @@ var addCmd = &cobra.Command{ This is useful for excluding files or directories from version control without modifying the .gitignore file.`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - f, ok := FileFromContext(cmd.Context()) + ctx, ok := ContextObjectFromContext(cmd.Context()) if !ok { return fmt.Errorf("no exclude file found in context") } - return runAddCommand(f, args) + return runAddCommand(ctx.Out, ctx.File, args) }, } func init() { - rootCmd.AddCommand(addCmd) + RootCmd.AddCommand(addCmd) } -func runAddCommand(f io.Writer, args []string) error { - for _, pattern := range args { - if _, err := fmt.Fprintln(f, pattern); err != nil { - return fmt.Errorf("error writing to exclude file: %w", err) - } +// runAddCommand adds the specified patterns to the exclude file, ensuring no duplicates +// It handles both file and in-memory buffer cases for testing +func runAddCommand(out io.Writer, f io.ReadWriter, args []string) error { + existingPatterns, err := readExistingPatterns(f) + if err != nil { + return fmt.Errorf("error reading existing patterns: %v", err) } - fmt.Println("Patterns added to .git/info/exclude file:") + for _, pattern := range args { - fmt.Println(" -", pattern) + if slices.Contains(existingPatterns, pattern) { + fmt.Fprintf(out, "Pattern '%s' already exists in the exclude file. Skipping.\n", pattern) + continue + } + + _, err := fmt.Fprintln(f, pattern) + if err != nil { + return fmt.Errorf("error writing to exclude file: %v", err) + } + fmt.Fprintf(out, "Added pattern '%s' to the exclude file.\n", pattern) } return nil } diff --git a/cmd/add_test.go b/cmd/add_test.go new file mode 100644 index 0000000..e205e2c --- /dev/null +++ b/cmd/add_test.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "bytes" + "testing" +) + +func TestRunAddCommand(t *testing.T) { + tests := []struct { + name string + existing string + args []string + expectedOutput string + }{ + { + name: "Add new patterns", + existing: "", + args: []string{"*.log", "temp/"}, + expectedOutput: "Added pattern '*.log' to the exclude file.\nAdded pattern 'temp/' to the exclude file.\n", + }, + { + name: "Add duplicate patterns", + existing: "*.log\ntemp/\n", + args: []string{"*.log", "temp/"}, + expectedOutput: "Pattern '*.log' already exists in the exclude file. Skipping.\nPattern 'temp/' already exists in the exclude file. Skipping.\n", + }, + { + name: "Add mix of new and duplicate patterns", + existing: "*.log\n", + args: []string{"*.log", "temp/"}, + expectedOutput: "Pattern '*.log' already exists in the exclude file. Skipping.\nAdded pattern 'temp/' to the exclude file.\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + f := bytes.NewBufferString(tt.existing) + + err := runAddCommand(&buf, f, tt.args) + if err != nil { + t.Fatalf("runAddCommand returned an error: %v", err) + } + + if buf.String() != tt.expectedOutput { + t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOutput, buf.String()) + } + }) + } +} diff --git a/cmd/context.go b/cmd/context.go index 3343063..7c032ca 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -2,20 +2,27 @@ package cmd import ( "context" + "io" "os" ) type contextKey string -const fileContextKey contextKey = "excludeFile" +const contextObjectKey = contextKey("contextObject") -// ContextWithFile returns a new context with the given file set. -func ContextWithFile(ctx context.Context, file *os.File) context.Context { - return context.WithValue(ctx, fileContextKey, file) +type contextObject struct { + File *os.File + Out io.Writer } -// FileFromContext retrieves the file from the context, if it exists. -func FileFromContext(ctx context.Context) (*os.File, bool) { - file, ok := ctx.Value(fileContextKey).(*os.File) - return file, ok +func createContext(file *os.File, out io.Writer) context.Context { + return context.WithValue(context.Background(), contextObjectKey, &contextObject{ + File: file, + Out: out, + }) +} + +func ContextObjectFromContext(ctx context.Context) (*contextObject, bool) { + obj, ok := ctx.Value(contextObjectKey).(*contextObject) + return obj, ok } diff --git a/cmd/del.go b/cmd/del.go new file mode 100644 index 0000000..a33a57d --- /dev/null +++ b/cmd/del.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "fmt" + "io" + "slices" + + "github.com/spf13/cobra" +) + +// delCmd represents the del command +var delCmd = &cobra.Command{ + Use: "del", + Short: "Delete a pattern from the exclude file", + Long: `The del command removes a specified pattern from the .git/info/exclude file. +This is useful for un-excluding files or directories that were previously excluded.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, ok := ContextObjectFromContext(cmd.Context()) + if !ok { + return fmt.Errorf("no exclude file found in context") + } + if len(args) == 0 { + return fmt.Errorf("no pattern provided to delete") + } + pattern := args[0] + return runDelCommand(ctx.Out, ctx.File, pattern) + }, +} + +func init() { + RootCmd.AddCommand(delCmd) +} + +// runDelCommand deletes the specified pattern from the exclude file and writes the updated content back +// It handles both file and in-memory buffer cases for testing +func runDelCommand(out io.Writer, f any, pattern string) error { + r, ok := f.(io.Reader) + if !ok { + return fmt.Errorf("provided file does not support Reader") + } + existingPatterns, err := readExistingPatterns(r) + if err != nil { + return fmt.Errorf("error reading existing patterns: %v", err) + } + + if !slices.Contains(existingPatterns, pattern) { + fmt.Fprintf(out, "Pattern '%s' not found in the exclude file. Nothing to delete.\n", pattern) + return nil + } + + var updatedPatterns []string + for _, p := range existingPatterns { + if p != pattern { + updatedPatterns = append(updatedPatterns, p) + } + } + + var w io.Writer + if t, ok := f.(truncater); ok { + if err := t.Truncate(0); err != nil { + return fmt.Errorf("error truncating exclude file: %w", err) + } + if s, ok := f.(io.Seeker); ok { + if _, err := s.Seek(0, 0); err != nil { + return fmt.Errorf("error seeking to the beginning of exclude file: %w", err) + } + } + w, _ = f.(io.Writer) + } else if buf, ok := f.(interface{ Reset() }); ok { + buf.Reset() + w, _ = f.(io.Writer) + } else { + return fmt.Errorf("provided file does not support writing") + } + + if err := writeDefaultExcludeContent(w); err != nil { + return fmt.Errorf("error writing default exclude content: %w", err) + } + for _, p := range updatedPatterns { + if _, err := fmt.Fprintln(w, p); err != nil { + return fmt.Errorf("error writing updated patterns to exclude file: %v", err) + } + } + fmt.Fprintf(out, "Deleted pattern '%s' from the exclude file.\n", pattern) + return nil +} diff --git a/cmd/del_test.go b/cmd/del_test.go new file mode 100644 index 0000000..8724376 --- /dev/null +++ b/cmd/del_test.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "bytes" + "testing" +) + +func TestRunDelCommand(t *testing.T) { + tests := []struct { + name string + initialContent string + patternToDelete string + expectedOutput string + expectedContent string + }{ + { + name: "Delete existing pattern", + initialContent: defaultExcludeFileContent + "node_modules\n.DS_Store\n", + patternToDelete: "node_modules", + expectedOutput: defaultExcludeFileContent + ".DS_Store\n" + "Deleted pattern 'node_modules' from the exclude file.\n", + }, + { + name: "Delete non-existing pattern", + initialContent: defaultExcludeFileContent + "node_modules\n.DS_Store\n", + patternToDelete: "dist", + expectedOutput: "Pattern 'dist' not found in the exclude file. Nothing to delete.\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + buf.WriteString(tt.initialContent) + + err := runDelCommand(&buf, &buf, tt.patternToDelete) + if err != nil { + t.Fatalf("runDelCommand returned an error: %v", err) + } + + if buf.String() != tt.expectedOutput { + t.Errorf( + "Expected output and content:\n%s\nGot:\n%s", + tt.expectedOutput, + buf.String(), + ) + } + }) + } +} diff --git a/cmd/list.go b/cmd/list.go index e9765d4..8406f64 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -18,31 +18,36 @@ that are not empty and do not start with a comment (#). This is useful for reviewing which files or directories are currently excluded from version control.`, Args: cobra.NoArgs, // No arguments expected RunE: func(cmd *cobra.Command, args []string) error { - f, ok := FileFromContext(cmd.Context()) + ctx, ok := ContextObjectFromContext(cmd.Context()) if !ok { return fmt.Errorf("no exclude file found in context") } - return runListCommand(f, args) + return runListCommand(ctx.Out, ctx.File) }, } func init() { - rootCmd.AddCommand(listCmd) + RootCmd.AddCommand(listCmd) } // runListCommand is the function that will be executed when the list command is called -func runListCommand(f io.Reader, _ []string) error { - // Read from the exclude file line by line +func runListCommand(out io.Writer, f io.Reader) error { + var count int scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() if line != "" && !strings.HasPrefix(line, "#") { - fmt.Println(line) // Print each non-empty, non-comment line + fmt.Fprintln(out, line) + count++ } } if err := scanner.Err(); err != nil { return fmt.Errorf("error reading exclude file: %w", err) } + if count == 0 { + fmt.Fprintln(out, "No patterns found in the exclude file.") + } + return nil } diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..062c64e --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "bytes" + "testing" +) + +func TestRunListCommand(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Empty file", + input: defaultExcludeFileContent, + expected: "No patterns found in the exclude file.\n", + }, + { + name: "Exclude file with patterns", + input: defaultExcludeFileContent + "node_modules/\nbuild/\n# This is a comment\n", + expected: "node_modules/\nbuild/\n", + }, + { + name: "Exclude file with only comments and empty lines", + input: defaultExcludeFileContent + "# Comment 1\n# Comment 2\n\n", + expected: "No patterns found in the exclude file.\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := bytes.NewBufferString(tt.input) + var output bytes.Buffer + + err := runListCommand(&output, reader) + if err != nil { + t.Fatalf("runListCommand returned an error: %v", err) + } + + if output.String() != tt.expected { + t.Errorf("Expected output:\n%q\nGot:\n%q", tt.expected, output.String()) + } + }) + } +} diff --git a/cmd/reset.go b/cmd/reset.go index 83ad6f8..cadf429 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -3,7 +3,7 @@ package cmd import ( _ "embed" "fmt" - "os" + "io" "github.com/spf13/cobra" ) @@ -15,36 +15,42 @@ var resetCmd = &cobra.Command{ Long: `The reset command clears all patterns from the .git/info/exclude file. This is useful for starting fresh or removing all exclusions at once.`, RunE: func(cmd *cobra.Command, _ []string) error { - f, ok := FileFromContext(cmd.Context()) + ctx, ok := ContextObjectFromContext(cmd.Context()) if !ok { return fmt.Errorf("no exclude file found in context") } - return runResetCommand(f) + return resetAndWriteExcludeFile(ctx.File) }, } func init() { - rootCmd.AddCommand(resetCmd) + RootCmd.AddCommand(resetCmd) } -//go:embed template/exclude -var defaultExcludeFile string +// Truncate and seek to beginning +type truncater interface{ Truncate(size int64) error } -// runResetCommand clears the exclude file -func runResetCommand(f *os.File) error { - // Clear the exclude file by truncating it - if err := f.Truncate(0); err != nil { +// resetAndWriteExcludeFile truncates and resets the file, then writes the default content +func resetAndWriteExcludeFile(f any) error { + // Try to assert to io.ReadWriteSeeker for file operations + rws, ok := f.(io.ReadWriteSeeker) + if !ok { + // If not a file, try as io.Writer (for test buffers) + if w, ok := f.(io.Writer); ok { + return writeDefaultExcludeContent(w) + } + return fmt.Errorf("provided file does not support ReadWriteSeeker or Writer") + } + + t, ok := f.(truncater) + if !ok { + return fmt.Errorf("provided file does not support Truncate") + } + if err := t.Truncate(0); err != nil { return fmt.Errorf("error truncating exclude file: %w", err) } - // Reset the file pointer to the beginning - if _, err := f.Seek(0, 0); err != nil { + if _, err := rws.Seek(0, 0); err != nil { return fmt.Errorf("error seeking to the beginning of exclude file: %w", err) } - - // Write the default exclude patterns to the file - if _, err := f.WriteString(defaultExcludeFile); err != nil { - return fmt.Errorf("error writing default exclude file: %w", err) - } - - return nil + return writeDefaultExcludeContent(rws) } diff --git a/cmd/reset_test.go b/cmd/reset_test.go new file mode 100644 index 0000000..5b80167 --- /dev/null +++ b/cmd/reset_test.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "bytes" + "io" + "testing" +) + +func TestRunResetCommand(t *testing.T) { + var buf bytes.Buffer + + if err := resetAndWriteExcludeFile(&buf); err != nil { + t.Fatalf("resetAndWriteExcludeFile failed: %v", err) + } + + resetContent, err := io.ReadAll(&buf) + if err != nil { + t.Fatalf("failed to read from temp file: %v", err) + } + + if string(resetContent) != defaultExcludeFileContent { + t.Errorf( + "unexpected content after reset:\nGot:\n%s\nExpected:\n%s", + string(resetContent), + defaultExcludeFileContent, + ) + } +} diff --git a/cmd/root.go b/cmd/root.go index a3e1b0f..86b7c07 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,56 +1,58 @@ package cmd import ( - "context" "os" + "path/filepath" "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +const defaultExcludeFileContent = `# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ + +` + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ Use: "exclude", Short: "A command line tool to manage .git/info/exclude files", Long: `Exclude is a command line tool designed to help you manage your .git/info/exclude files. -It allows you to add, list, and remove patterns from the exclude file easily. +It allows you to add, list, and delete patterns from the exclude file easily. This tool is particularly useful for developers who want to keep their repository clean by excluding certain files or directories from version control.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { - // This function runs before any command is executed. - // You can use it to set up global configurations or checks. - // For example, you might want to check if the .git directory exists if _, err := os.Stat(".git"); os.IsNotExist(err) { cmd.Println("Error: This command must be run in a Git repository.") os.Exit(1) } - f, err := os.OpenFile(".git/info/exclude", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + path, err := cmd.Flags().GetString("path") + if err != nil { + cmd.Println("Error getting path flag:", err) + os.Exit(1) + } + + f, err := os.OpenFile(filepath.Join(path, "exclude"), os.O_RDWR|os.O_APPEND, 0644) if err != nil { cmd.Println("Error opening .git/info/exclude file:", err) os.Exit(1) } - ctx := context.WithValue(context.Background(), fileContextKey, f) + ctx := createContext(f, cmd.OutOrStdout()) cmd.SetContext(ctx) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { - if f, ok := FileFromContext(cmd.Context()); ok { - defer f.Close() + if obj, ok := ContextObjectFromContext(cmd.Context()); ok { + defer obj.File.Close() } }, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } } func init() { + RootCmd.PersistentFlags(). + StringP("path", "p", ".git/info/", "Path the exclude file resides in (default is .git/info/)") } diff --git a/cmd/template/exclude b/cmd/template/exclude deleted file mode 100644 index a5196d1..0000000 --- a/cmd/template/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/cmd/util.go b/cmd/util.go new file mode 100644 index 0000000..1a8a2ff --- /dev/null +++ b/cmd/util.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// readExistingPatterns reads the existing patterns from the exclude file, ignoring comments and empty lines +func readExistingPatterns(f io.Reader) ([]string, error) { + var patterns []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if line != "" && !strings.HasPrefix(line, "#") { + patterns = append(patterns, line) + } + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error scanning exclude file: %v", err) + } + return patterns, nil +} + +// writeDefaultExcludeContent writes the default exclude content to the writer +func writeDefaultExcludeContent(w io.Writer) error { + if _, err := w.Write([]byte(defaultExcludeFileContent)); err != nil { + return fmt.Errorf("error writing default exclude file: %w", err) + } + return nil +} diff --git a/go.mod b/go.mod index 357d826..e096d82 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,35 @@ module github.com/onyx-and-iris/exclude go 1.24.3 -require github.com/spf13/cobra v1.10.2 +require ( + github.com/charmbracelet/fang v1.0.0 + github.com/spf13/cobra v1.10.2 +) require ( + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect + github.com/charmbracelet/colorprofile v0.3.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect + github.com/charmbracelet/x/ansi v0.11.0 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.4.1 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/mango v0.1.0 // indirect + github.com/muesli/mango-cobra v1.2.0 // indirect + github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/roff v0.1.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index ef5d78d..1da224e 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,73 @@ +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= +charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= +github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= +github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ= +github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= +github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ= +github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0= +github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA= +github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU= +github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= +github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= +github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= +github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= +github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= +github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index ab28313..7fb4900 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,31 @@ package main -import "github.com/onyx-and-iris/exclude/cmd" +import ( + "context" + "os" + "runtime/debug" + "strings" + + "github.com/charmbracelet/fang" + "github.com/onyx-and-iris/exclude/cmd" +) + +var version string // Version of the CLI, set during build time func main() { - cmd.Execute() + if err := fang.Execute(context.Background(), cmd.RootCmd, fang.WithVersion(versionFromBuild())); err != nil { + os.Exit(1) + } +} + +func versionFromBuild() string { + if version != "" { + return version + } + + info, ok := debug.ReadBuildInfo() + if !ok { + return "(unable to read version)" + } + return strings.Split(info.Main.Version, "-")[0] }