diff --git a/README.md b/README.md index d438f69..c00c1f2 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ You may pass additional flags to `gi_gen`. These are the currently available fla | `-clean-output` \| `-c` | Perform cleanup on the output .gitignore file, removing any unused patterns | | `-append` \| `-a` | Append to .gitignore file if it already exists | | `-overwrite` \| `-w` | Overwrite .gitignore file if it already exists | +| `-detect-languages` | Outputs the automatically-detected languages, separated by newlines, and exits. Useful for outside tools detection. | | `-clear-cache` | Clear the .gitignore cache directory, for troubleshooting or for removing trace files of this program.
Exits after running, so other flags will be ignored. | | `-help` \| `-h` | Display help message | diff --git a/cmd/gi_gen.go b/cmd/gi_gen.go index 053a04d..a6f5ffb 100644 --- a/cmd/gi_gen.go +++ b/cmd/gi_gen.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/chenasraf/gi_gen/internal" + "github.com/chenasraf/gi_gen/internal/utils" ) func RunMainCmd() { @@ -15,6 +16,11 @@ func RunMainCmd() { flag.Parse() + shouldReturn = detectLanguageCommand() + if shouldReturn { + return + } + shouldReturn = cleanCommand() if shouldReturn { return @@ -22,10 +28,13 @@ func RunMainCmd() { flagLangs := getLangsFromArgs() internal.GIGen(&internal.GIGenOptions{ - Languages: &flagLangs, - CleanOutput: &cleanOutput, - OverwriteFile: &overwriteFile, - AppendFile: &appendFile, + Languages: &flagLangs, + CleanOutput: &cleanOutput, + CleanOutputUsed: isFlagPassed("clean-output") || isFlagPassed("c"), + OverwriteFile: &overwriteFile, + OverwriteFileUsed: isFlagPassed("overwrite") || isFlagPassed("w"), + AppendFile: &appendFile, + AppendFileUsed: isFlagPassed("append") || isFlagPassed("a"), }) } @@ -34,6 +43,7 @@ var cleanCache bool = false var cleanOutput bool var overwriteFile bool var appendFile bool +var detectLanguage bool func shorthand(msg string) string { return "" @@ -41,13 +51,15 @@ func shorthand(msg string) string { } func initFlags() { - appendUsage := "Append to .gitignore file if it already exists" langsUsage := "List the languages you want to use as templates.\n" + "To add multiple templates, use commas as separators, e.g.: -languages Node,Python" + cleanOutputUsage := "Perform cleanup on the output .gitignore file, removing any unused patterns" + appendUsage := "Append to .gitignore file if it already exists" + overwriteUsage := "Overwrite .gitignore file if it already exists" clearCacheUsage := "Clear the .gitignore cache directory, for troubleshooting or for removing trace files of this " + "program. Exits after running, so other flags will be ignored." - cleanOutputUsage := "Perform cleanup on the output .gitignore file, removing any unused patterns" - overwriteUsage := "Overwrite .gitignore file if it already exists" + detectLanguagesUsage := "Outputs the automatically-detected languages, separated by newlines, and exits. Useful " + + "for outside tools detection." flag.Bool("help", false, "Display help message") flag.BoolVar(&cleanCache, "clear-cache", false, clearCacheUsage) @@ -57,10 +69,21 @@ func initFlags() { flag.BoolVar(&overwriteFile, "overwrite", false, overwriteUsage) flag.BoolVar(&appendFile, "a", false, shorthand(appendUsage)) flag.BoolVar(&appendFile, "append", false, appendUsage) + flag.BoolVar(&detectLanguage, "detect-languages", false, detectLanguagesUsage) flag.StringVar(&langsRaw, "l", langsRaw, shorthand(langsUsage)) flag.StringVar(&langsRaw, "languages", langsRaw, langsUsage) } +func isFlagPassed(name string) bool { + found := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} + func getLangsFromArgs() []string { return strings.Split(langsRaw, ",") } @@ -73,6 +96,17 @@ func cleanCommand() bool { return false } +func detectLanguageCommand() bool { + if detectLanguage { + allFiles, err := internal.InitCache() + discovery, _ := internal.AutoDiscover(allFiles) + utils.HandleErr(err) + fmt.Println(strings.Join(discovery, "\n")) + return true + } + return false +} + func initHelpCommand() { flag.Usage = func() { w := flag.CommandLine.Output() diff --git a/internal/ask.go b/internal/ask.go index 1c35ced..40e2faf 100644 --- a/internal/ask.go +++ b/internal/ask.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/AlecAivazis/survey/v2" + "github.com/chenasraf/gi_gen/internal/utils" "golang.org/x/exp/maps" ) @@ -41,7 +42,7 @@ func askCleanup() bool { } func askYesNo(message string, defaultValue bool) bool { - return askSelection(message, []string{"Yes", "No"}, ternary(defaultValue, "Yes", "No")) == "Yes" + return askSelection(message, []string{"Yes", "No"}, utils.Ternary(defaultValue, "Yes", "No")) == "Yes" } func askMulti(message string, options []string) []string { @@ -54,7 +55,7 @@ func askMulti(message string, options []string) []string { survey.AskOne(langPrompt, &selections) if selections == nil { - keyInterrupt() + utils.KeyInterrupt() } return selections @@ -71,7 +72,7 @@ func askSelection(message string, options []string, defaultValue string) string survey.AskOne(langPrompt, &selection) if selection == "" { - keyInterrupt() + utils.KeyInterrupt() } return selection diff --git a/internal/cache.go b/internal/cache.go index 5ffb0e3..482b35e 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -4,18 +4,20 @@ import ( "fmt" "os" "path/filepath" + + "github.com/chenasraf/gi_gen/internal/utils" ) func InitCache() ([]string, error) { gitignoresDir := GetCacheDir() - if !fileExists(gitignoresDir) { + if !utils.FileExists(gitignoresDir) { fmt.Println("Getting gitignore files...") - runCmd("git", "clone", "--depth=2", repoUrl, gitignoresDir) + utils.RunCmd("git", "clone", "--depth=2", utils.RepoUrl, gitignoresDir) fmt.Println() } else if isCacheNeedsUpdate() { fmt.Println("Updating gitignore files...") - runCmd("git", "-C", gitignoresDir, "pull", "origin", "main") + utils.RunCmd("git", "-C", gitignoresDir, "pull", "origin", "main") fmt.Println() } diff --git a/internal/core.go b/internal/core.go index efcba88..2279000 100644 --- a/internal/core.go +++ b/internal/core.go @@ -6,25 +6,29 @@ import ( "path/filepath" "strings" + "github.com/chenasraf/gi_gen/internal/utils" "golang.org/x/exp/maps" ) type GIGenOptions struct { - Languages *[]string - CleanOutput *bool - OverwriteFile *bool - AppendFile *bool + Languages *[]string + CleanOutput *bool + CleanOutputUsed bool + OverwriteFile *bool + OverwriteFileUsed bool + AppendFile *bool + AppendFileUsed bool } func GIGen(options *GIGenOptions) { wd, err := os.Getwd() - handleErr(err) - opts := ternary(options != nil, *options, GIGenOptions{}) + utils.HandleErr(err) + opts := utils.Ternary(options != nil, *options, GIGenOptions{}) outFile := filepath.Join(wd, ".gitignore") allFiles, err := InitCache() cacheDir := GetCacheDir() - handleErr(err) + utils.HandleErr(err) var fileNames []string var files map[string]string @@ -39,25 +43,25 @@ func GIGen(options *GIGenOptions) { cleanupSelection := getCleanupSelection(opts) outContents := processFileOutput(cleanupSelection, selectedContents, selectedKeys) - if fileExists(outFile) { - getOverwriteSelection := newFunction(opts) - handleFileOverwrite(outFile, outContents, getOverwriteSelection) + if utils.FileExists(outFile) { + overwriteSelection := getOverwriteSelection(opts) + utils.HandleFileOverwrite(outFile, outContents, overwriteSelection) } else { fmt.Println() fmt.Printf("Writing to %s\n", outFile) - writeFile(outFile, outContents, true) + utils.WriteFile(outFile, outContents, true) } fmt.Println() fmt.Println("Done.") } -func newFunction(opts GIGenOptions) string { +func getOverwriteSelection(opts GIGenOptions) string { var overwriteSelection string - if opts.OverwriteFile != nil || opts.AppendFile != nil { - overwriteSelection = ternary(opts.OverwriteFile != nil, "Overwrite", "Append") + if opts.OverwriteFileUsed || opts.AppendFileUsed { + overwriteSelection = utils.Ternary(opts.OverwriteFileUsed, "Overwrite", "Append") } else { - askOverwrite() + return askOverwrite() } return overwriteSelection } @@ -74,7 +78,7 @@ func processFileOutput(cleanupSelection bool, selectedContents []string, selecte func getCleanupSelection(opts GIGenOptions) bool { var cleanupSelection bool - if opts.CleanOutput != nil { + if opts.CleanOutputUsed { cleanupSelection = *opts.CleanOutput } else { cleanupSelection = askCleanup() @@ -87,16 +91,16 @@ func getProcessFiles( ) ([]string, []string, map[string]string) { mappedFileNames := []string{} - if len(*opts.Languages) > 0 { + if len(*opts.Languages) > 0 && (*opts.Languages)[0] != "" { for _, lng := range *opts.Languages { filePath := filepath.Join(cacheDir, lng+".gitignore") - if fileExists(filePath) { + if utils.FileExists(filePath) { mappedFileNames = append(mappedFileNames, filePath) } } fileNames, files = mappedFileNames, getAllFiles(mappedFileNames) } else { - fileNames, files = autoDiscover(allFiles) + fileNames, files = readFromSelections(allFiles) } return mappedFileNames, fileNames, files } diff --git a/internal/discovery.go b/internal/discovery.go index 86b5b10..863b35e 100644 --- a/internal/discovery.go +++ b/internal/discovery.go @@ -5,22 +5,11 @@ import ( "path/filepath" "strings" + "github.com/chenasraf/gi_gen/internal/utils" "golang.org/x/exp/maps" ) -func autoDiscover(allFiles []string) ([]string, map[string]string) { - answer := askDiscovery() - - if !answer { - baseNames := []string{} - for _, fn := range allFiles { - basename := filepath.Base(fn) - langName := basename[:strings.Index(basename, ".")] - baseNames = append(baseNames, langName) - } - return baseNames, getAllFiles(allFiles) - } - +func AutoDiscover(allFiles []string) ([]string, map[string]string) { list := discoverByExplicitProjectType() if len(list) == 0 { @@ -30,11 +19,31 @@ func autoDiscover(allFiles []string) ([]string, map[string]string) { return maps.Keys(list), list } +func readFromSelections(allFiles []string) ([]string, map[string]string) { + answer := askDiscovery() + + if !answer { + return readAllFiles(allFiles) + } + + return AutoDiscover(allFiles) +} + +func readAllFiles(allFiles []string) ([]string, map[string]string) { + baseNames := []string{} + for _, fn := range allFiles { + basename := filepath.Base(fn) + langName := basename[:strings.Index(basename, ".")] + baseNames = append(baseNames, langName) + } + return baseNames, getAllFiles(allFiles) +} + func getAllFiles(allFiles []string) map[string]string { files := make(map[string]string) for _, filename := range allFiles { - contents := readFile(filename) + contents := utils.ReadFile(filename) basename := filepath.Base(filename) langName := basename[:strings.Index(basename, ".")] @@ -48,7 +57,7 @@ func discoverByExistingPatterns(allFiles []string) map[string]string { files := make(map[string]string) for _, filename := range allFiles { - contents := readFile(filename) + contents := utils.ReadFile(filename) basename := filepath.Base(filename) langName := basename[:strings.Index(basename, ".")] @@ -61,7 +70,7 @@ func discoverByExistingPatterns(allFiles []string) map[string]string { func discoverByExplicitProjectType() map[string]string { wd, err := os.Getwd() - handleErr(err) + utils.HandleErr(err) discoveryMap := make(map[string]string) @@ -142,8 +151,8 @@ func discoverByExplicitProjectType() map[string]string { checkFile := filepath.Join(wd, key) _, keyExists := results[langName] - if !keyExists && globExists(checkFile) { - results[langName] = readFile(ignoreFile) + if !keyExists && utils.GlobExists(checkFile) { + results[langName] = utils.ReadFile(ignoreFile) } } diff --git a/internal/ignore_files.go b/internal/ignore_files.go index 4e648c5..621d504 100644 --- a/internal/ignore_files.go +++ b/internal/ignore_files.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/chenasraf/gi_gen/internal/utils" "golang.org/x/exp/maps" ) @@ -17,7 +18,7 @@ func getGitignoreFiles(sourceDir string) ([]string, error) { func isCacheNeedsUpdate() bool { gitignoresDir := GetCacheDir() localBytes, localErr := exec.Command("git", "-C", gitignoresDir, "rev-list", "--count", "HEAD..@{u}").Output() - handleErr(localErr) + utils.HandleErr(localErr) localStr := strings.TrimSpace(string(localBytes)) return localStr != "0" @@ -50,10 +51,10 @@ func findPatternFileMatches(patterns string) bool { line = strings.TrimSpace(line[0:idx]) } - if len(line) == 0 || contains(ignoreLines, line) { + if len(line) == 0 || utils.Contains(ignoreLines, line) { continue } - if globExists(filepath.Join(wd, line)) { + if utils.GlobExists(filepath.Join(wd, line)) { return true } } @@ -76,8 +77,8 @@ func removeUnusedPatterns(contents string) string { continue } - if globExists(filepath.Join(wd, trimmed)) { - if contains(patternCache, trimmed) { + if utils.GlobExists(filepath.Join(wd, trimmed)) { + if utils.Contains(patternCache, trimmed) { continue } @@ -113,7 +114,7 @@ func gatherPreviousCommentGroup(i int, lastTakenIdx int, lines []string, keep [] if len(cur) > 0 && cur[0] == '#' { foundComment = true } - comments = insert(comments, 0, cur) + comments = utils.Insert(comments, 0, cur) } j++ } @@ -151,7 +152,7 @@ func langHeader(langName string) string { func getAllRaw(selected []string, selectedKeys []string) string { for i, selection := range selected { - header := ternary(len(selected) > 1, langHeader(selectedKeys[i]), "") + header := utils.Ternary(len(selected) > 1, langHeader(selectedKeys[i]), "") selected[i] = header + selection } return strings.Join(selected, "\n") @@ -164,8 +165,8 @@ func cleanupMultipleFiles(files []string, langKeys []string) string { if strings.TrimSpace(cleanSelection) == "" { continue } - header := ternary(len(files) > 1, langHeader(langKeys[i]), "") - prefixNewline := ternary(i > 0, "\n", "") + header := utils.Ternary(len(files) > 1, langHeader(langKeys[i]), "") + prefixNewline := utils.Ternary(i > 0, "\n", "") contents := prefixNewline + header + cleanSelection out = append(out, contents) } diff --git a/internal/utils.go b/internal/utils/utils.go similarity index 56% rename from internal/utils.go rename to internal/utils/utils.go index d32d507..758a8fe 100644 --- a/internal/utils.go +++ b/internal/utils/utils.go @@ -1,4 +1,4 @@ -package internal +package utils import ( "fmt" @@ -7,47 +7,47 @@ import ( "path/filepath" ) -var repoUrl = "https://github.com/github/gitignore" +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 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 { err = os.WriteFile(path, []byte(data), 0644) - 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 { fmt.Println("Encountered an error while running gi_gen:") fmt.Println(err) @@ -55,11 +55,11 @@ func handleErr(err error) { } } -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 @@ -68,41 +68,41 @@ 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 toString[T any](obj T) string { +func ToString[T any](obj T) string { return fmt.Sprint(obj) } -func handleFileOverwrite(path string, contents string, selection string) { +func HandleFileOverwrite(path string, contents string, selection string) { switch selection { case "Skip": - quit("Nothing to do, exiting") + Quit("Nothing to do, exiting") return case "Overwrite": fmt.Println() fmt.Printf("Writing to %s\n", path) - writeFile(path, contents, true) + WriteFile(path, contents, true) return case "Append": fmt.Println() fmt.Printf("Appending to %s\n", path) - writeFile(path, contents, false) + WriteFile(path, contents, false) return } - quit("Bad selection") + Quit("Bad selection") } -func keyInterrupt() { - quit("KeyInterrupt: Quitting") +func KeyInterrupt() { + Quit("KeyInterrupt: Quitting") } -func quit(message string) { +func Quit(message string) { fmt.Println() fmt.Println(message) os.Exit(1)