github.com/zaquestion/lab@v0.25.1/cmd/root.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "strings" 8 "text/template" 9 10 "github.com/rsteube/carapace" 11 "github.com/spf13/cobra" 12 gitconfig "github.com/tcnksm/go-gitconfig" 13 "github.com/zaquestion/lab/internal/git" 14 lab "github.com/zaquestion/lab/internal/gitlab" 15 "github.com/zaquestion/lab/internal/logger" 16 ) 17 18 // Get internal lab logger instance 19 var log = logger.GetInstance() 20 21 // RootCmd represents the base command when called without any subcommands 22 var RootCmd = &cobra.Command{ 23 Use: "lab", 24 Short: "lab: A GitLab Command Line Interface Utility", 25 Long: ``, 26 Run: func(cmd *cobra.Command, args []string) { 27 if ok, err := cmd.Flags().GetBool("version"); err == nil && ok { 28 versionCmd.Run(cmd, args) 29 return 30 } 31 helpCmd.Run(cmd, args) 32 }, 33 } 34 35 func rpad(s string, padding int) string { 36 template := fmt.Sprintf("%%-%ds", padding) 37 return fmt.Sprintf(template, s) 38 } 39 40 var templateFuncs = template.FuncMap{ 41 "rpad": rpad, 42 } 43 44 const labUsageTmpl = `{{range .Commands}}{{if (and (not .Hidden) (or .IsAvailableCommand (ne .Name "help")) (and (ne .Name "clone") (ne .Name "version") (ne .Name "merge-request")))}} 45 {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}` 46 47 func labUsageFormat(c *cobra.Command) string { 48 t := template.New("top") 49 t.Funcs(templateFuncs) 50 template.Must(t.Parse(labUsageTmpl)) 51 52 var buf bytes.Buffer 53 err := t.Execute(&buf, c) 54 if err != nil { 55 c.Println(err) 56 } 57 return buf.String() 58 } 59 60 func helpFunc(cmd *cobra.Command, args []string) { 61 // When help func is called from the help command args will be 62 // populated. When help is called with cmd.Help(), the args are not 63 // passed through, so we pick them up ourselves here 64 if len(args) == 0 { 65 args = os.Args[1:] 66 } 67 rootCmd := cmd.Root() 68 // Show help for sub/commands -- any commands that isn't "lab" or "help" 69 if cmd, _, err := rootCmd.Find(args); err == nil { 70 // Cobra will check parent commands for a helpFunc and we only 71 // want the root command to actually use this custom help func. 72 // Here we trick cobra into thinking that there is no help func 73 // so it will use the default help for the subcommands 74 cmd.Root().SetHelpFunc(nil) 75 err2 := cmd.Help() 76 if err2 != nil { 77 log.Fatal(err) 78 } 79 return 80 } 81 } 82 83 var helpCmd = &cobra.Command{ 84 Use: "help [command [subcommand...]]", 85 Short: "Show the help for lab", 86 Long: ``, 87 Run: helpFunc, 88 } 89 90 // Version is set with linker flags during build. 91 var Version string 92 93 // versionCmd represents the version command 94 var versionCmd = &cobra.Command{ 95 Use: "version", 96 Short: "", 97 Long: ``, 98 Run: func(cmd *cobra.Command, args []string) { 99 fmt.Printf("%s %s\n", "lab version", Version) 100 }, 101 } 102 103 func init() { 104 // NOTE: Calling SetHelpCommand like this causes helpFunc to be called 105 // with correct arguments. If the default cobra help func is used no 106 // arguments are passed through and subcommand help breaks. 107 RootCmd.SetHelpCommand(helpCmd) 108 RootCmd.SetHelpFunc(helpFunc) 109 RootCmd.AddCommand(versionCmd) 110 RootCmd.Flags().Bool("version", false, "Show the lab version") 111 RootCmd.PersistentFlags().Bool("no-pager", false, "Do not pipe output into a pager") 112 RootCmd.PersistentFlags().Bool("debug", false, "Enable debug logging level") 113 RootCmd.PersistentFlags().Bool("quiet", false, "Turn off any sort of logging. Only command output is printed") 114 115 // We need to set the logger level before any other piece of code is 116 // called, thus we make sure we don't lose any debug message, but for 117 // that we need to parse the args from command input and let flag errors be 118 // handled by the subcommands themselves. 119 _ = RootCmd.ParseFlags(os.Args[1:]) 120 debugLogger, _ := RootCmd.Flags().GetBool("debug") 121 quietLogger, _ := RootCmd.Flags().GetBool("quiet") 122 if debugLogger && quietLogger { 123 log.Fatal("option --debug cannot be combined with --quiet") 124 } 125 if debugLogger { 126 log.SetLogLevel(logger.LogLevelDebug) 127 } else if quietLogger { 128 log.SetLogLevel(logger.LogLevelNone) 129 } 130 carapace.Gen(RootCmd) 131 } 132 133 var ( 134 // Will be updated to upstream in Execute() if "upstream" remote exists 135 defaultRemote = "" 136 // Will be updated to lab.User() in Execute() if forkedFrom is "origin" 137 forkRemote = "" 138 ) 139 140 // Try to guess what should be the default remote. 141 func guessDefaultRemote() string { 142 // Allow to force a default remote. If set, return early. 143 if config := getMainConfig(); config != nil { 144 defaultRemote := config.GetString("core.default_remote") 145 if defaultRemote != "" { 146 return defaultRemote 147 } 148 } 149 150 guess := "" 151 152 // defaultRemote should try to always point to the upstream project. 153 // Since "origin" may have two different meanings depending on how the 154 // user forked the project, thus make "upstream" as the most significant 155 // remote. 156 // In forkRemoteProject approach, "origin" remote is the one pointing to 157 // the upstream project by default. 158 _, err := gitconfig.Local("remote.origin.url") 159 if err == nil { 160 guess = "origin" 161 } 162 // In forkCleanProject approach, "upstream" remote is the one pointing 163 // to the upstream project by default. 164 _, err = gitconfig.Local("remote.upstream.url") 165 if err == nil { 166 guess = "upstream" 167 } 168 169 // But it's still possible the user used a custom name 170 if guess == "" { 171 // use the remote tracked by the default branch if set 172 if remote, err := gitconfig.Local("branch.main.remote"); err == nil { 173 guess = remote 174 } else if remote, err = gitconfig.Local("branch.master.remote"); err == nil { 175 guess = remote 176 } else { 177 // use the first remote added to .git/config file, which, usually, is 178 // the one from which the repo was clonned 179 remotesStr, err := git.GetLocalRemotesFromFile() 180 if err == nil { 181 remotes := strings.Split(remotesStr, "\n") 182 // remotes format: remote.<name>.<url|fetch> 183 remoteName := strings.Split(remotes[0], ".")[1] 184 guess = remoteName 185 } 186 } 187 } 188 189 return guess 190 } 191 192 // Execute adds all child commands to the root command and sets flags appropriately. 193 // This is called by main.main(). It only needs to happen once to the rootCmd. 194 func Execute(initSkipped bool) { 195 // Try to gather remote information if running inside a git tree/repo. 196 // Otherwise, skip it, since the info won't be used at all, also avoiding 197 // misleading error/warning messages about missing remote. 198 if !initSkipped && git.InsideGitRepo() { 199 defaultRemote = guessDefaultRemote() 200 if defaultRemote == "" { 201 log.Infoln("No default remote found") 202 } 203 204 // Check if the user fork exists 205 _, err := gitconfig.Local("remote." + lab.User() + ".url") 206 if err == nil { 207 forkRemote = lab.User() 208 } else { 209 forkRemote = defaultRemote 210 } 211 } 212 213 // Set commandPrefix 214 cmd, _, _ := RootCmd.Find(os.Args[1:]) 215 scmd, _, _ := cmd.Find(os.Args) 216 setCommandPrefix(scmd) 217 218 if err := RootCmd.Execute(); err != nil { 219 // Execute has already logged the error 220 os.Exit(1) 221 } 222 }