From bf213be9fbaf91d1cdd829256aed8eed52ce0dcb Mon Sep 17 00:00:00 2001 From: Ryan D McGuire Date: Tue, 12 Dec 2023 16:30:33 -0500 Subject: [PATCH] Add project open --- README.md | 2 + cmd/project_open.go | 185 ++++++++++++++++++++++++++++++ cmd/util_constants.go | 6 + contrib/gpm_func_omz.zsh | 4 + internal/config/config.go | 17 ++- internal/projects/projects_git.go | 2 +- 6 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 cmd/project_open.go diff --git a/README.md b/README.md index 8a90dec..3976bbf 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ cache: ``` ## TODO +- [ ] Fix initial setup requiring project path, and set https:// as default for gitlab host - [ ] Fix NPE when cache is reset or project for whatever reason leaves an orphaned alias - [ ] Add config setters and getters - [ ] Add TTL check to cache load, and add -f / --force flag to re-build regardless @@ -64,6 +65,7 @@ cache: - [ ] Add open command - [ ] config should exist for editor (vim, code, etc..) - [ ] Update README for shell completion, aliases, usage +- [ ] Add fzf to `plist` / `gpm projects list` - [ ] Make a Makefile - [ ] Add git repo status to project go (up-to-date, pending commits, etc..) - [x] Update `gpm project show` with pterm box like `gpm project list` diff --git a/cmd/project_open.go b/cmd/project_open.go new file mode 100644 index 0000000..49f3d70 --- /dev/null +++ b/cmd/project_open.go @@ -0,0 +1,185 @@ +package cmd + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +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] + } + + // Find a project + 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) + + 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")) +} diff --git a/cmd/util_constants.go b/cmd/util_constants.go index 60db429..dd0798f 100644 --- a/cmd/util_constants.go +++ b/cmd/util_constants.go @@ -44,6 +44,12 @@ uses fuzzy find to locate the project` const projShowCmdLong = `Shows detail for a particular project Will always fuzzy find` +const projOpenCmdLong = `Opens the given project directory in the editor +of your choice. Will find certain well-known entrypoints (e.g. main.go). + +If your editor is set in your config file, it will be used, otherwise +one will be found in your path from a list of known defaults.` + const configCmdLong = `Commands for managing configuration, particulary useful for seeding a new config file` diff --git a/contrib/gpm_func_omz.zsh b/contrib/gpm_func_omz.zsh index bdcc18b..6f0e509 100644 --- a/contrib/gpm_func_omz.zsh +++ b/contrib/gpm_func_omz.zsh @@ -24,3 +24,7 @@ plist () { pshow () { gitlab-project-manager project show --current } + +popen () { + gitlab-project-manager project open $1 +} diff --git a/internal/config/config.go b/internal/config/config.go index 797fe17..27c14c3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,16 +3,23 @@ package config import "time" type Config struct { - GitlabHost string `yaml:"gitlabHost" json:"gitlabHost"` - GitlabToken string `yaml:"gitlabToken" json:"gitlabToken"` - LogLevel string `yaml:"logLevel" json:"logLevel" enum:"info,warn,debug,error"` - ProjectPath string `yaml:"projectPath" json:"projectPath"` - Cache cacheConfig `yaml:"cache" json:"cache"` + Editor editorConfig `yaml:"editor" json:"editor"` + GitlabHost string `yaml:"gitlabHost" json:"gitlabHost"` + GitlabToken string `yaml:"gitlabToken" json:"gitlabToken"` + LogLevel string `yaml:"logLevel" json:"logLevel" enum:"info,warn,debug,error"` + ProjectPath string `yaml:"projectPath" json:"projectPath"` + Cache cacheConfig `yaml:"cache" json:"cache"` Dump struct { Full bool } } +type editorConfig struct { + DisplayName string `yaml:"displanName" json:"displanName"` + Binary string `yaml:"binary" json:"binary"` + OpenFlags string `yaml:"openFlags" json:"openFlags"` +} + type loadConfig struct { OwnerOnly bool `yaml:"ownerOnly" json:"ownerOnly"` } diff --git a/internal/projects/projects_git.go b/internal/projects/projects_git.go index fdd877e..fe23a75 100644 --- a/internal/projects/projects_git.go +++ b/internal/projects/projects_git.go @@ -8,7 +8,7 @@ import ( "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" ) -const gitCloneTimeoutSecs = 10 +const gitCloneTimeoutSecs = 30 // Will either read in the current repo, preparing a report // on its current state, or will clone the project if it has not