github.com/matthewdale/lab@v0.14.0/cmd/root.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "os" 8 "os/exec" 9 "strconv" 10 "strings" 11 "syscall" 12 "text/template" 13 14 "github.com/pkg/errors" 15 "github.com/spf13/cobra" 16 gitconfig "github.com/tcnksm/go-gitconfig" 17 "github.com/zaquestion/lab/internal/git" 18 lab "github.com/zaquestion/lab/internal/gitlab" 19 ) 20 21 // RootCmd represents the base command when called without any subcommands 22 var RootCmd = &cobra.Command{ 23 Use: "lab", 24 Short: "A Git Wrapper for GitLab", 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 (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 cmd != rootCmd && strings.Split(cmd.Use, " ")[0] != "help" { 71 // Cobra will check parent commands for a helpFunc and we only 72 // want the root command to actually use this custom help func. 73 // Here we trick cobra into thinking that there is no help func 74 // so it will use the default help for the subcommands 75 cmd.Root().SetHelpFunc(nil) 76 err2 := cmd.Help() 77 if err2 != nil { 78 log.Fatal(err) 79 } 80 return 81 } 82 formatChar := "\n" 83 if git.IsHub { 84 formatChar = "" 85 } 86 87 git := git.New() 88 git.Stdout = nil 89 git.Stderr = nil 90 usage, _ := git.CombinedOutput() 91 fmt.Printf("%s%sThese GitLab commands are provided by lab:\n%s\n\n", string(usage), formatChar, labUsageFormat(cmd.Root())) 92 } 93 94 var helpCmd = &cobra.Command{ 95 Use: "help [command [subcommand...]]", 96 Short: "Show the help for lab", 97 Long: ``, 98 Run: helpFunc, 99 } 100 101 func init() { 102 // NOTE: Calling SetHelpCommand like this causes helpFunc to be called 103 // with correct arguments. If the default cobra help func is used no 104 // arguments are passed through and subcommand help breaks. 105 RootCmd.SetHelpCommand(helpCmd) 106 RootCmd.SetHelpFunc(helpFunc) 107 RootCmd.Flags().Bool("version", false, "Show the lab version") 108 } 109 110 // parseArgsStr returns a string and a number if parsed. Many commands accept a 111 // string to operate on (remote or search) and number such as a page id 112 func parseArgsStr(args []string) (string, int64, error) { 113 if len(args) == 2 { 114 n, err := strconv.ParseInt(args[1], 0, 64) 115 if err != nil { 116 return args[0], 0, err 117 } 118 return args[0], n, nil 119 } 120 if len(args) == 1 { 121 n, err := strconv.ParseInt(args[0], 0, 64) 122 if err != nil { 123 return args[0], 0, nil 124 } 125 return "", n, nil 126 } 127 return "", 0, nil 128 } 129 130 func parseArgs(args []string) (string, int64, error) { 131 if !git.InsideGitRepo() { 132 return "", 0, nil 133 } 134 remote, num, err := parseArgsStr(args) 135 if err != nil { 136 return "", 0, err 137 } 138 ok, err := git.IsRemote(remote) 139 if err != nil { 140 return "", 0, err 141 } else if !ok && remote != "" { 142 switch len(args) { 143 case 1: 144 return "", 0, errors.Errorf("%s is not a valid remote or number", args[0]) 145 default: 146 return "", 0, errors.Errorf("%s is not a valid remote", args[0]) 147 } 148 } 149 if remote == "" { 150 remote = forkedFromRemote 151 } 152 rn, err := git.PathWithNameSpace(remote) 153 if err != nil { 154 return "", 0, err 155 } 156 return rn, num, nil 157 } 158 159 var ( 160 // Will be updated to upstream in Execute() if "upstream" remote exists 161 forkedFromRemote = "origin" 162 // Will be updated to lab.User() in Execute() if forkedFrom is "origin" 163 forkRemote = "origin" 164 ) 165 166 // Execute adds all child commands to the root command and sets flags appropriately. 167 // This is called by main.main(). It only needs to happen once to the rootCmd. 168 func Execute() { 169 _, err := gitconfig.Local("remote.upstream.url") 170 if err == nil { 171 forkedFromRemote = "upstream" 172 } 173 174 if forkedFromRemote == "origin" { 175 // Check if the user fork exists 176 _, err = gitconfig.Local("remote." + lab.User() + ".url") 177 if err == nil { 178 forkRemote = lab.User() 179 } 180 } 181 // Check if the user is calling a lab command or if we should passthrough 182 // NOTE: The help command won't be found by Find, which we are counting on 183 cmd, _, err := RootCmd.Find(os.Args[1:]) 184 if err != nil || cmd.Use == "clone" { 185 // Determine if any undefined flags were passed to "clone" 186 // TODO: Evaluate and support some of these flags 187 // NOTE: `hub help -a` wraps the `git help -a` output 188 if (cmd.Use == "clone" && len(os.Args) > 2) || os.Args[1] == "help" { 189 // ParseFlags will err in these cases 190 err = cmd.ParseFlags(os.Args[1:]) 191 if err == nil { 192 if err := RootCmd.Execute(); err != nil { 193 // Execute has already logged the error 194 os.Exit(1) 195 } 196 return 197 } 198 } 199 200 // Lab passthrough for these commands can cause confusion. See #163 201 if os.Args[1] == "create" { 202 log.Fatalf("Please call `hub create` directly for github, the lab equivalent is `lab project create`") 203 } 204 if os.Args[1] == "browse" { 205 log.Fatalf("Please call `hub browse` directly for github, the lab equivalent is `lab <object> browse`") 206 } 207 208 // Passthrough to git for any unrecognized commands 209 err = git.New(os.Args[1:]...).Run() 210 if exiterr, ok := err.(*exec.ExitError); ok { 211 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 212 os.Exit(status.ExitStatus()) 213 } 214 } 215 if err != nil { 216 log.Fatal(err) 217 } 218 return 219 } 220 221 // allow flags to the root cmd to be passed through. Technically we'll drop any exit code info which isn't ideal. 222 // TODO: remove for 1.0 when we stop wrapping git 223 if cmd.Use == RootCmd.Use && len(os.Args) > 1 { 224 var hFlaged bool 225 for _, v := range os.Args { 226 if v == "--help" { 227 hFlaged = true 228 } 229 } 230 if !hFlaged { 231 git.New(os.Args[1:]...).Run() 232 return 233 } 234 } 235 if err := RootCmd.Execute(); err != nil { 236 // Execute has already logged the error 237 os.Exit(1) 238 } 239 }