Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

20 changed files with 93 additions and 721 deletions

22
.gitignore vendored
View File

@ -1,6 +1,6 @@
# Generated by ignr: github.com/onyx-and-iris/ignr # Auto-generated .gitignore by gignore: github.com/onyx-and-iris/gignore
## Go ## ### Go ###
# If you prefer the allow list template instead of the deny list, see community template: # 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 # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# #
@ -15,27 +15,13 @@ bin/
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
# Code coverage profiles and other test artifacts # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Go workspace file # Go workspace file
go.work go.work
go.work.sum
# env file # End of gignore: github.com/onyx-and-iris/gignore
.env
.envrc
# Editor/IDE
# .idea/
# .vscode/
# End of ignr
testdata/

View File

@ -13,7 +13,8 @@ builds:
goos: goos:
- linux - linux
- windows - windows
- darwin goarch:
- amd64
archives: archives:
- formats: ['tar.gz'] - formats: ['tar.gz']
@ -36,7 +37,6 @@ changelog:
exclude: exclude:
- '^docs:' - '^docs:'
- '^test:' - '^test:'
- '^chore:'
release: release:
footer: >- footer: >-

View File

@ -1,5 +0,0 @@
repos:
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.4
hooks:
- id: go-mod-tidy

View File

@ -1,62 +0,0 @@
![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white)
![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)
![macOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)
# exclude
Exclude files from local git tracking
## Install
```console
go install github.com/onyx-and-iris/exclude@latest
```
## Configuration
*flags*
- --path/-p: Path the exclude file resides in (default is .git/info/)
*environment variables*
```bash
#!/usr/bin/env bash
export EXCLUDE_PATH="./cmd/testdata/"
```
## Commands
### Add
usage: *exclude add [pattern-1] [pattern-2]...*
```console
exclude add "*.log", "temp/"
```
### Del
usage: *exclude del [pattern]*
```console
exclude del "*.log"
```
### List
```console
exclude list
```
### Reset
```console
exclude reset
```
## Special Thanks
- [spf13](https://github.com/spf13) for the [cobra](https://github.com/spf13/cobra) and [viper](https://github.com/spf13/viper) packages.
- The developers at [charmbracelet](https://github.com/charmbracelet) for the [fang](https://github.com/charmbracelet/fang) package.

View File

@ -1,61 +0,0 @@
version: '3'
vars:
PROGRAM: exclude
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'
BIN_DIR: bin
VERSION:
sh: 'git describe --tags $(git rev-list --tags --max-count=1)'
tasks:
default:
desc: Build the exclude project
cmds:
- task: build
build:
desc: Build the exclude project
deps: [vet]
cmds:
- task: build-windows
- task: build-linux
- task: build-macos
vet:
desc: Vet the code
deps: [fmt]
cmds:
- go vet ./...
fmt:
desc: Fmt the code
cmds:
- go fmt ./...
build-windows:
desc: Build the exclude project for Windows
cmds:
- GOOS=windows GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe -ldflags="-X main.version={{.VERSION}}"
internal: true
build-linux:
desc: Build the exclude project for Linux
cmds:
- GOOS=linux GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64 -ldflags="-X main.version={{.VERSION}}"
internal: true
build-macos:
desc: Build the exclude project for macOS
cmds:
- GOOS=darwin GOARCH=amd64 go build -o {{.BIN_DIR}}/{{.PROGRAM}}_darwin_amd64 -ldflags="-X main.version={{.VERSION}}"
internal: true
test:
desc: Run tests
cmds:
- go test ./...
clean:
desc: Clean the build artifacts
cmds:
- '{{.SHELL}} rm -r {{.BIN_DIR}}'

View File

@ -3,7 +3,6 @@ package cmd
import ( import (
"fmt" "fmt"
"io" "io"
"slices"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -16,37 +15,27 @@ var addCmd = &cobra.Command{
This is useful for excluding files or directories from version control without modifying the .gitignore file.`, This is useful for excluding files or directories from version control without modifying the .gitignore file.`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx, ok := ContextObjectFromContext(cmd.Context()) f, ok := FileFromContext(cmd.Context())
if !ok { if !ok {
return fmt.Errorf("no exclude file found in context") return fmt.Errorf("no exclude file found in context")
} }
return runAddCommand(ctx.Out, ctx.File, args) return runAddCommand(f, args)
}, },
} }
func init() { func init() {
RootCmd.AddCommand(addCmd) rootCmd.AddCommand(addCmd)
}
// 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)
} }
func runAddCommand(f io.Writer, args []string) error {
for _, pattern := range args { for _, pattern := range args {
if slices.Contains(existingPatterns, pattern) { if _, err := fmt.Fprintln(f, pattern); err != nil {
fmt.Fprintf(out, "Pattern '%s' already exists in the exclude file. Skipping.\n", pattern) return fmt.Errorf("error writing to exclude file: %w", err)
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) fmt.Println("Patterns added to .git/info/exclude file:")
for _, pattern := range args {
fmt.Println(" -", pattern)
} }
return nil return nil
} }

View File

@ -1,50 +0,0 @@
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())
}
})
}
}

View File

