1bb3a04a0c
In commit 15723008062b941b53743295784554b2f3ba875e of PR #5311 the "git lfs completion" command was introduced, utilizing the support provided by the spf13/cobra package to generate tab-completion scripts for a number of shells, including the Bash, fish, and Zsh shells. These scripts make use of "hidden" __complete and __completeNoDesc commands, also implemented by the spf13/cobra package, which the shell completion functions may query to retrieve dynamic lists of command names and flags from the Git LFS client. At present, the __complete command is used, which also returns any short help text defined each command. This additional descriptive text is then presented to the user if they are running a shell like Zsh whose completion system supports the display of such hints. However, as we only define short help text for a single Git LFS command, namely the "git lfs help" command, the display of this one text string causes the columnar display of available command names to be prefaced with a single "help" line when the user types "git lfs [Tab]": help -- Help about any command checkout fsck post-checkout status clean install post-commit track ... ... ... ... This irregularity makes the display output less helpful and more difficult to parse than if we simply suppress the inclusion of the per-command descriptions entirely, so we do so by setting the appropriate flags or using a different script generation method of the spf13/cobra package. Note that we then also need to update the name of the __complete command to __completeNoDesc in the search-and-replace operation we perform on the script generated for the Zsh shell. We can always revisit this choice in the future should we choose to add short help text to all our command definitions. This would require refactoring our NewCommand() and RegisterCommand() functions to accept the per-command text strings as extra parameters.
213 lines
5.8 KiB
Go
213 lines
5.8 KiB
Go
package commands
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/git-lfs/git-lfs/v3/config"
|
|
"github.com/git-lfs/git-lfs/v3/tools"
|
|
"github.com/git-lfs/git-lfs/v3/tr"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
commandFuncs []func() *cobra.Command
|
|
commandMu sync.Mutex
|
|
|
|
rootVersion bool
|
|
)
|
|
|
|
// NewCommand creates a new 'git-lfs' sub command, given a command name and
|
|
// command run function.
|
|
//
|
|
// Each command will initialize the local storage ('.git/lfs') directory when
|
|
// run, unless the PreRun hook is set to nil.
|
|
func NewCommand(name string, runFn func(*cobra.Command, []string)) *cobra.Command {
|
|
return &cobra.Command{Use: name, Run: runFn, PreRun: setupHTTPLogger}
|
|
}
|
|
|
|
// RegisterCommand creates a direct 'git-lfs' subcommand, given a command name,
|
|
// a command run function, and an optional callback during the command
|
|
// initialization process.
|
|
//
|
|
// The 'git-lfs' command initialization is deferred until the `commands.Run()`
|
|
// function is called. The fn callback is passed the output from NewCommand,
|
|
// and gives the caller the flexibility to customize the command by adding
|
|
// flags, tweaking command hooks, etc.
|
|
func RegisterCommand(name string, runFn func(cmd *cobra.Command, args []string), fn func(cmd *cobra.Command)) {
|
|
commandMu.Lock()
|
|
commandFuncs = append(commandFuncs, func() *cobra.Command {
|
|
cmd := NewCommand(name, runFn)
|
|
if fn != nil {
|
|
fn(cmd)
|
|
}
|
|
return cmd
|
|
})
|
|
commandMu.Unlock()
|
|
}
|
|
|
|
// Run initializes the 'git-lfs' command and runs it with the given stdin and
|
|
// command line args.
|
|
//
|
|
// It returns an exit code.
|
|
func Run() int {
|
|
log.SetOutput(ErrorWriter)
|
|
tr.InitializeLocale()
|
|
|
|
root := NewCommand("git-lfs", gitlfsCommand)
|
|
root.PreRun = nil
|
|
|
|
completionCmd := &cobra.Command{
|
|
Use: "completion [bash|fish|zsh]",
|
|
DisableFlagsInUseLine: true,
|
|
ValidArgs: []string{"bash", "fish", "zsh"},
|
|
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
switch args[0] {
|
|
case "bash":
|
|
completion := new(bytes.Buffer)
|
|
cmd.Root().GenBashCompletionV2(completion, false)
|
|
|
|
// this is needed for git bash completion to pick up the completion for the subcommand
|
|
completionSource := []byte(` local out directive
|
|
__git-lfs_get_completion_results
|
|
`)
|
|
completionReplace := []byte(` if [[ ${words[0]} == "git" && ${words[1]} == "lfs" ]]; then
|
|
words=("git-lfs" "${words[@]:2:${#words[@]}-2}")
|
|
__git-lfs_debug "Rewritten words[*]: ${words[*]},"
|
|
fi
|
|
|
|
local out directive
|
|
__git-lfs_get_completion_results
|
|
`)
|
|
newCompletion := bytes.NewBuffer(bytes.Replace(completion.Bytes(), completionSource, completionReplace, 1))
|
|
newCompletion.WriteString("_git_lfs() { __start_git-lfs; }\n")
|
|
|
|
newCompletion.WriteTo(os.Stdout)
|
|
case "fish":
|
|
cmd.Root().GenFishCompletion(os.Stdout, false)
|
|
case "zsh":
|
|
completion := new(bytes.Buffer)
|
|
cmd.Root().GenZshCompletionNoDesc(completion)
|
|
|
|
// this is needed for git zsh completion to use the right command for completion
|
|
completionSource := []byte(` requestComp="${words[1]} __completeNoDesc ${words[2,-1]}"`)
|
|
completionReplace := []byte(` requestComp="git-${words[1]#*git-} __completeNoDesc ${words[2,-1]}"`)
|
|
newCompletion := bytes.NewBuffer(bytes.Replace(completion.Bytes(), completionSource, completionReplace, 1))
|
|
|
|
newCompletion.WriteTo(os.Stdout)
|
|
}
|
|
},
|
|
}
|
|
|
|
root.AddCommand(completionCmd)
|
|
|
|
// Set up help/usage funcs based on manpage text
|
|
helpcmd := &cobra.Command{
|
|
Use: "help [command]",
|
|
Short: "Help about any command",
|
|
Long: `Help provides help for any command in the application.
|
|
Simply type ` + root.Name() + ` help [path to command] for full details.`,
|
|
|
|
Run: func(c *cobra.Command, args []string) {
|
|
cmd, _, e := c.Root().Find(args)
|
|
// In the case of "git lfs help config" or "git lfs help
|
|
// faq", pretend the last arg was "help" so our command
|
|
// lookup succeeds, since cmd will be ignored in
|
|
// helpCommand().
|
|
if e != nil && (args[0] == "config" || args[0] == "faq") {
|
|
cmd, _, e = c.Root().Find([]string{"help"})
|
|
}
|
|
if cmd == nil || e != nil {
|
|
c.Println(tr.Tr.Get("Unknown help topic %#q", args))
|
|
c.Root().Usage()
|
|
} else {
|
|
c.HelpFunc()(cmd, args)
|
|
}
|
|
},
|
|
}
|
|
|
|
root.SetHelpCommand(helpcmd)
|
|
|
|
root.SetHelpTemplate("{{.UsageString}}")
|
|
root.SetHelpFunc(helpCommand)
|
|
root.SetUsageFunc(usageCommand)
|
|
|
|
root.Flags().BoolVarP(&rootVersion, "version", "v", false, "")
|
|
|
|
canonicalizeEnvironment()
|
|
|
|
cfg = config.New()
|
|
|
|
for _, f := range commandFuncs {
|
|
if cmd := f(); cmd != nil {
|
|
root.AddCommand(cmd)
|
|
}
|
|
}
|
|
|
|
err := root.Execute()
|
|
closeAPIClient()
|
|
|
|
if err != nil {
|
|
return 127
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func gitlfsCommand(cmd *cobra.Command, args []string) {
|
|
versionCommand(cmd, args)
|
|
if !rootVersion {
|
|
cmd.Usage()
|
|
}
|
|
}
|
|
|
|
func helpCommand(cmd *cobra.Command, args []string) {
|
|
if len(args) == 0 {
|
|
printHelp("git-lfs")
|
|
} else {
|
|
printHelp(args[0])
|
|
}
|
|
}
|
|
|
|
func usageCommand(cmd *cobra.Command) error {
|
|
printHelp(cmd.Name())
|
|
return nil
|
|
}
|
|
|
|
func printHelp(commandName string) {
|
|
if commandName == "--help" {
|
|
commandName = "git-lfs"
|
|
}
|
|
if txt, ok := ManPages[commandName]; ok {
|
|
fmt.Println(strings.TrimSpace(txt))
|
|
} else {
|
|
fmt.Println(tr.Tr.Get("Sorry, no usage text found for %q", commandName))
|
|
}
|
|
}
|
|
|
|
func setupHTTPLogger(cmd *cobra.Command, args []string) {
|
|
if len(os.Getenv("GIT_LOG_STATS")) < 1 {
|
|
return
|
|
}
|
|
|
|
logBase := filepath.Join(cfg.LocalLogDir(), "http")
|
|
if err := tools.MkdirAll(logBase, cfg); err != nil {
|
|
fmt.Fprintln(os.Stderr, tr.Tr.Get("Error logging HTTP stats: %s", err))
|
|
return
|
|
}
|
|
|
|
logFile := fmt.Sprintf("http-%d.log", time.Now().Unix())
|
|
file, err := os.Create(filepath.Join(logBase, logFile))
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, tr.Tr.Get("Error logging HTTP stats: %s", err))
|
|
} else {
|
|
getAPIClient().LogHTTPStats(file)
|
|
}
|
|
}
|