diff --git a/file_iter.go b/file_iter.go deleted file mode 100644 index bf3124d..0000000 --- a/file_iter.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "os" - "path/filepath" -) - -func getGitignores(sourceDir string) ([]string, error) { - return filepath.Glob(filepath.Join(sourceDir, "*.gitignore")) -} - -func readFile(path string) string { - res, err := os.ReadFile(path) - handleErr(err) - return string(res) -} diff --git a/gi_gen.go b/gi_gen.go index 9a158c4..ac09323 100644 --- a/gi_gen.go +++ b/gi_gen.go @@ -1,72 +1,83 @@ package main import ( + "fmt" "log" "os" "path/filepath" - "runtime" "strings" - // "github.com/tcnksm/go-input" -) -// UNUSED allows unused variables to be included in Go programs -func UNUSED(x ...interface{}) {} + "github.com/AlecAivazis/survey/v2" + "golang.org/x/exp/maps" +) // create line cache map for skipping previously checked glob lines var lineCache = make(map[string]bool) func main() { - var callerSkip = 0 // might need to be 1 in release mode? - _, filename, _, _ := runtime.Caller(callerSkip) - binDir := filepath.Dir(filename) - sourceDir := filepath.Join(binDir, ".github.gitignore") - wd, _ := os.Getwd() + allFiles, err := prepareGitignores() - log.Println(binDir) - - if !fileExists(sourceDir) { - log.Println("Getting gitignore files...") - runCmd("git", "clone", "--depth=1", repoUrl, sourceDir) - } - - if getNeedsUpdate() { - log.Println("Updating gitignore files...") - runCmd("git", "pull", "origin", "master") - } - - allFiles, err := getGitignores(sourceDir) handleErr(err) - keep := []string{} + files := make(map[string]string) + fileNames := []string{} - log.Println("Found:") - for _, v := range allFiles { - log.Println("Parsing file: " + v) - contents := readFile(v) - lines := strings.Split(contents, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - _, exists := lineCache[trimmed] - if exists { - continue - } - lineCache[trimmed] = true - if len(trimmed) == 0 || strings.ToLower(string(trimmed[0])) == "#" { - lineCache[trimmed] = false - continue - } - log.Println("Parsing line: " + line) - if globExists(filepath.Join(wd, line)) { - keep = append(keep, line) - } + for _, filename := range allFiles { + contents := readFile(filename) + basename := filepath.Base(filename) + langName := basename[:strings.Index(basename, ".")] + + if findFileMatches(contents) { + sep := "#========================================================================\n" + header := fmt.Sprintf(sep+"# %s\n"+sep+"\n", basename) + files[langName] = header + contents + fileNames = append(fileNames, langName) } } - log.Println("Done") + log.Println("Done.") - log.Println("Final output:") - for _, v := range keep { - log.Println(v) + selected := []string{} + keys := maps.Keys(files) + + if len(keys) > 1 { + prompt := &survey.MultiSelect{ + Message: "Found matches in your project for the following gitignore files.\nPlease select which you want to write to .gitignore:", + Options: fileNames, + } + + var inp []string + survey.AskOne(prompt, &inp) + + for _, sel := range inp { + selected = append(selected, files[sel]) + } + } else { + selected = keys } + wd, _ := os.Getwd() + out := filepath.Join(wd, ".gitignore") + + if fileExists(out) { + prompt2 := &survey.Select{ + Message: ".gitignore file found in this directory. Please pick an option:", + Options: []string{"Overwrite", "Append", "Skip"}, + } + var inp2 string + survey.AskOne(prompt2, &inp2) + switch inp2 { + case "Overwrite": + log.Printf("Writing to %s", out) + writeFile(out, strings.Join(selected, "\n"), true) + break + case "Append": + log.Printf("Appending to %s", out) + writeFile(out, strings.Join(selected, "\n"), false) + break + } + } else { + log.Printf("Writing to %s", out) + writeFile(out, strings.Join(selected, "\n"), true) + } } diff --git a/go.mod b/go.mod index e477a8b..a6fb626 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,17 @@ go 1.18 require github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 require ( - golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + golang.org/x/text v0.3.6 // indirect +) + +require ( + github.com/AlecAivazis/survey/v2 v2.3.4 + golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect + golang.org/x/exp v0.0.0-20220516143420-24438e51023a + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect + golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect ) diff --git a/go.sum b/go.sum index 14eb5de..e5aa706 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,40 @@ +github.com/AlecAivazis/survey/v2 v2.3.4 h1:pchTU9rsLUSvWEl2Aq9Pv3k0IE2fkqtGxazskAMd9Ng= +github.com/AlecAivazis/survey/v2 v2.3.4/go.mod h1:hrV6Y/kQCLhIZXGcriDCUBtB3wnN7156gMXJ3+b23xM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 h1:RB0v+/pc8oMzPsN97aZYEwNuJ6ouRJ2uhjxemJ9zvrY= github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8/go.mod h1:IlWNj9v/13q7xFbaK4mbyzMNwrZLaWSHx/aibKIZuIg= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20220516143420-24438e51023a h1:tiLLxEjKNE6Hrah/Dp/cyHvsyjDLcMFSocOHO5XDmOM= +golang.org/x/exp v0.0.0-20220516143420-24438e51023a/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ignore_files.go b/ignore_files.go new file mode 100644 index 0000000..7a293b7 --- /dev/null +++ b/ignore_files.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +func prepareGitignores() ([]string, error) { + var callerSkip = 0 // might need to be 1 in release mode? + _, filename, _, _ := runtime.Caller(callerSkip) + binDir := filepath.Dir(filename) + gitignoresDir := filepath.Join(binDir, ".github.gitignore") + + if !fileExists(gitignoresDir) { + log.Println("Getting gitignore files...") + runCmd("git", "clone", "--depth=1", repoUrl, gitignoresDir) + } + + if getNeedsUpdate() { + log.Println("Updating gitignore files...") + runCmd("git", "pull", "origin", "master") + } + + return getGitignores(gitignoresDir) +} + +func getGitignores(sourceDir string) ([]string, error) { + return filepath.Glob(filepath.Join(sourceDir, "*.gitignore")) +} + +var ignoreLines = []string{ + "/*", + ".", + ".vscode", + ".vscode/*", + ".idea", + ".idea/*", +} + +func findFileMatches(patterns string) bool { + lines := strings.Split(patterns, "\n") + wd, _ := os.Getwd() + + for _, line := range lines { + // ignore empty lines / comments + line = strings.TrimSpace(line) + + if len(line) == 0 || strings.ToLower(line)[0] == '#' { + continue + } + idx := strings.Index(line, "#") + // ignore comments at end of line + if idx > -1 && (idx == 0 || line[idx-1] != '\\') { + line = strings.TrimSpace(line[0:idx]) + } + if len(line) == 0 || contains(ignoreLines, line) { + continue + } + if globExists(filepath.Join(wd, line)) { + return true + } + } + + return false +} + +func removeUnusedPatterns(filename string, keep []string) []string { + wd, _ := os.Getwd() + contents := readFile(filename) + lines := strings.Split(contents, "\n") + keepCopy := []string{} + copy(keep, keepCopy) + found := false + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + _, exists := lineCache[trimmed] + if exists { + continue + } + lineCache[trimmed] = true + if len(trimmed) == 0 || strings.ToLower(string(trimmed[0])) == "#" { + lineCache[trimmed] = false + continue + } + + if globExists(filepath.Join(wd, line)) { + found = true + keepCopy = append(keepCopy, line) + } + } + + if found { + keepCopy = insert(keepCopy, 0, fmt.Sprintf("\n# %s", filepath.Base(filename))) + } + + return keep +} diff --git a/utils.go b/utils.go index 2480ab0..48c4890 100644 --- a/utils.go +++ b/utils.go @@ -1,23 +1,26 @@ package main import ( + "io/fs" "log" "os" "os/exec" "path/filepath" ) +// UNUSED allows unused variables to be included in Go programs +func UNUSED(x ...interface{}) {} + var repoUrl = "https://github.com/github/gitignore" func fileExists(path string) bool { _, err := os.Stat(path) - exists := os.IsExist(err) + exists := !os.IsNotExist(err) return exists } func globExists(path string) bool { res, err := filepath.Glob(path) - // log.Println("globExists? " + path) handleErr(err) return res != nil } @@ -44,9 +47,49 @@ func runCmd(cmd string, args ...string) (string, error) { return string(res), err } +func readFile(path string) string { + res, err := os.ReadFile(path) + handleErr(err) + return string(res) +} + +func writeFile(path string, data string, overwrite bool) bool { + var err error + if overwrite { + // os.Create(path) + err = os.WriteFile(path, []byte(data), fs.ModeAppend) + } else { + f, _ := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + defer f.Close() + _, err = f.WriteString("\n" + data) + } + handleErr(err) + return true +} + func handleErr(err error) { if err != nil { log.Fatal(err) os.Exit(1) } } + +func insert[T any](a []T, i int, item T) []T { + return append(a[:i], append([]T{item}, a[i:]...)...) +} + +func contains[T comparable](list []T, item T) bool { + for _, v := range list { + if v == item { + return true + } + } + return false +} + +func ternary[T any](cond bool, whenTrue T, whenFalse T) T { + if cond { + return whenTrue + } + return whenFalse +}