@ -2,27 +2,20 @@ package cmd
import ( import (
"context" "context"
"io"
"os" "os"
) )
type contextKey string type contextKey string
const contextObjectKey = contextKey("contextObject") const fileContextKey contextKey = "excludeFile"
type contextObject struct { // ContextWithFile returns a new context with the given file set.
File *os.File func ContextWithFile(ctx context.Context, file *os.File) context.Context {
Out io.Writer return context.WithValue(ctx, fileContextKey, file)
} }
func createContext(file *os.File, out io.Writer) context.Context { // FileFromContext retrieves the file from the context, if it exists.
return context.WithValue(context.Background(), contextObjectKey, &contextObject{ func FileFromContext(ctx context.Context) (*os.File, bool) {
File: file, file, ok := ctx.Value(fileContextKey).(*os.File)
Out: out, return file, ok
})
}
func ContextObjectFromContext(ctx context.Context) (*contextObject, bool) {
obj, ok := ctx.Value(contextObjectKey).(*contextObject)
return obj, ok
} }

View File

@ -1,86 +0,0 @@
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
}

View File

@ -1,49 +0,0 @@
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(),
)
}
})
}
}

View File

@ -18,36 +18,31 @@ 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.`, This is useful for reviewing which files or directories are currently excluded from version control.`,
Args: cobra.NoArgs, // No arguments expected Args: cobra.NoArgs, // No arguments expected
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx, ok := ContextObjectFromContext(cmd.Context()) f, ok := FileFromContext(cmd.Context())
if !ok { if !ok {
return fmt.Errorf("no exclude file found in context") return fmt.Errorf("no exclude file found in context")
} }
return runListCommand(ctx.Out, ctx.File) return runListCommand(f, args)
}, },
} }
func init() { func init() {
RootCmd.AddCommand(listCmd) rootCmd.AddCommand(listCmd)
} }
// runListCommand is the function that will be executed when the list command is called // runListCommand is the function that will be executed when the list command is called
func runListCommand(out io.Writer, f io.Reader) error { func runListCommand(f io.Reader, _ []string) error {
var count int // Read from the exclude file line by line
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if line != "" && !strings.HasPrefix(line, "#") { if line != "" && !strings.HasPrefix(line, "#") {
fmt.Fprintln(out, line) fmt.Println(line) // Print each non-empty, non-comment line
count++
} }
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading exclude file: %w", err) return fmt.Errorf("error reading exclude file: %w", err)
} }
if count == 0 {
fmt.Fprintln(out, "No patterns found in the exclude file.")
}
return nil return nil
} }

View File

@ -1,46 +0,0 @@
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())
}
})
}
}

View File

