diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8b13789..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.vscode/launch.json b/.vscode/launch.json index 8bb7397..03d9238 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,15 +1,15 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "gi_gen.go" - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "main.go" + } + ] +} diff --git a/cmd/gi_gen.go b/cmd/gi_gen.go new file mode 100644 index 0000000..7d4926e --- /dev/null +++ b/cmd/gi_gen.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "log" + "os" + "path/filepath" + + . "github.com/chenasraf/gi_gen/internal" +) + +func RunMainCmd() { + wd, err := os.Getwd() + HandleErr(err) + + outFile := filepath.Join(wd, ".gitignore") + allFiles, err := PrepareGitignores() + HandleErr(err) + + fileNames, files := GetRelevantFiles(allFiles) + + log.Println("Done.") + + selected, selectedKeys := GetLanguageSelections(files, fileNames) + cleanupSelection := GetCleanupSelection() + outContents := Ternary(cleanupSelection, CleanupMultiple(selected, selectedKeys), GetAllRaw(selected, selectedKeys)) + + if FileExists(outFile) { + HandleFileOverwrite(outFile, outContents) + } else { + log.Printf("Writing to %s", outFile) + WriteFile(outFile, outContents, true) + } +} diff --git a/gi_gen.go b/gi_gen.go deleted file mode 100644 index ae8a955..0000000 --- a/gi_gen.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "log" - "os" - "path/filepath" - "strings" -) - -func main() { - wd, err := os.Getwd() - handleErr(err) - - outFile := filepath.Join(wd, ".gitignore") - allFiles, err := prepareGitignores() - - handleErr(err) - - fileNames, files := getPossibleFiles(allFiles) - - log.Println("Done.") - - selected, selectedKeys := getLanguages(files, fileNames) - - cleanupSelection := getCleanupSelection() - var outContents string - if cleanupSelection { - out := []string{} - for i, selection := range selected { - cleanSelection := removeUnusedPatterns(selection) - if strings.TrimSpace(cleanSelection) == "" { - continue - } - header := langHeader(selectedKeys[i]) - prefixNewline := ternary(i > 0, "\n", "") - contents := prefixNewline + header + cleanSelection - out = append(out, contents) - } - outContents = strings.Join(out, "\n") - } else { - for i, selection := range selected { - header := langHeader(selectedKeys[i]) - selected[i] = header + selection - } - outContents = strings.Join(selected, "\n") - } - - if fileExists(outFile) { - handleFileOverwrite(outFile, outContents) - } else { - log.Printf("Writing to %s", outFile) - writeFile(outFile, outContents, true) - } -} diff --git a/ignore_files.go b/ignore_files.go deleted file mode 100644 index 390adcf..0000000 --- a/ignore_files.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "golang.org/x/exp/maps" -) - -func prepareGitignores() ([]string, error) { - gitignoresDir := getCacheDir() - - 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") - os.RemoveAll(filepath.Join(gitignoresDir, ".git")) - } - - return getGitignores(gitignoresDir) -} - -func getCacheDir() string { - homeDir, _ := os.UserHomeDir() - gitignoresDir := filepath.Join(homeDir, ".github.gitignore") - return 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 { - line = strings.TrimSpace(line) - - // ignore empty lines / comments - 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 -} - -var patternCache []string = []string{} - -func removeUnusedPatterns(contents string) string { - wd, _ := os.Getwd() - lines := strings.Split(contents, "\n") - keep := []string{} - lastTakenIdx := -1 - - for i, line := range lines { - trimmed := strings.TrimSpace(line) - - if len(trimmed) == 0 || trimmed[0] == '#' { - continue - } - - if globExists(filepath.Join(wd, trimmed)) { - if contains(patternCache, trimmed) { - continue - } - - patternCache = append(patternCache, trimmed) - - if i > 0 { - j := 1 - foundComment := false - comments := []string{} - for { - if i-j < 0 || i-j <= lastTakenIdx { - break - } - cur := lines[i-j] - if len(cur) > 0 && cur[0] != '#' { - if !foundComment { - } else { - break - } - } else { - lastTakenIdx = i - j - if len(cur) > 0 && cur[0] == '#' { - foundComment = true - } - comments = insert(comments, 0, cur) - } - j++ - } - for _, v := range comments { - keep = append(keep, v) - } - } - - keep = append(keep, line) - } - } - - return strings.Join(keep, "\n") -} - -func getLanguages(files map[string]string, fileNames []string) ([]string, []string) { - selected := []string{} - allKeys := maps.Keys(files) - selectedKeys := maps.Keys(files) - - if len(allKeys) > 1 { - selected, selectedKeys = getLanguageSelections(fileNames, selected, files) - } else { - selected = []string{files[allKeys[0]]} - } - - return selected, selectedKeys -} - -func langHeader(langName string) string { - sep := "#========================================================================\n" - header := fmt.Sprintf(sep+"# %s\n"+sep+"\n", langName) - return header -} diff --git a/core.go b/internal/core.go similarity index 61% rename from core.go rename to internal/core.go index c7db700..3527cc1 100644 --- a/core.go +++ b/internal/core.go @@ -1,33 +1,31 @@ -package main +package internal import ( "fmt" "log" "os" - "path/filepath" - "strings" "github.com/AlecAivazis/survey/v2" ) -func handleFileOverwrite(path string, contents string) { - overwriteSelection := getOverwriteSelection() +func HandleFileOverwrite(path string, contents string) { + overwriteSelection := AskOverwrite() switch overwriteSelection { case "": - quit() + Quit() break case "Overwrite": log.Printf("Writing to %s", path) - writeFile(path, contents, true) + WriteFile(path, contents, true) break case "Append": log.Printf("Appending to %s", path) - writeFile(path, contents, false) + WriteFile(path, contents, false) break } } -func getOverwriteSelection() string { +func AskOverwrite() string { overwritePrompt := &survey.Select{ Message: ".gitignore file found in this directory. Please pick an option:", Options: []string{"Overwrite", "Append", "Skip"}, @@ -37,7 +35,7 @@ func getOverwriteSelection() string { return overwriteSelection } -func getCleanupSelection() bool { +func GetCleanupSelection() bool { cleanupPrompt := &survey.Confirm{ Message: "Do you want to remove patterns not existing in your project?", Default: true, @@ -48,7 +46,7 @@ func getCleanupSelection() bool { return cleanupSelection } -func getLanguageSelections(fileNames []string, selected []string, files map[string]string) ([]string, []string) { +func AskLanguage(fileNames []string, selected []string, files map[string]string) ([]string, []string) { langPrompt := &survey.MultiSelect{ Message: "Found " + fmt.Sprint(len(fileNames)) + " possible matches in your project for gitignore files.\n" + @@ -59,7 +57,7 @@ func getLanguageSelections(fileNames []string, selected []string, files map[stri var langSelections []string survey.AskOne(langPrompt, &langSelections) if langSelections == nil { - quit() + Quit() } keys := []string{} for _, selection := range langSelections { @@ -70,10 +68,7 @@ func getLanguageSelections(fileNames []string, selected []string, files map[stri return selected, keys } -func getPossibleFiles(allFiles []string) ([]string, map[string]string) { - files := make(map[string]string) - fileNames := []string{} - +func AskDiscovery() bool { prompt := &survey.Confirm{ Message: "Would you like to try to scan for available templates automatically?\n" + "Select 'No' ('n') to see all available templates", @@ -81,26 +76,9 @@ func getPossibleFiles(allFiles []string) ([]string, map[string]string) { } var answer bool survey.AskOne(prompt, &answer) - - for _, filename := range allFiles { - contents := readFile(filename) - basename := filepath.Base(filename) - langName := basename[:strings.Index(basename, ".")] - - if answer { - if findFileMatches(contents) { - files[langName] = contents - fileNames = append(fileNames, langName) - } - } else { - files[langName] = contents - fileNames = append(fileNames, langName) - } - } - - return fileNames, files + return answer } -func quit() { +func Quit() { os.Exit(1) } diff --git a/internal/ignore_files.go b/internal/ignore_files.go new file mode 100644 index 0000000..cae3287 --- /dev/null +++ b/internal/ignore_files.go @@ -0,0 +1,204 @@ +package internal + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "golang.org/x/exp/maps" +) + +func PrepareGitignores() ([]string, error) { + gitignoresDir := GetCacheDir() + + 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") + os.RemoveAll(filepath.Join(gitignoresDir, ".git")) + } + + return GetGitignores(gitignoresDir) +} + +func GetCacheDir() string { + homeDir, _ := os.UserHomeDir() + gitignoresDir := filepath.Join(homeDir, ".github.gitignore") + return 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 { + line = strings.TrimSpace(line) + + // ignore empty lines / comments + 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 +} + +var patternCache []string = []string{} + +func RemoveUnusedPatterns(contents string) string { + wd, _ := os.Getwd() + lines := strings.Split(contents, "\n") + keep := []string{} + lastTakenIdx := -1 + + for i, line := range lines { + trimmed := strings.TrimSpace(line) + + if len(trimmed) == 0 || trimmed[0] == '#' { + continue + } + + if GlobExists(filepath.Join(wd, trimmed)) { + if Contains(patternCache, trimmed) { + continue + } + + patternCache = append(patternCache, trimmed) + + if i > 0 { + keep = GatherPreviousCommentGroup(i, lastTakenIdx, lines, keep) + } + + keep = append(keep, line) + } + } + + return strings.Join(keep, "\n") +} + +func GatherPreviousCommentGroup(i int, lastTakenIdx int, lines []string, keep []string) []string { + j := 1 + foundComment := false + comments := []string{} + for { + if i-j < 0 || i-j <= lastTakenIdx { + break + } + cur := lines[i-j] + if len(cur) > 0 && cur[0] != '#' { + if !foundComment { + } else { + break + } + } else { + lastTakenIdx = i - j + if len(cur) > 0 && cur[0] == '#' { + foundComment = true + } + comments = Insert(comments, 0, cur) + } + j++ + } + for _, v := range comments { + keep = append(keep, v) + } + return keep +} + +func GetLanguageSelections(files map[string]string, fileNames []string) ([]string, []string) { + selected := []string{} + allKeys := maps.Keys(files) + selectedKeys := maps.Keys(files) + + if len(allKeys) > 1 { + selected, selectedKeys = AskLanguage(fileNames, selected, files) + } else { + selected = []string{files[allKeys[0]]} + } + + return selected, selectedKeys +} + +func GetRelevantFiles(allFiles []string) ([]string, map[string]string) { + files := make(map[string]string) + fileNames := []string{} + + answer := AskDiscovery() + + for _, filename := range allFiles { + contents := ReadFile(filename) + basename := filepath.Base(filename) + langName := basename[:strings.Index(basename, ".")] + + if answer { + if FindFileMatches(contents) { + files[langName] = contents + fileNames = append(fileNames, langName) + } + } else { + files[langName] = contents + fileNames = append(fileNames, langName) + } + } + + return fileNames, files +} + +func LangHeader(langName string) string { + sep := "#========================================================================\n" + header := fmt.Sprintf(sep+"# %s\n"+sep+"\n", langName) + return header +} + +func GetAllRaw(selected []string, selectedKeys []string) string { + for i, selection := range selected { + header := LangHeader(selectedKeys[i]) + selected[i] = header + selection + } + return strings.Join(selected, "\n") +} + +func CleanupMultiple(selected []string, keys []string) string { + out := []string{} + for i, selection := range selected { + cleanSelection := RemoveUnusedPatterns(selection) + if strings.TrimSpace(cleanSelection) == "" { + continue + } + header := LangHeader(keys[i]) + prefixNewline := Ternary(i > 0, "\n", "") + contents := prefixNewline + header + cleanSelection + out = append(out, contents) + } + return strings.Join(out, "\n") +} diff --git a/utils.go b/internal/utils.go similarity index 71% rename from utils.go rename to internal/utils.go index 5bd7ffe..0faa8ef 100644 --- a/utils.go +++ b/internal/utils.go @@ -1,4 +1,4 @@ -package main +package internal import ( "fmt" @@ -14,19 +14,19 @@ func UNUSED(x ...interface{}) {} var repoUrl = "https://github.com/github/gitignore" -func fileExists(path string) bool { +func FileExists(path string) bool { _, err := os.Stat(path) exists := !os.IsNotExist(err) return exists } -func globExists(path string) bool { +func GlobExists(path string) bool { res, err := filepath.Glob(path) - handleErr(err) + HandleErr(err) return res != nil } -func getNeedsUpdate() bool { +func GetNeedsUpdate() bool { localBytes, localErr := exec.Command("git", "rev-parse", "@").Output() baseBytes, baseErr := exec.Command("git", "merge-base", "@", "@{u}").Output() if localErr != nil { @@ -43,46 +43,46 @@ func getNeedsUpdate() bool { return localStr == baseStr } -func runCmd(cmd string, args ...string) (string, error) { +func RunCmd(cmd string, args ...string) (string, error) { res, err := exec.Command(cmd, args...).Output() return string(res), err } -func readFile(path string) string { +func ReadFile(path string) string { res, err := os.ReadFile(path) - handleErr(err) + HandleErr(err) return string(res) } -func writeFile(path string, data string, overwrite bool) bool { +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) - handleErr(err) + HandleErr(err) } else { f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - handleErr(err) + HandleErr(err) defer f.Close() _, err = f.WriteString("\n" + data) - handleErr(err) + HandleErr(err) } return true } -func handleErr(err error) { +func HandleErr(err error) { if err != nil { log.Println("Encountered an error while running gi_gen:") log.Fatalln(err) } } -func insert[T any](a []T, i int, item T) []T { +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 { +func Contains[T comparable](list []T, item T) bool { for _, v := range list { if v == item { return true @@ -91,13 +91,13 @@ func contains[T comparable](list []T, item T) bool { return false } -func ternary[T any](cond bool, whenTrue T, whenFalse T) T { +func Ternary[T any](cond bool, whenTrue T, whenFalse T) T { if cond { return whenTrue } return whenFalse } -func toS[T any](obj T) string { +func ToString[T any](obj T) string { return fmt.Sprint(obj) } diff --git a/main.go b/main.go new file mode 100644 index 0000000..8a9f37d --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/chenasraf/gi_gen/cmd" +) + +func main() { + cmd.RunMainCmd() +}