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  }