@ -3,7 +3,7 @@ package cmd
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"io" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -15,47 +15,36 @@ var resetCmd = &cobra.Command{
Long: `The reset command clears all patterns from the .git/info/exclude file. 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.`, This is useful for starting fresh or removing all exclusions at once.`,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
ctx, ok := ContextObjectFromContext(cmd.Context()) f, ok := FileFromContext(cmd.Context())
if !ok { if !ok {
return fmt.Errorf("no exclude file found in context") return fmt.Errorf("no exclude file found in context")
} }
return resetAndWriteExcludeFile(ctx.Out, ctx.File) return runResetCommand(f)
}, },
} }
func init() { func init() {
RootCmd.AddCommand(resetCmd) rootCmd.AddCommand(resetCmd)
} }
// Truncate and seek to beginning //go:embed template/exclude
type truncater interface{ Truncate(size int64) error } var defaultExcludeFile string
// resetAndWriteExcludeFile truncates and resets the file, then writes the default content // runResetCommand clears the exclude file
func resetAndWriteExcludeFile(out io.Writer, f any) error { func runResetCommand(f *os.File) error {
// Try to assert to io.ReadWriteSeeker for file operations // Clear the exclude file by truncating it
rws, ok := f.(io.ReadWriteSeeker) if err := f.Truncate(0); err != nil {
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) return fmt.Errorf("error truncating exclude file: %w", err)
} }
if _, err := rws.Seek(0, 0); err != nil { // Reset the file pointer to the beginning
if _, err := f.Seek(0, 0); err != nil {
return fmt.Errorf("error seeking to the beginning of exclude file: %w", err) return fmt.Errorf("error seeking to the beginning of exclude file: %w", err)
} }
err := writeDefaultExcludeContent(rws)
if err != nil { // Write the default exclude patterns to the file
return fmt.Errorf("error writing default exclude content: %w", err) if _, err := f.WriteString(defaultExcludeFile); err != nil {
return fmt.Errorf("error writing default exclude file: %w", err)
} }
fmt.Fprintf(out, "Exclude file reset successfully.\n")
return nil return nil
} }

View File

@ -1,28 +0,0 @@
package cmd
import (
"bytes"
"io"
"testing"
)
func TestRunResetCommand(t *testing.T) {
var buf bytes.Buffer
if err := resetAndWriteExcludeFile(&buf, &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,
)
}
}

View File

@ -1,74 +1,56 @@
package cmd package cmd
import ( import (
"fmt" "context"
"os" "os"
"path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
) )
const defaultExcludeFileContent = `# git ls-files --others --exclude-from=.git/info/exclude // rootCmd represents the base command when called without any subcommands
# Lines that start with '#' are comments. var rootCmd = &cobra.Command{
# 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", Use: "exclude",
Short: "A command line tool to manage .git/info/exclude files", 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. Long: `Exclude is a command line tool designed to help you manage your .git/info/exclude files.
It allows you to add, list, and delete patterns from the exclude file easily. It allows you to add, list, and remove patterns from the exclude file easily.
This tool is particularly useful for developers who want to keep their repository clean This tool is particularly useful for developers who want to keep their repository clean
by excluding certain files or directories from version control.`, by excluding certain files or directories from version control.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRun: func(cmd *cobra.Command, args []string) {
if strings.HasPrefix(cmd.CommandPath(), "exclude completion") || // This function runs before any command is executed.
strings.HasPrefix(cmd.CommandPath(), "exclude help") { // You can use it to set up global configurations or checks.
return nil // For example, you might want to check if the .git directory exists
}
if _, err := os.Stat(".git"); os.IsNotExist(err) { if _, err := os.Stat(".git"); os.IsNotExist(err) {
return fmt.Errorf("this command must be run in a Git repository") cmd.Println("Error: This command must be run in a Git repository.")
os.Exit(1)
} }
f, err := os.OpenFile(filepath.Join(viper.GetString("path"), "exclude"), os.O_RDWR|os.O_APPEND, 0644) f, err := os.OpenFile(".git/info/exclude", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil { if err != nil {
return fmt.Errorf("error opening exclude file: %w", err) cmd.Println("Error opening .git/info/exclude file:", err)
os.Exit(1)
} }
ctx := createContext(f, cmd.OutOrStdout()) ctx := context.WithValue(context.Background(), fileContextKey, f)
cmd.SetContext(ctx) cmd.SetContext(ctx)
return nil
}, },
PersistentPostRunE: func(cmd *cobra.Command, args []string) error { PersistentPostRun: func(cmd *cobra.Command, args []string) {
if strings.HasPrefix(cmd.CommandPath(), "exclude completion") || if f, ok := FileFromContext(cmd.Context()); ok {
strings.HasPrefix(cmd.CommandPath(), "exclude help") { defer f.Close()
return nil }
},
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
} }
if ctx, ok := ContextObjectFromContext(cmd.Context()); ok { // Execute adds all child commands to the root command and sets flags appropriately.
defer ctx.File.Close() // This is called by main.main(). It only needs to happen once to the rootCmd.
} else { func Execute() {
return fmt.Errorf("unable to retrieve context after command execution") err := rootCmd.Execute()
if err != nil {
os.Exit(1)
} }
return nil
},
} }
func init() { func init() {
RootCmd.PersistentFlags().
StringP("path", "p", ".git/info/", "Path the exclude file resides in (default is .git/info/)")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.SetEnvPrefix("EXCLUDE")
viper.AutomaticEnv()
if err := viper.BindPFlag("path", RootCmd.PersistentFlags().Lookup("path")); err != nil {
panic(fmt.Errorf("unable to bind flags: %w", err))
}
} }

6
cmd/template/exclude Normal file
View File

@ -0,0 +1,6 @@
# 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]
# *~

View File

@ -1,32 +0,0 @@
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
}

43
go.mod
View File

@ -1,45 +1,10 @@
module github.com/onyx-and-iris/exclude module github.com/onyx-and-iris/exclude
go 1.25.0 go 1.24.3
require github.com/spf13/cobra v1.9.1
require ( require (
github.com/charmbracelet/fang v1.0.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
)
require (
charm.land/lipgloss/v2 v2.0.2 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260329003944-7eda8903d971 // 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.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-cobra v1.3.0 // indirect
github.com/muesli/mango-pflag v0.2.0 // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
) )

98
go.sum
View File

@ -1,100 +1,10 @@
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
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-20260316091819-b93f6a3b8502 h1:hzWNs3UQRSUTS6YCbLaQnwqKBFXT5Yh1OOw6+26apqg=
github.com/charmbracelet/ultraviolet v0.0.0-20260316091819-b93f6a3b8502/go.mod h1:mkUCcxn9w9j89JJp3pOza5tmDQZPgIB75UfmQlFYvas=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260329003944-7eda8903d971 h1:0yC3HXybXjsyg+l2qiTSLXSCwF85fQZOZvmyAw8elLI=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260329003944-7eda8903d971/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
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.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/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.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k=
github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
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/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

28
main.go
View File

@ -1,31 +1,7 @@
package main package main
import ( import "github.com/onyx-and-iris/exclude/cmd"
"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 versionFromBuild() string {
if version != "" {
return version
}
info, ok := debug.ReadBuildInfo()
if !ok {
return "(unable to read version)"
}
return strings.Split(info.Main.Version, "-")[0]
}
func main() { func main() {
if err := fang.Execute(context.Background(), cmd.RootCmd, fang.WithVersion(versionFromBuild())); err != nil { cmd.Execute()
os.Exit(1)
}
} }