package cmd import ( "errors" "os" "os/exec" "path/filepath" "github.com/spf13/cobra" "github.com/spf13/viper" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" ) var projectOpenCmd = &cobra.Command{ Use: "open [fuzzy alias search]", Short: "Open project in your IDE", Aliases: []string{"goto", "cd"}, Args: cobra.MaximumNArgs(1), ArgAliases: []string{"project"}, ValidArgsFunction: validAliasesFunc, Long: projOpenCmdLong, Run: projectOpenCmdRun, } var knownEditors = []string{ "vim", "emacs", "code", "codium", "/Applications/GoLand.app/Contents/MacOS/goland", } var entrypointFiles = []string{ "README.md", "README", "main.go", "main.py", "app.py", "index.php", "server.php", "index.js", "app.js", "server.js", "app.ts", "main.c", "main.cpp", } func projectOpenCmdRun(cmd *cobra.Command, args []string) { // Find an editor editor := findEditor() if editor == "" { plog.Fatal("No usable editor found") } // Identify search terms var searchTerm string if len(args) > 0 { searchTerm = args[0] } // Try to open local project var project *gitlab.Project if searchTerm == "." { project, _ = cache.GetProjectFromCwd() } // Find a project if not local if project == nil { project = fzfSearchProjectAliases(searchTerm) if project == nil { plog.Fatal("No project to open, nothing to do") } } // Check the project path := cache.GetProjectPath(project) if _, err := os.Stat(path); err != nil { plog.Fatal("Unable to open project", plog.Args("error", err)) } // Open the project with the editor file := getEntrypointFile(path) openEditor(editor, path+"/"+file) } func openEditor(editor string, path string) { // Compile arguments args := make([]string, 0, 1) if conf.Editor.OpenFlags != "" { args = append(args, conf.Editor.OpenFlags) } args = append(args, path) // Launch editor cmd := exec.CommandContext(rootCmd.Context(), editor, args...) cmd.Dir = filepath.Dir(path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { plog.Error("Failed to open project", plog.Args( "error", err.Error(), "command", cmd.String(), )) } cmd.Wait() } func getEntrypointFile(projectPath string) string { if err := os.Chdir(projectPath); err != nil { return "" } for _, f := range entrypointFiles { if _, err := os.Stat(f); err == nil { return f } } return "" } func findEditor() string { var editor string var err error // First try configured editor if conf.Editor.Binary != "" { editor, err = getEditor(conf.Editor.Binary) if err != nil || editor == "" { plog.Error("Configured editor is not usable, finding others", plog.Args( "error", err, )) } } // Then try to find a known editor if editor == "" || err != nil { conf.Editor.OpenFlags = "" for _, e := range knownEditors { path, err := getEditor(e) if err == nil && path != "" { editor = path break } } } plog.Debug("Editor found for open", plog.Args( "editor", editor, "command", editor+" "+conf.Editor.OpenFlags, )) return editor } func getEditor(editor string) (string, error) { path, err := getEditorPath(editor) if path != "" && err == nil { if !isEditorExecutable(path) { err = errors.New("Editor is not executable") } } return path, err } func getEditorPath(editor string) (string, error) { // Check path first if editor[0] != '/' { editor, _ = exec.LookPath(editor) } return resolvePath(editor) } func isEditorExecutable(editor string) bool { var canExec bool stat, err := os.Stat(editor) if err == nil && (stat.Mode()&0444 != 0 && stat.Mode()&0111 != 0) { canExec = true } return canExec } func init() { projectCmd.AddCommand(projectOpenCmd) projectOpenCmd.PersistentFlags().String("displayName", "", "Set display name of editor (meant for config file)") projectOpenCmd.PersistentFlags().String("binary", "", "Path to editor binary") projectOpenCmd.PersistentFlags().String("openFlags", "", "Optional flags when opening project (e.g. --reuse-window)") viper.BindPFlag("editor.displayName", projectOpenCmd.Flag("displayName")) viper.BindPFlag("editor.binary", projectOpenCmd.Flag("binary")) viper.BindPFlag("editor.openFlags", projectOpenCmd.Flag("openFlags")) }