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  }