first commit

This commit is contained in:
onyx-and-iris 2025-03-09 23:18:02 +00:00
commit 9ccbac7082
18 changed files with 658 additions and 0 deletions

31
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: goreleaser
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v5
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Auto-generated .gitignore by gignore: github.com/onyx-and-iris/gignore
### 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
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# End of gignore: github.com/onyx-and-iris/gignore
internal/registry/templates/*/*.gitignore

54
.goreleaser.yaml Normal file
View File

@ -0,0 +1,54 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- main: ./cmd/gignore/
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
archives:
- formats: ['tar.gz']
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: ['zip']
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
release:
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

12
CHANGELOG.md Normal file
View File

@ -0,0 +1,12 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [0.1.0] - 2025-09-03
### Added
- Initial release.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Onyx and Iris
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# Gignore - Generate .gitinore files
## Install
With [Task][task]:
```bash
task install
```
## Usage
```bash
Usage of gignore:
gignore [flags] <template>
Flags:
-dir string
directory containing .gitignore templates (default "gitignoreio")
-list
list available templates
-ls
list available templates (shorthand)
Example:
gignore go
```
## Custom Templates
It's possible to add your own custom templates, simply create a directory in `internal/registry/templates`. You'll need to rebuild the project before you can load the new templates.
Then pass the dir name as a flag, for example:
```bash
gignore -dir=custom go
```
You may set an environment variable `GIGNORE_TEMPLATE_DIR` to avoid passing the `-dir` flag each time.
If a template is requested but not found in the custom directory then the gitignoreio registry will act as a fallback.
## Special Thanks
[gitignore.io][gitignoreio] For providing such a useful .gitignore service
[gigo][gigo] For writing the Go client library for gitignore.io
[task]: https://taskfile.dev/
[gitignoreio]: https://www.toptal.com/developers/gitignore
[gigo]: https://github.com/mh-cbon/gigo
[ignore]: https://github.com/neptship/ignore

73
Taskfile.yaml Normal file
View File

@ -0,0 +1,73 @@
version: '3'
vars:
PROGRAM: gignore
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'
BIN_DIR: bin
WINDOWS: '{{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe'
LINUX: '{{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64'
GIT_COMMIT:
sh: git log -n 1 --format=%h
tasks:
default:
desc: Build the gignore project
cmds:
- task: build
build:
desc: Build the gignore project
deps: [vet]
cmds:
- task: build-windows
- task: build-linux
release:
desc: Generate the gitignore.io templates and then build the gignore project for Windows and Linux
deps: [generate]
cmds:
- task: build-windows
- task: build-linux
vet:
desc: Vet the code
deps: [fmt]
cmds:
- go vet ./...
fmt:
desc: Fmt the code
cmds:
- go fmt ./...
generate:
desc: Generate the gitignore.io templates
cmds:
- go generate .
build-windows:
desc: Build the gignore project for Windows
cmds:
- GOOS=windows GOARCH=amd64 go build -o {{.WINDOWS}} -ldflags="-X main.Version={{.GIT_COMMIT}}" ./cmd/{{.PROGRAM}}
build-linux:
desc: Build the gignore project for Linux
cmds:
- GOOS=linux GOARCH=amd64 go build -o {{.LINUX}} -ldflags="-X main.Version={{.GIT_COMMIT}}" ./cmd/{{.PROGRAM}}
test:
desc: Run tests
cmds:
- go test ./...
install:
desc: Install the gignore project
deps: [generate]
cmds:
- go install ./cmd/{{.PROGRAM}}
clean:
desc: Clean the build artifacts
cmds:
- '{{.SHELL}} rm -r {{.BIN_DIR}}'

53
cmd/gen/gen.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/cuonglm/gogi"
)
func main() {
fmt.Println("Generating gitignore.io templates...")
gogiClient, _ := gogi.NewHTTPClient()
templates, err := fetchTemplates(gogiClient)
if err != nil {
log.Fatal(err)
}
for _, template := range templates {
err := createTemplate(template)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create template %s: %v\n", template.Name, err)
}
}
}
func fetchTemplates(gogiClient *gogi.Client) (map[string]*gogi.ListJsonItem, error) {
data, err := gogiClient.ListJson()
if err != nil {
return nil, err
}
return data, nil
}
func createTemplate(template *gogi.ListJsonItem) error {
file, err := os.Create(
fmt.Sprintf("internal/registry/templates/gitignoreio/%s.gitignore", strings.ToLower(template.Name)),
)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write([]byte(template.Contents))
if err != nil {
return err
}
return nil
}

79
cmd/gignore/main.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"flag"
"fmt"
"slices"
"github.com/onyx-and-iris/gignore"
log "github.com/sirupsen/logrus"
)
func main() {
flag.Usage = func() {
w := flag.CommandLine.Output()
fmt.Fprint(w, "Usage of gignore:\n")
fmt.Fprintf(w, " gignore [flags] <template>\n")
fmt.Fprint(w, "\n")
fmt.Fprint(w, "Flags:\n")
flag.PrintDefaults()
fmt.Fprint(w, "\n")
fmt.Fprintf(w, "Example:\n")
fmt.Fprint(w, " gignore go\n")
}
var (
list bool
templateDir string
loglevel int
)
flag.BoolVar(&list, "list", false, "list available templates")
flag.BoolVar(&list, "ls", false, "list available templates (shorthand)")
flag.StringVar(
&templateDir,
"dir",
getEnv("GIGNORE_TEMPLATE_DIR", "gitignoreio"),
"directory containing .gitignore templates",
)
flag.IntVar(&loglevel, "loglevel", int(log.WarnLevel), "log level")
flag.IntVar(&loglevel, "l", int(log.WarnLevel), "log level (shorthand)")
flag.Parse()
if slices.Contains(log.AllLevels, log.Level(loglevel)) {
log.SetLevel(log.Level(loglevel))
}
client := gignore.New(gignore.WithTemplateDirectory(templateDir))
if list {
listTemplates(client)
return
}
args := flag.Args()
if len(args) != 1 {
flag.Usage()
return
}
err := client.Create(args[0])
if err != nil {
log.Fatalf("failed to create .gitignore file: %v", err)
}
fmt.Printf("√ created %s .gitignore file\n", args[0])
}
func listTemplates(client *gignore.GignoreClient) {
templates, err := client.List()
if err != nil {
log.Fatalf("failed to list templates: %v", err)
}
for _, template := range templates {
fmt.Println(template)
}
}

11
cmd/gignore/util.go Normal file
View File

@ -0,0 +1,11 @@
package main
import "os"
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if len(value) == 0 {
return defaultValue
}
return value
}

16
error.go Normal file
View File

@ -0,0 +1,16 @@
package gignore
import (
"fmt"
"github.com/onyx-and-iris/gignore/internal/registry"
)
type templateNotFoundError struct {
template string
registry *registry.TemplateRegistry
}
func (e *templateNotFoundError) Error() string {
return fmt.Sprintf("template '%s' not found in %s registry", e.template, e.registry.Directory)
}

69
gignore.go Normal file
View File

@ -0,0 +1,69 @@
package gignore
import (
"io"
log "github.com/sirupsen/logrus"
"github.com/onyx-and-iris/gignore/internal/filewriter"
"github.com/onyx-and-iris/gignore/internal/registry"
)
//go:generate go run cmd/gen/gen.go
type GignoreClient struct {
registry *registry.TemplateRegistry
writer io.Writer
}
func New(options ...Option) *GignoreClient {
gc := &GignoreClient{
registry.NewTemplateRegistry(),
filewriter.New()}
for _, option := range options {
option(gc)
}
return gc
}
func (g *GignoreClient) List() ([]string, error) {
return g.registry.ListTemplates()
}
func (g *GignoreClient) Create(template string) error {
ok, err := g.registry.Contains(template)
if err != nil {
return err
}
if !ok {
templateNotFoundErr := &templateNotFoundError{template, g.registry}
if g.registry.Directory == "gitignoreio" {
return templateNotFoundErr
}
log.Errorf("%s. Checking default registry...", templateNotFoundErr)
g.registry.Directory = "gitignoreio"
ok, err = g.registry.Contains(template)
if err != nil {
return err
}
if !ok {
return templateNotFoundErr
}
log.Infof("template '%s' found in default gitignoreio registry", template)
}
content, err := g.registry.GetTemplate(template)
if err != nil {
return err
}
_, err = g.writer.Write(content)
if err != nil {
return err
}
return nil
}

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module github.com/onyx-and-iris/gignore
go 1.24.0
require (
github.com/cuonglm/gogi v1.0.1
github.com/sirupsen/logrus v1.9.3
)
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

17
go.sum Normal file
View File

@ -0,0 +1,17 @@
github.com/cuonglm/gogi v1.0.1 h1:Jotx6uAfFK6YHFOOek37R9y3Ae9qp/nUt/3mYGCl+44=
github.com/cuonglm/gogi v1.0.1/go.mod h1:ZLU5wl3d+FSSkiYYDpmPJI2dWdAGj8q28rFjpeWv1g4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,53 @@
package filewriter
import (
"bytes"
"io"
"os"
)
type FileWriter struct {
}
func New() *FileWriter {
return &FileWriter{}
}
func (fw *FileWriter) writeContent(content []byte, dst io.Writer) (int64, error) {
r := bytes.NewReader(content)
n, err := io.Copy(dst, r)
if err != nil {
return 0, err
}
return n, nil
}
func (fw *FileWriter) Write(content []byte) (int, error) {
f, err := os.Create(".gitignore")
if err != nil {
return 0, err
}
defer f.Close()
const header = `# Auto-generated .gitignore by gignore: github.com/onyx-and-iris/gignore`
const footer = `# End of gignore: github.com/onyx-and-iris/gignore`
var sz int64
n, err := fw.writeContent([]byte(header), f)
if err != nil {
return 0, err
}
sz += n
n, err = fw.writeContent(content, f)
if err != nil {
return 0, err
}
sz += n
n, err = fw.writeContent([]byte(footer), f)
if err != nil {
return 0, err
}
return int(sz + n), nil
}

View File

@ -0,0 +1,71 @@
package registry
import (
"embed"
"errors"
"fmt"
"io/fs"
)
//go:embed templates
var templates embed.FS
type TemplateRegistry struct {
templates fs.FS
Directory string
}
func NewTemplateRegistry() *TemplateRegistry {
return &TemplateRegistry{
templates: templates,
}
}
func (t *TemplateRegistry) filePath(name string) string {
return fmt.Sprintf("templates/%s/%s.gitignore", t.Directory, name)
}
func (t *TemplateRegistry) Contains(name string) (bool, error) {
_, err := fs.Stat(t.templates, t.filePath(name))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
return true, nil
}
func (t *TemplateRegistry) GetTemplate(name string) ([]byte, error) {
data, err := fs.ReadFile(t.templates, t.filePath(name))
if err != nil {
return nil, err
}
return data, nil
}
func (t *TemplateRegistry) ListTemplates() ([]string, error) {
var paths []string
err := fs.WalkDir(
t.templates,
fmt.Sprintf("templates/%s", t.Directory),
func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
paths = append(paths, path)
}
return nil
},
)
if err != nil {
return nil, err
}
return paths, nil
}

9
option.go Normal file
View File

@ -0,0 +1,9 @@
package gignore
type Option func(*GignoreClient)
func WithTemplateDirectory(directory string) Option {
return func(g *GignoreClient) {
g.registry.Directory = directory
}
}