mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-10 20:05:50 +02:00
Clean up the configuration of where a custom command's output goes (#4525)
- **PR Description** Previously, custom commands had a `stream` field that was overloaded with two meanings: 1) it made the command's output appear in the log view, and 2) it used a pty for running the command. It makes sense to be able to configure these independently, so add a separate `pty` field (although that's probably rarely needed in practice). Also, the `stream` and `showOutput` fields were conflicting; they could be used together, but when setting them both to true, the popup would always show "empty output", so this doesn't make sense. Combine them both into a single `output` property with the possible values "none", "log", or "popup". We still have some more redundancy here, for example pty is only used when output is set to "log", and neither output nor pty are used when subprocess is true. But I stopped there, because I think this is already an improvement over the previous state. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [x] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [x] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
commit
223978eb82
39 changed files with 487 additions and 461 deletions
|
@ -14,7 +14,7 @@ customCommands:
|
|||
- key: 'C'
|
||||
context: 'global'
|
||||
command: "git commit"
|
||||
subprocess: true
|
||||
output: terminal
|
||||
- key: 'n'
|
||||
context: 'localBranches'
|
||||
prompts:
|
||||
|
@ -53,13 +53,11 @@ For a given custom command, here are the allowed fields:
|
|||
| key | The key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md). Custom commands without a key specified can be triggered by selecting them from the keybindings (`?`) menu | no |
|
||||
| command | The command to run (using Go template syntax for placeholder values) | yes |
|
||||
| context | The context in which to listen for the key (see [below](#contexts)) | yes |
|
||||
| subprocess | Whether you want the command to run in a subprocess (e.g. if the command requires user input) | no |
|
||||
| prompts | A list of prompts that will request user input before running the final command | no |
|
||||
| loadingText | Text to display while waiting for command to finish | no |
|
||||
| description | Label for the custom command when displayed in the keybindings menu | no |
|
||||
| stream | Whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | Whether you want to show the command's output in a popup within Lazygit | no |
|
||||
| outputTitle | The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. | no |
|
||||
| output | Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup. | no |
|
||||
| outputTitle | The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title. | no |
|
||||
| after | Actions to take after the command has completed | no |
|
||||
|
||||
Here are the options for the `after` key:
|
||||
|
@ -365,7 +363,7 @@ If you use the commandMenu property, none of the other properties except key and
|
|||
|
||||
## Debugging
|
||||
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `output: popup` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
|
||||
|
||||
## More Examples
|
||||
|
||||
|
|
|
@ -30,11 +30,11 @@ func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuild
|
|||
|
||||
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
|
||||
|
||||
func (self *gitCmdObjBuilder) New(args []string) oscommands.ICmdObj {
|
||||
func (self *gitCmdObjBuilder) New(args []string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) oscommands.ICmdObj {
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,12 +20,12 @@ type gitCmdObjRunner struct {
|
|||
innerRunner oscommands.ICmdObjRunner
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
|
||||
func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
func (self *gitCmdObjRunner) RunWithOutput(cmdObj *oscommands.CmdObj) (string, error) {
|
||||
var output string
|
||||
var err error
|
||||
for i := 0; i < RetryCount; i++ {
|
||||
|
@ -44,7 +44,7 @@ func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, e
|
|||
return output, err
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) {
|
||||
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj *oscommands.CmdObj) (string, string, error) {
|
||||
var stdout, stderr string
|
||||
var err error
|
||||
for i := 0; i < RetryCount; i++ {
|
||||
|
@ -64,6 +64,6 @@ func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string,
|
|||
}
|
||||
|
||||
// Retry logic not implemented here, but these commands typically don't need to obtain a lock.
|
||||
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj *oscommands.CmdObj, onLine func(line string) (bool, error)) error {
|
||||
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
|
||||
}
|
||||
|
|
|
@ -154,7 +154,7 @@ func (self *BranchCommands) GetGraph(branchName string) (string, error) {
|
|||
return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj {
|
||||
func (self *BranchCommands) GetGraphCmdObj(branchName string) *oscommands.CmdObj {
|
||||
branchLogCmdTemplate := self.UserConfig().Git.BranchLogCmd
|
||||
templateValues := map[string]string{
|
||||
"branchName": self.cmd.Quote(branchName),
|
||||
|
@ -255,7 +255,7 @@ func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
|
|||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
|
||||
func (self *BranchCommands) AllBranchesLogCmdObj() *oscommands.CmdObj {
|
||||
// Only choose between non-empty, non-identical commands
|
||||
candidates := lo.Uniq(lo.WithoutEmpty(append([]string{
|
||||
self.UserConfig().Git.AllBranchesLogCmd,
|
||||
|
|
|
@ -85,7 +85,7 @@ func (self *CommitCommands) ResetToCommit(hash string, strength string, envVars
|
|||
Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitCmdObj(summary string, description string, forceSkipHooks bool) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) CommitCmdObj(summary string, description string, forceSkipHooks bool) *oscommands.CmdObj {
|
||||
messageArgs := self.commitMessageArgs(summary, description)
|
||||
skipHookPrefix := self.UserConfig().Git.SkipHookPrefix
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
|
@ -97,16 +97,16 @@ func (self *CommitCommands) CommitCmdObj(summary string, description string, for
|
|||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj {
|
||||
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").Arg("--allow-empty", "--amend", "--only").ToArgv())
|
||||
}
|
||||
|
||||
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").
|
||||
Arg("--allow-empty", "--amend", "--only", "--edit", "--file="+tmpMessageFile).ToArgv())
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string, forceSkipHooks bool) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string, forceSkipHooks bool) *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("commit").
|
||||
ArgIf(forceSkipHooks, "--no-verify").
|
||||
Arg("--edit").
|
||||
|
@ -116,7 +116,7 @@ func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile s
|
|||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(summary string, description string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) RewordLastCommit(summary string, description string) *oscommands.CmdObj {
|
||||
messageArgs := self.commitMessageArgs(summary, description)
|
||||
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
|
@ -138,7 +138,7 @@ func (self *CommitCommands) commitMessageArgs(summary string, description string
|
|||
}
|
||||
|
||||
// runs git commit without the -m argument meaning it will invoke the user's editor
|
||||
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
|
||||
func (self *CommitCommands) CommitEditorCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
|
||||
ToArgv()
|
||||
|
@ -246,7 +246,7 @@ func (self *CommitCommands) AmendHead() error {
|
|||
return self.AmendHeadCmdObj().Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
|
||||
func (self *CommitCommands) AmendHeadCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--amend", "--no-edit", "--allow-empty").
|
||||
ToArgv()
|
||||
|
@ -254,7 +254,7 @@ func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
|
|||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) *oscommands.CmdObj {
|
||||
contextSize := self.AppState.DiffContextSize
|
||||
|
||||
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
|
||||
|
@ -278,7 +278,7 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommand
|
|||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Arg(fmt.Sprintf("%s:%s", hash, filePath)).
|
||||
ToArgv()
|
||||
|
|
|
@ -583,7 +583,7 @@ func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
|||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
|
||||
gitLogOrder := self.AppState.GitLogOrder
|
||||
|
||||
refSpec := opts.RefName
|
||||
|
|
|
@ -18,7 +18,7 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
|||
|
||||
// This is for generating diffs to be shown in the UI (e.g. rendering a range
|
||||
// diff to the main view). It uses a custom pager if one is configured.
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {
|
||||
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
|
||||
useExtDiff := extDiffCmd != ""
|
||||
ignoreWhitespace := self.AppState.IgnoreWhitespaceInDiffView
|
||||
|
@ -83,7 +83,7 @@ type DiffToolCmdOptions struct {
|
|||
Staged bool
|
||||
}
|
||||
|
||||
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands.ICmdObj {
|
||||
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("difftool").
|
||||
Arg("--no-prompt").
|
||||
ArgIf(opts.IsDirectory, "--dir-diff").
|
||||
|
@ -95,7 +95,7 @@ func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands
|
|||
ToArgv())
|
||||
}
|
||||
|
||||
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) oscommands.ICmdObj {
|
||||
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) *oscommands.CmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff-index").
|
||||
Config("diff.noprefix=false").
|
||||
|
|
|
@ -24,7 +24,7 @@ func (self *FlowCommands) GitFlowEnabled() bool {
|
|||
return self.config.GetGitFlowPrefixes() != ""
|
||||
}
|
||||
|
||||
func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, error) {
|
||||
func (self *FlowCommands) FinishCmdObj(branchName string) (*oscommands.CmdObj, error) {
|
||||
prefixes := self.config.GetGitFlowPrefixes()
|
||||
|
||||
// need to find out what kind of branch this is
|
||||
|
@ -54,7 +54,7 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
|
|||
return self.cmd.New(cmdArgs), nil
|
||||
}
|
||||
|
||||
func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj {
|
||||
func (self *FlowCommands) StartCmdObj(branchType string, name string) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("flow").Arg(branchType, "start", name).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
|
|
|
@ -53,7 +53,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, su
|
|||
return self.ContinueRebase()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (*oscommands.CmdObj, error) {
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Hash: commits[index].Hash(),
|
||||
NewAction: todo.Reword,
|
||||
|
@ -209,7 +209,7 @@ type PrepareInteractiveRebaseCommandOpts struct {
|
|||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
||||
// lazygit instructions what to do with the todo file
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) *oscommands.CmdObj {
|
||||
ex := oscommands.GetLazygitPath()
|
||||
|
||||
cmdArgs := NewGitCmd("rebase").
|
||||
|
@ -446,7 +446,7 @@ func (self *RebaseCommands) RebaseBranchFromBaseCommit(targetBranchName string,
|
|||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd(commandType).Arg("--" + command).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
|
@ -485,7 +485,7 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
|
||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj *oscommands.CmdObj) error {
|
||||
instruction := daemon.NewExitImmediatelyInstruction()
|
||||
lazyGitPath := oscommands.GetLazygitPath()
|
||||
return cmdObj.
|
||||
|
|
|
@ -69,7 +69,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
|
|||
// environment variables that suppress an interactive editor
|
||||
func TestRebaseSkipEditorCommand(t *testing.T) {
|
||||
cmdArgs := []string{"git", "blah"}
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc("matches editor env var", func(cmdObj oscommands.ICmdObj) bool {
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc("matches editor env var", func(cmdObj *oscommands.CmdObj) bool {
|
||||
assert.EqualValues(t, cmdArgs, cmdObj.Args())
|
||||
envVars := cmdObj.GetEnvVars()
|
||||
for _, regexStr := range []string{
|
||||
|
|
|
@ -80,7 +80,7 @@ func (self *StashCommands) Hash(index int) (string, error) {
|
|||
return strings.Trim(hash, "\r\n"), err
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj {
|
||||
// "-u" is the same as "--include-untracked", but the latter fails in older git versions for some reason
|
||||
cmdArgs := NewGitCmd("stash").Arg("show").
|
||||
Arg("-p").
|
||||
|
|
|
@ -237,35 +237,35 @@ func (self *SubmoduleCommands) Update(path string) error {
|
|||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkInitCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("init").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkUpdateCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--force").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkUpdateRecursivelyCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkUpdateRecursivelyCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("update", "--init", "--recursive").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
|
||||
func (self *SubmoduleCommands) BulkDeinitCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("submodule").Arg("deinit", "--all", "--force").
|
||||
ToArgv()
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ type PushOpts struct {
|
|||
SetUpstream bool
|
||||
}
|
||||
|
||||
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands.ICmdObj, error) {
|
||||
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommands.CmdObj, error) {
|
||||
if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" {
|
||||
return nil, errors.New(self.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ func (self *SyncCommands) fetchCommandBuilder(fetchAll bool) *GitCommandBuilder
|
|||
ArgIf(self.version.IsAtLeast(2, 29, 0), "--no-write-fetch-head")
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FetchCmdObj(task gocui.Task) oscommands.ICmdObj {
|
||||
func (self *SyncCommands) FetchCmdObj(task gocui.Task) *oscommands.CmdObj {
|
||||
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
|
@ -74,7 +74,7 @@ func (self *SyncCommands) Fetch(task gocui.Task) error {
|
|||
return self.FetchCmdObj(task).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FetchBackgroundCmdObj() oscommands.ICmdObj {
|
||||
func (self *SyncCommands) FetchBackgroundCmdObj() *oscommands.CmdObj {
|
||||
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs)
|
||||
|
|
|
@ -12,14 +12,14 @@ func TestSyncPush(t *testing.T) {
|
|||
type scenario struct {
|
||||
testName string
|
||||
opts PushOpts
|
||||
test func(oscommands.ICmdObj, error)
|
||||
test func(*oscommands.CmdObj, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Push with force disabled",
|
||||
opts: PushOpts{ForceWithLease: false},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
@ -27,7 +27,7 @@ func TestSyncPush(t *testing.T) {
|
|||
{
|
||||
testName: "Push with force-with-lease enabled",
|
||||
opts: PushOpts{ForceWithLease: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
@ -35,7 +35,7 @@ func TestSyncPush(t *testing.T) {
|
|||
{
|
||||
testName: "Push with force enabled",
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
@ -48,7 +48,7 @@ func TestSyncPush(t *testing.T) {
|
|||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "refs/heads/master:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
@ -62,7 +62,7 @@ func TestSyncPush(t *testing.T) {
|
|||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--set-upstream", "origin", "refs/heads/master-local:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
@ -76,7 +76,7 @@ func TestSyncPush(t *testing.T) {
|
|||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "refs/heads/master:master"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
|
@ -89,7 +89,7 @@ func TestSyncPush(t *testing.T) {
|
|||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||
},
|
||||
|
@ -109,14 +109,14 @@ func TestSyncFetch(t *testing.T) {
|
|||
type scenario struct {
|
||||
testName string
|
||||
fetchAllConfig bool
|
||||
test func(oscommands.ICmdObj)
|
||||
test func(*oscommands.CmdObj)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Fetch in foreground (all=false)",
|
||||
fetchAllConfig: false,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.True(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
|
||||
|
@ -125,7 +125,7 @@ func TestSyncFetch(t *testing.T) {
|
|||
{
|
||||
testName: "Fetch in foreground (all=true)",
|
||||
fetchAllConfig: true,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.True(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
|
||||
|
@ -147,14 +147,14 @@ func TestSyncFetchBackground(t *testing.T) {
|
|||
type scenario struct {
|
||||
testName string
|
||||
fetchAllConfig bool
|
||||
test func(oscommands.ICmdObj)
|
||||
test func(*oscommands.CmdObj)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Fetch in background (all=false)",
|
||||
fetchAllConfig: false,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.False(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
|
||||
|
@ -163,7 +163,7 @@ func TestSyncFetchBackground(t *testing.T) {
|
|||
{
|
||||
testName: "Fetch in background (all=true)",
|
||||
fetchAllConfig: true,
|
||||
test: func(cmdObj oscommands.ICmdObj) {
|
||||
test: func(cmdObj *oscommands.CmdObj) {
|
||||
assert.False(t, cmdObj.ShouldLog())
|
||||
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
|
||||
|
|
|
@ -15,7 +15,7 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
|
|||
}
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force bool) oscommands.ICmdObj {
|
||||
func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force bool) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("tag").
|
||||
ArgIf(force, "--force").
|
||||
Arg("--", tagName).
|
||||
|
@ -25,7 +25,7 @@ func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force
|
|||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateAnnotatedObj(tagName, ref, msg string, force bool) oscommands.ICmdObj {
|
||||
func (self *TagCommands) CreateAnnotatedObj(tagName, ref, msg string, force bool) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("tag").Arg(tagName).
|
||||
ArgIf(force, "--force").
|
||||
ArgIf(len(ref) > 0, ref).
|
||||
|
|
|
@ -29,7 +29,7 @@ func NewWorkingTreeCommands(
|
|||
}
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() *oscommands.CmdObj {
|
||||
return self.cmd.New(NewGitCmd("mergetool").ToArgv())
|
||||
}
|
||||
|
||||
|
@ -255,7 +255,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool,
|
|||
return s
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) *oscommands.CmdObj {
|
||||
colorArg := self.UserConfig().Git.Paging.ColorArg
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
|
@ -293,7 +293,7 @@ func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bo
|
|||
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) *oscommands.CmdObj {
|
||||
contextSize := self.AppState.DiffContextSize
|
||||
|
||||
colorArg := self.UserConfig().Git.Paging.ColorArg
|
||||
|
|
|
@ -11,70 +11,6 @@ import (
|
|||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line.
|
||||
type ICmdObj interface {
|
||||
GetCmd() *exec.Cmd
|
||||
// outputs string representation of command. Note that if the command was built
|
||||
// using NewFromArgs, the output won't be quite the same as what you would type
|
||||
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
|
||||
ToString() string
|
||||
|
||||
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
|
||||
Args() []string
|
||||
|
||||
// Set a string to be used as stdin for the command.
|
||||
SetStdin(input string) ICmdObj
|
||||
|
||||
AddEnvVars(...string) ICmdObj
|
||||
GetEnvVars() []string
|
||||
|
||||
// sets the working directory
|
||||
SetWd(string) ICmdObj
|
||||
|
||||
// runs the command and returns an error if any
|
||||
Run() error
|
||||
// runs the command and returns the output as a string, and an error if any
|
||||
RunWithOutput() (string, error)
|
||||
// runs the command and returns stdout and stderr as a string, and an error if any
|
||||
RunWithOutputs() (string, string, error)
|
||||
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
|
||||
RunAndProcessLines(onLine func(line string) (bool, error)) error
|
||||
|
||||
// Be calling DontLog(), we're saying that once we call Run(), we don't want to
|
||||
// log the command in the UI (it'll still be logged in the log file). The general rule
|
||||
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
|
||||
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
|
||||
// we do. The only exception is if we're running a command in the background periodically
|
||||
// like `git fetch`, which technically does mutate stuff but isn't something we need
|
||||
// to notify the user about.
|
||||
DontLog() ICmdObj
|
||||
|
||||
// This returns false if DontLog() was called
|
||||
ShouldLog() bool
|
||||
|
||||
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
|
||||
StreamOutput() ICmdObj
|
||||
// returns true if StreamOutput() was called
|
||||
ShouldStreamOutput() bool
|
||||
|
||||
// if you call this before ShouldStreamOutput we'll consider an error with no
|
||||
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
|
||||
// but adding support is trivial)
|
||||
IgnoreEmptyError() ICmdObj
|
||||
// returns true if IgnoreEmptyError() was called
|
||||
ShouldIgnoreEmptyError() bool
|
||||
|
||||
PromptOnCredentialRequest(task gocui.Task) ICmdObj
|
||||
FailOnCredentialRequest() ICmdObj
|
||||
|
||||
WithMutex(mutex *deadlock.Mutex) ICmdObj
|
||||
Mutex() *deadlock.Mutex
|
||||
|
||||
GetCredentialStrategy() CredentialStrategy
|
||||
GetTask() gocui.Task
|
||||
|
||||
Clone() ICmdObj
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmd *exec.Cmd
|
||||
|
||||
|
@ -86,6 +22,9 @@ type CmdObj struct {
|
|||
// see StreamOutput()
|
||||
streamOutput bool
|
||||
|
||||
// see UsePty()
|
||||
usePty bool
|
||||
|
||||
// see IgnoreEmptyError()
|
||||
ignoreEmptyError bool
|
||||
|
||||
|
@ -112,12 +51,13 @@ const (
|
|||
FAIL
|
||||
)
|
||||
|
||||
var _ ICmdObj = &CmdObj{}
|
||||
|
||||
func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||
return self.cmd
|
||||
}
|
||||
|
||||
// outputs string representation of command. Note that if the command was built
|
||||
// using NewFromArgs, the output won't be quite the same as what you would type
|
||||
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
|
||||
func (self *CmdObj) ToString() string {
|
||||
// if a given arg contains a space, we need to wrap it in quotes
|
||||
quotedArgs := lo.Map(self.cmd.Args, func(arg string, _ int) string {
|
||||
|
@ -130,17 +70,19 @@ func (self *CmdObj) ToString() string {
|
|||
return strings.Join(quotedArgs, " ")
|
||||
}
|
||||
|
||||
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
|
||||
func (self *CmdObj) Args() []string {
|
||||
return self.cmd.Args
|
||||
}
|
||||
|
||||
func (self *CmdObj) SetStdin(input string) ICmdObj {
|
||||
// Set a string to be used as stdin for the command.
|
||||
func (self *CmdObj) SetStdin(input string) *CmdObj {
|
||||
self.cmd.Stdin = strings.NewReader(input)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) *CmdObj {
|
||||
self.cmd.Env = append(self.cmd.Env, vars...)
|
||||
|
||||
return self
|
||||
|
@ -150,75 +92,109 @@ func (self *CmdObj) GetEnvVars() []string {
|
|||
return self.cmd.Env
|
||||
}
|
||||
|
||||
func (self *CmdObj) SetWd(wd string) ICmdObj {
|
||||
// sets the working directory
|
||||
func (self *CmdObj) SetWd(wd string) *CmdObj {
|
||||
self.cmd.Dir = wd
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) DontLog() ICmdObj {
|
||||
// By calling DontLog(), we're saying that once we call Run(), we don't want to
|
||||
// log the command in the UI (it'll still be logged in the log file). The general rule
|
||||
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
|
||||
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
|
||||
// we do. The only exception is if we're running a command in the background periodically
|
||||
// like `git fetch`, which technically does mutate stuff but isn't something we need
|
||||
// to notify the user about.
|
||||
func (self *CmdObj) DontLog() *CmdObj {
|
||||
self.dontLog = true
|
||||
return self
|
||||
}
|
||||
|
||||
// This returns false if DontLog() was called
|
||||
func (self *CmdObj) ShouldLog() bool {
|
||||
return !self.dontLog
|
||||
}
|
||||
|
||||
func (self *CmdObj) StreamOutput() ICmdObj {
|
||||
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
|
||||
func (self *CmdObj) StreamOutput() *CmdObj {
|
||||
self.streamOutput = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// returns true if StreamOutput() was called
|
||||
func (self *CmdObj) ShouldStreamOutput() bool {
|
||||
return self.streamOutput
|
||||
}
|
||||
|
||||
func (self *CmdObj) IgnoreEmptyError() ICmdObj {
|
||||
// when you call this, then call Run(), we'll use a PTY to run the command. Only
|
||||
// has an effect if StreamOutput() was also called. Ignored on Windows.
|
||||
func (self *CmdObj) UsePty() *CmdObj {
|
||||
self.usePty = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// returns true if UsePty() was called
|
||||
func (self *CmdObj) ShouldUsePty() bool {
|
||||
return self.usePty
|
||||
}
|
||||
|
||||
// if you call this before ShouldStreamOutput we'll consider an error with no
|
||||
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
|
||||
// but adding support is trivial)
|
||||
func (self *CmdObj) IgnoreEmptyError() *CmdObj {
|
||||
self.ignoreEmptyError = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// returns true if IgnoreEmptyError() was called
|
||||
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
|
||||
return self.ignoreEmptyError
|
||||
}
|
||||
|
||||
func (self *CmdObj) Mutex() *deadlock.Mutex {
|
||||
return self.mutex
|
||||
}
|
||||
|
||||
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) ICmdObj {
|
||||
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) *CmdObj {
|
||||
self.mutex = mutex
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
|
||||
return self.ignoreEmptyError
|
||||
}
|
||||
|
||||
// runs the command and returns an error if any
|
||||
func (self *CmdObj) Run() error {
|
||||
return self.runner.Run(self)
|
||||
}
|
||||
|
||||
// runs the command and returns the output as a string, and an error if any
|
||||
func (self *CmdObj) RunWithOutput() (string, error) {
|
||||
return self.runner.RunWithOutput(self)
|
||||
}
|
||||
|
||||
// runs the command and returns stdout and stderr as a string, and an error if any
|
||||
func (self *CmdObj) RunWithOutputs() (string, string, error) {
|
||||
return self.runner.RunWithOutputs(self)
|
||||
}
|
||||
|
||||
// runs the command and runs a callback function on each line of the output. If the callback
|
||||
// returns true for the boolean value, we kill the process and return.
|
||||
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
|
||||
return self.runner.RunAndProcessLines(self, onLine)
|
||||
}
|
||||
|
||||
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) ICmdObj {
|
||||
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) *CmdObj {
|
||||
self.credentialStrategy = PROMPT
|
||||
self.usePty = true
|
||||
self.task = task
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
|
||||
func (self *CmdObj) FailOnCredentialRequest() *CmdObj {
|
||||
self.credentialStrategy = FAIL
|
||||
|
||||
return self
|
||||
|
@ -232,7 +208,7 @@ func (self *CmdObj) GetTask() gocui.Task {
|
|||
return self.task
|
||||
}
|
||||
|
||||
func (self *CmdObj) Clone() ICmdObj {
|
||||
func (self *CmdObj) Clone() *CmdObj {
|
||||
clone := &CmdObj{}
|
||||
*clone = *self
|
||||
clone.cmd = cloneCmd(self.cmd)
|
||||
|
|
|
@ -11,11 +11,11 @@ import (
|
|||
|
||||
type ICmdObjBuilder interface {
|
||||
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object.
|
||||
New(args []string) ICmdObj
|
||||
New(args []string) *CmdObj
|
||||
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
|
||||
// shellFunctionsFile is an optional file path that will be sourced before executing the command. Callers should pass
|
||||
// the value of UserConfig.OS.ShellFunctionsFile.
|
||||
NewShell(commandStr string, shellFunctionsFile string) ICmdObj
|
||||
NewShell(commandStr string, shellFunctionsFile string) *CmdObj
|
||||
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
|
||||
Quote(str string) string
|
||||
}
|
||||
|
@ -28,13 +28,13 @@ type CmdObjBuilder struct {
|
|||
// poor man's version of explicitly saying that struct X implements interface Y
|
||||
var _ ICmdObjBuilder = &CmdObjBuilder{}
|
||||
|
||||
func (self *CmdObjBuilder) New(args []string) ICmdObj {
|
||||
func (self *CmdObjBuilder) New(args []string) *CmdObj {
|
||||
cmdObj := self.NewWithEnviron(args, os.Environ())
|
||||
return cmdObj
|
||||
}
|
||||
|
||||
// A command with explicit environment from env
|
||||
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
|
||||
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) *CmdObj {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Env = env
|
||||
|
||||
|
@ -44,7 +44,7 @@ func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
|
|||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string) ICmdObj {
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string) *CmdObj {
|
||||
if len(shellFunctionsFile) > 0 {
|
||||
commandStr = fmt.Sprintf("%ssource %s\n%s", self.platform.PrefixForShellFunctionsFile, shellFunctionsFile, commandStr)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -11,14 +12,15 @@ import (
|
|||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICmdObjRunner interface {
|
||||
Run(cmdObj ICmdObj) error
|
||||
RunWithOutput(cmdObj ICmdObj) (string, error)
|
||||
RunWithOutputs(cmdObj ICmdObj) (string, string, error)
|
||||
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
|
||||
Run(cmdObj *CmdObj) error
|
||||
RunWithOutput(cmdObj *CmdObj) (string, error)
|
||||
RunWithOutputs(cmdObj *CmdObj) (string, string, error)
|
||||
RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error
|
||||
}
|
||||
|
||||
type cmdObjRunner struct {
|
||||
|
@ -28,7 +30,7 @@ type cmdObjRunner struct {
|
|||
|
||||
var _ ICmdObjRunner = &cmdObjRunner{}
|
||||
|
||||
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
func (self *cmdObjRunner) Run(cmdObj *CmdObj) error {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
|
@ -46,7 +48,7 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj *CmdObj) (string, error) {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
|
@ -71,7 +73,7 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
|||
return self.RunWithOutputAux(cmdObj)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutputs(cmdObj *CmdObj) (string, string, error) {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
|
@ -96,7 +98,7 @@ func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error)
|
|||
return self.RunWithOutputsAux(cmdObj)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutputAux(cmdObj *CmdObj) (string, error) {
|
||||
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
|
@ -114,7 +116,7 @@ func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
|
|||
return output, err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, error) {
|
||||
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj *CmdObj) (string, string, error) {
|
||||
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
|
@ -139,7 +141,7 @@ func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, err
|
|||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
func (self *cmdObjRunner) RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
|
@ -190,7 +192,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
|
||||
func (self *cmdObjRunner) logCmdObj(cmdObj *CmdObj) {
|
||||
self.guiIO.logCommandFn(cmdObj.ToString(), true)
|
||||
}
|
||||
|
||||
|
@ -213,7 +215,7 @@ type cmdHandler struct {
|
|||
close func() error
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
|
||||
func (self *cmdObjRunner) runAndStream(cmdObj *CmdObj) error {
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
go func() {
|
||||
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
|
||||
|
@ -222,7 +224,7 @@ func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
|
|||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStreamAux(
|
||||
cmdObj ICmdObj,
|
||||
cmdObj *CmdObj,
|
||||
onRun func(*cmdHandler, io.Writer),
|
||||
) error {
|
||||
cmdWriter := self.guiIO.newCmdWriterFn()
|
||||
|
@ -236,7 +238,13 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
var stderr bytes.Buffer
|
||||
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
|
||||
|
||||
handler, err := self.getCmdHandler(cmd)
|
||||
var handler *cmdHandler
|
||||
var err error
|
||||
if cmdObj.ShouldUsePty() {
|
||||
handler, err = self.getCmdHandlerPty(cmd)
|
||||
} else {
|
||||
handler, err = self.getCmdHandlerNonPty(cmd)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -297,7 +305,7 @@ var failPromptFn = func(CredentialType) <-chan string {
|
|||
return ch
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
|
||||
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj *CmdObj) error {
|
||||
promptFn, err := self.getCredentialPromptFn(cmdObj)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -306,7 +314,7 @@ func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
|
|||
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(CredentialType) <-chan string, error) {
|
||||
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj *CmdObj) (func(CredentialType) <-chan string, error) {
|
||||
switch cmdObj.GetCredentialStrategy() {
|
||||
case PROMPT:
|
||||
return self.guiIO.promptForCredentialFn, nil
|
||||
|
@ -322,7 +330,7 @@ func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(Credential
|
|||
// promptUserForCredential is a function that gets executed when this function detect you need to fill in a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
||||
cmdObj ICmdObj,
|
||||
cmdObj *CmdObj,
|
||||
promptUserForCredential func(CredentialType) <-chan string,
|
||||
) error {
|
||||
// setting the output to english so we can parse it for a username/password request
|
||||
|
@ -410,3 +418,38 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
|
|||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m deadlock.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) getCmdHandlerNonPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
// we define this separately for windows and non-windows given that windows does
|
||||
// not have great PTY support and we need a PTY to handle a credential request
|
||||
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -4,48 +4,10 @@
|
|||
package oscommands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m deadlock.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// because we don't yet have windows support for a pty, we instead just
|
||||
// pass our standard stream handlers and because there's no pty to close
|
||||
// we pass a no-op function for that.
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
}, nil
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
// We don't have PTY support on Windows yet, so we just return a non-PTY handler.
|
||||
return self.getCmdHandlerNonPty(cmd)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ type FakeCmdObjRunner struct {
|
|||
type CmdObjMatcher struct {
|
||||
description string
|
||||
// returns true if the matcher matches the command object
|
||||
test func(ICmdObj) bool
|
||||
test func(*CmdObj) bool
|
||||
|
||||
// output of the command
|
||||
output string
|
||||
|
@ -48,12 +48,12 @@ func (self *FakeCmdObjRunner) remainingExpectedCmds() []CmdObjMatcher {
|
|||
})
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
func (self *FakeCmdObjRunner) Run(cmdObj *CmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj *CmdObj) (string, error) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
|
@ -78,12 +78,12 @@ func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
|
||||
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj *CmdObj) (string, string, error) {
|
||||
output, err := self.RunWithOutput(cmdObj)
|
||||
return output, "", err
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error {
|
||||
output, err := self.RunWithOutput(cmdObj)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -105,7 +105,7 @@ func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(lin
|
|||
return nil
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmdObj) bool, output string, err error) *FakeCmdObjRunner {
|
||||
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj *CmdObj) bool, output string, err error) *FakeCmdObjRunner {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
|
@ -121,7 +121,7 @@ func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmd
|
|||
|
||||
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
|
||||
description := fmt.Sprintf("matches args %s", strings.Join(expectedArgs, " "))
|
||||
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
|
||||
self.ExpectFunc(description, func(cmdObj *CmdObj) bool {
|
||||
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args)
|
||||
}, output, err)
|
||||
|
||||
|
@ -130,7 +130,7 @@ func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, e
|
|||
|
||||
func (self *FakeCmdObjRunner) ExpectGitArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
|
||||
description := fmt.Sprintf("matches git args %s", strings.Join(expectedArgs, " "))
|
||||
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
|
||||
self.ExpectFunc(description, func(cmdObj *CmdObj) bool {
|
||||
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args[1:])
|
||||
}, output, err)
|
||||
|
||||
|
|
|
@ -208,13 +208,13 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
|||
}
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
|
||||
cmds := lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) *exec.Cmd {
|
||||
func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error {
|
||||
cmds := lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) *exec.Cmd {
|
||||
return cmdObj.GetCmd()
|
||||
})
|
||||
|
||||
logCmdStr := strings.Join(
|
||||
lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) string {
|
||||
lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) string {
|
||||
return cmdObj.ToString()
|
||||
}),
|
||||
" | ",
|
||||
|
|
|
@ -281,6 +281,11 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
|
|||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
err = changeCustomCommandStreamAndOutputToOutputEnum(&rootNode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
|
||||
}
|
||||
|
||||
// Add more migrations here...
|
||||
|
||||
if !reflect.DeepEqual(rootNode, originalCopy) {
|
||||
|
@ -341,6 +346,46 @@ func changeCommitPrefixesMap(rootNode *yaml.Node) error {
|
|||
})
|
||||
}
|
||||
|
||||
func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node) error {
|
||||
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
|
||||
// We are being lazy here and rely on the fact that the only mapping
|
||||
// nodes in the tree under customCommands are actual custom commands. If
|
||||
// this ever changes (e.g. because we add a struct field to
|
||||
// customCommand), then we need to change this to iterate properly.
|
||||
if strings.HasPrefix(path, "customCommands[") && node.Kind == yaml.MappingNode {
|
||||
output := ""
|
||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "subprocess"); streamKey != nil {
|
||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" {
|
||||
output = "terminal"
|
||||
}
|
||||
}
|
||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "stream"); streamKey != nil {
|
||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
||||
output = "log"
|
||||
}
|
||||
}
|
||||
if streamKey, streamValue := yaml_utils.RemoveKey(node, "showOutput"); streamKey != nil {
|
||||
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
|
||||
output = "popup"
|
||||
}
|
||||
}
|
||||
if output != "" {
|
||||
outputKeyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: "output",
|
||||
Tag: "!!str",
|
||||
}
|
||||
outputValueNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: output,
|
||||
Tag: "!!str",
|
||||
}
|
||||
node.Content = append(node.Content, outputKeyNode, outputValueNode)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetDebug() bool {
|
||||
return c.debug
|
||||
}
|
||||
|
|
|
@ -88,6 +88,92 @@ git:
|
|||
}
|
||||
}
|
||||
|
||||
func TestCustomCommandsOutputMigration(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expected: "",
|
||||
}, {
|
||||
name: "Convert subprocess to output=terminal",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
subprocess: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: terminal
|
||||
`,
|
||||
}, {
|
||||
name: "Convert stream to output=log",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
stream: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: log
|
||||
`,
|
||||
}, {
|
||||
name: "Convert showOutput to output=popup",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
showOutput: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: popup
|
||||
`,
|
||||
}, {
|
||||
name: "Subprocess wins over the other two",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
subprocess: true
|
||||
stream: true
|
||||
showOutput: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: terminal
|
||||
`,
|
||||
}, {
|
||||
name: "Stream wins over showOutput",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
stream: true
|
||||
showOutput: true
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
output: log
|
||||
`,
|
||||
}, {
|
||||
name: "Explicitly setting to false doesn't create an output=none key",
|
||||
input: `customCommands:
|
||||
- command: echo 'hello'
|
||||
subprocess: false
|
||||
stream: false
|
||||
showOutput: false
|
||||
`,
|
||||
expected: `customCommands:
|
||||
- command: echo 'hello'
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
actual, err := computeMigratedConfig("path doesn't matter", []byte(s.input))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var largeConfiguration = []byte(`
|
||||
# Config relating to the Lazygit UI
|
||||
gui:
|
||||
|
|
|
@ -637,22 +637,15 @@ type CustomCommand struct {
|
|||
Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"`
|
||||
// The command to run (using Go template syntax for placeholder values)
|
||||
Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"`
|
||||
// If true, run the command in a subprocess (e.g. if the command requires user input)
|
||||
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
|
||||
Subprocess *bool `yaml:"subprocess"`
|
||||
// A list of prompts that will request user input before running the final command
|
||||
Prompts []CustomCommandPrompt `yaml:"prompts"`
|
||||
// Text to display while waiting for command to finish
|
||||
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`
|
||||
// Label for the custom command when displayed in the keybindings menu
|
||||
Description string `yaml:"description"`
|
||||
// If true, stream the command's output to the Command Log panel
|
||||
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
|
||||
Stream *bool `yaml:"stream"`
|
||||
// If true, show the command's output in a popup within Lazygit
|
||||
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
|
||||
ShowOutput *bool `yaml:"showOutput"`
|
||||
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
|
||||
// Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup.
|
||||
Output string `yaml:"output" jsonschema:"enum=none,enum=terminal,enum=log,enum=logWithPty,enum=popup"`
|
||||
// The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title.
|
||||
OutputTitle string `yaml:"outputTitle"`
|
||||
// Actions to take after the command has completed
|
||||
// [dev] Pointer so that we can tell whether it appears in the config file
|
||||
|
|
|
@ -101,21 +101,29 @@ func validateCustomCommands(customCommands []CustomCommand) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(customCommand.CommandMenu) > 0 &&
|
||||
(len(customCommand.Context) > 0 ||
|
||||
if len(customCommand.CommandMenu) > 0 {
|
||||
if len(customCommand.Context) > 0 ||
|
||||
len(customCommand.Command) > 0 ||
|
||||
customCommand.Subprocess != nil ||
|
||||
len(customCommand.Prompts) > 0 ||
|
||||
len(customCommand.LoadingText) > 0 ||
|
||||
customCommand.Stream != nil ||
|
||||
customCommand.ShowOutput != nil ||
|
||||
len(customCommand.Output) > 0 ||
|
||||
len(customCommand.OutputTitle) > 0 ||
|
||||
customCommand.After != nil) {
|
||||
commandRef := ""
|
||||
if len(customCommand.Key) > 0 {
|
||||
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
|
||||
customCommand.After != nil {
|
||||
commandRef := ""
|
||||
if len(customCommand.Key) > 0 {
|
||||
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
|
||||
}
|
||||
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
|
||||
}
|
||||
|
||||
if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := validateEnum("customCommand.output", customCommand.Output,
|
||||
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -74,6 +74,46 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
|||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command keybinding in sub menu",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Description: "My Custom Commands",
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: value, Command: "echo 'hello'", Context: "global"},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "<disabled>", valid: true},
|
||||
{value: "q", valid: true},
|
||||
{value: "<c-c>", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command output",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Output: value,
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "none", valid: true},
|
||||
{value: "terminal", valid: true},
|
||||
{value: "log", valid: true},
|
||||
{value: "logWithPty", valid: true},
|
||||
{value: "popup", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command sub menu",
|
||||
setup: func(config *UserConfig, _ string) {
|
||||
|
@ -97,7 +137,7 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
|||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Context: "global",
|
||||
Context: "global", // context is not allowed for submenus
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: "1", Command: "echo 'hello'", Context: "global"},
|
||||
},
|
||||
|
@ -111,11 +151,10 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
|||
{
|
||||
name: "Custom command sub menu",
|
||||
setup: func(config *UserConfig, _ string) {
|
||||
falseVal := false
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Subprocess: &falseVal,
|
||||
Key: "X",
|
||||
LoadingText: "loading", // other properties are not allowed for submenus (using loadingText as an example)
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: "1", Command: "echo 'hello'", Context: "global"},
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ func NewGpgHelper(c *HelperCommon) *GpgHelper {
|
|||
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
|
||||
// fix this bug, or just stop running subprocesses from within there, given that
|
||||
// we don't need to see a loading status if we're in a subprocess.
|
||||
func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, configKey git_commands.GpgConfigKey, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
func (self *GpgHelper) WithGpgHandling(cmdObj *oscommands.CmdObj, configKey git_commands.GpgConfigKey, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
useSubprocess := self.c.Git().Config.NeedsGpgSubprocess(configKey)
|
||||
if useSubprocess {
|
||||
success, err := self.c.RunSubprocess(cmdObj)
|
||||
|
@ -42,7 +42,7 @@ func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, configKey git_
|
|||
}
|
||||
}
|
||||
|
||||
func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
func (self *GpgHelper) runAndStream(cmdObj *oscommands.CmdObj, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
|
||||
return self.c.WithWaitingStatus(waitingStatus, func(gocui.Task) error {
|
||||
if err := cmdObj.StreamOutput().Run(); err != nil {
|
||||
_ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: refreshScope})
|
||||
|
|
|
@ -24,7 +24,7 @@ func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper, gpg *GpgHelper
|
|||
|
||||
func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error {
|
||||
doCreateTag := func(tagName string, description string, force bool) error {
|
||||
var command oscommands.ICmdObj
|
||||
var command *oscommands.CmdObj
|
||||
if description != "" || self.c.Git().Config.GetGpgTagSign() {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
|
||||
command = self.c.Git().Tag.CreateAnnotatedObj(tagName, ref, description, force)
|
||||
|
|
|
@ -932,7 +932,7 @@ func (gui *Gui) checkForDeprecatedEditConfigs() {
|
|||
}
|
||||
|
||||
// returns whether command exited without error or not
|
||||
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdObj) error {
|
||||
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *oscommands.CmdObj) error {
|
||||
_, err := gui.runSubprocessWithSuspense(subprocess)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -946,7 +946,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdOb
|
|||
}
|
||||
|
||||
// returns whether command exited without error or not
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, error) {
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess *oscommands.CmdObj) (bool, error) {
|
||||
gui.Mutexes.SubprocessMutex.Lock()
|
||||
defer gui.Mutexes.SubprocessMutex.Unlock()
|
||||
|
||||
|
@ -970,7 +970,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error { //nolint:unparam
|
||||
func (gui *Gui) runSubprocess(cmdObj *oscommands.CmdObj) error { //nolint:unparam
|
||||
gui.LogCommand(cmdObj.ToString(), true)
|
||||
|
||||
subprocess := cmdObj.GetCmd()
|
||||
|
|
|
@ -34,11 +34,11 @@ func (self *guiCommon) PostRefreshUpdate(context types.Context) {
|
|||
self.gui.postRefreshUpdate(context)
|
||||
}
|
||||
|
||||
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj oscommands.ICmdObj) error {
|
||||
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj *oscommands.CmdObj) error {
|
||||
return self.gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
|
||||
}
|
||||
|
||||
func (self *guiCommon) RunSubprocess(cmdObj oscommands.ICmdObj) (bool, error) {
|
||||
func (self *guiCommon) RunSubprocess(cmdObj *oscommands.CmdObj) (bool, error) {
|
||||
return self.gui.runSubprocessWithSuspense(cmdObj)
|
||||
}
|
||||
|
||||
|
|
|
@ -262,7 +262,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
|||
|
||||
cmdObj := self.c.OS().Cmd.NewShell(cmdStr, self.c.UserConfig().OS.ShellFunctionsFile)
|
||||
|
||||
if customCommand.Subprocess != nil && *customCommand.Subprocess {
|
||||
if customCommand.Output == "terminal" {
|
||||
return self.c.RunSubprocessAndRefresh(cmdObj)
|
||||
}
|
||||
|
||||
|
@ -274,9 +274,12 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
|||
return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CustomCommand)
|
||||
|
||||
if customCommand.Stream != nil && *customCommand.Stream {
|
||||
if customCommand.Output == "log" || customCommand.Output == "logWithPty" {
|
||||
cmdObj.StreamOutput()
|
||||
}
|
||||
if customCommand.Output == "logWithPty" {
|
||||
cmdObj.UsePty()
|
||||
}
|
||||
output, err := cmdObj.RunWithOutput()
|
||||
|
||||
if refreshErr := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
|
||||
|
@ -291,7 +294,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
|||
return err
|
||||
}
|
||||
|
||||
if customCommand.ShowOutput != nil && *customCommand.ShowOutput {
|
||||
if customCommand.Output == "popup" {
|
||||
if strings.TrimSpace(output) == "" {
|
||||
output = self.c.Tr.EmptyOutput
|
||||
}
|
||||
|
|
|
@ -53,8 +53,8 @@ type IGuiCommon interface {
|
|||
GetViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager
|
||||
|
||||
// returns true if command completed successfully
|
||||
RunSubprocess(cmdObj oscommands.ICmdObj) (bool, error)
|
||||
RunSubprocessAndRefresh(oscommands.ICmdObj) error
|
||||
RunSubprocess(cmdObj *oscommands.CmdObj) (bool, error)
|
||||
RunSubprocessAndRefresh(*oscommands.CmdObj) error
|
||||
|
||||
Context() IContextMgr
|
||||
ContextForKey(key ContextKey) Context
|
||||
|
|
|
@ -2169,6 +2169,7 @@ git:
|
|||
autoForwardBranches: none
|
||||
|
||||
If, on the other hand, you want this even for feature branches, you can set it to 'allBranches' instead.`,
|
||||
"0.51.0": `- The 'subprocess', 'stream', and 'showOutput' fields of custom commands have been replaced by a single 'output' field. This should be transparent, if you used these in your config file it should have been automatically updated for you. There's one notable change though: the 'stream' field used to mean both that the command's output would be streamed to the command log, and that the command would be run in a pseudo terminal (pty). We converted this to 'output: log', which means that the command's output will be streamed to the command log, but not use a pty, on the assumption that this is what most people wanted. If you do actually want to run a command in a pty, you can change this to 'output: logWithPty' instead.`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,19 +15,18 @@ var ShowOutputInPanel = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
shell.EmptyCommit("my change")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
trueVal := true
|
||||
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "X",
|
||||
Context: "commits",
|
||||
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
|
||||
ShowOutput: &trueVal,
|
||||
Key: "X",
|
||||
Context: "commits",
|
||||
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
|
||||
Output: "popup",
|
||||
},
|
||||
{
|
||||
Key: "Y",
|
||||
Context: "commits",
|
||||
Command: "printf '%s' '{{ .SelectedLocalCommit.Name }}'",
|
||||
ShowOutput: &trueVal,
|
||||
Output: "popup",
|
||||
OutputTitle: "Subject of commit {{ .SelectedLocalCommit.Hash }}",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -4,92 +4,11 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// takes a yaml document in bytes, a path to a key, and a value to set. The value must be a scalar.
|
||||
func UpdateYamlValue(yamlBytes []byte, path []string, value string) ([]byte, error) {
|
||||
// Parse the YAML file.
|
||||
var node yaml.Node
|
||||
err := yaml.Unmarshal(yamlBytes, &node)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML: %w", err)
|
||||
}
|
||||
|
||||
// Empty document: need to create the top-level map ourselves
|
||||
if len(node.Content) == 0 {
|
||||
node.Content = append(node.Content, &yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
})
|
||||
}
|
||||
|
||||
body := node.Content[0]
|
||||
|
||||
if body.Kind != yaml.MappingNode {
|
||||
return yamlBytes, errors.New("yaml document is not a dictionary")
|
||||
}
|
||||
|
||||
if didChange, err := updateYamlNode(body, path, value); err != nil || !didChange {
|
||||
return yamlBytes, err
|
||||
}
|
||||
|
||||
// Convert the updated YAML node back to YAML bytes.
|
||||
updatedYAMLBytes, err := YamlMarshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert YAML node to bytes: %w", err)
|
||||
}
|
||||
|
||||
return updatedYAMLBytes, nil
|
||||
}
|
||||
|
||||
// Recursive function to update the YAML node.
|
||||
func updateYamlNode(node *yaml.Node, path []string, value string) (bool, error) {
|
||||
if len(path) == 0 {
|
||||
if node.Kind != yaml.ScalarNode {
|
||||
return false, errors.New("yaml node is not a scalar")
|
||||
}
|
||||
if node.Value != value {
|
||||
node.Value = value
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return false, errors.New("yaml node in path is not a dictionary")
|
||||
}
|
||||
|
||||
key := path[0]
|
||||
if _, valueNode := lookupKey(node, key); valueNode != nil {
|
||||
return updateYamlNode(valueNode, path[1:], value)
|
||||
}
|
||||
|
||||
// if the key doesn't exist, we'll add it
|
||||
|
||||
// at end of path: add the new key, done
|
||||
if len(path) == 1 {
|
||||
node.Content = append(node.Content, &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: key,
|
||||
}, &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: value,
|
||||
})
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// otherwise, create the missing intermediate node and continue
|
||||
newNode := &yaml.Node{
|
||||
Kind: yaml.MappingNode,
|
||||
}
|
||||
node.Content = append(node.Content, &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: key,
|
||||
}, newNode)
|
||||
return updateYamlNode(newNode, path[1:], value)
|
||||
}
|
||||
|
||||
func lookupKey(node *yaml.Node, key string) (*yaml.Node, *yaml.Node) {
|
||||
for i := 0; i < len(node.Content)-1; i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
|
@ -100,6 +19,19 @@ func lookupKey(node *yaml.Node, key string) (*yaml.Node, *yaml.Node) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// Returns the key and value if they were present
|
||||
func RemoveKey(node *yaml.Node, key string) (*yaml.Node, *yaml.Node) {
|
||||
for i := 0; i < len(node.Content)-1; i += 2 {
|
||||
if node.Content[i].Value == key {
|
||||
key, value := node.Content[i], node.Content[i+1]
|
||||
node.Content = slices.Delete(node.Content, i, i+2)
|
||||
return key, value
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Walks a yaml document from the root node to the specified path, and then applies the transformation to that node.
|
||||
// If the requested path is not defined in the document, no changes are made to the document.
|
||||
func TransformNode(rootNode *yaml.Node, path []string, transform func(node *yaml.Node) error) error {
|
||||
|
|
|
@ -8,113 +8,6 @@ import (
|
|||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestUpdateYamlValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
path []string
|
||||
value string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "update value",
|
||||
in: "foo: bar\n",
|
||||
path: []string{"foo"},
|
||||
value: "baz",
|
||||
expectedOut: "foo: baz\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "add new key and value",
|
||||
in: "foo: bar\n",
|
||||
path: []string{"foo2"},
|
||||
value: "baz",
|
||||
expectedOut: "foo: bar\nfoo2: baz\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "add new key and value when document was empty",
|
||||
in: "",
|
||||
path: []string{"foo"},
|
||||
value: "bar",
|
||||
expectedOut: "foo: bar\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "preserve inline comment",
|
||||
in: "foo: bar # my comment\n",
|
||||
path: []string{"foo2"},
|
||||
value: "baz",
|
||||
expectedOut: "foo: bar # my comment\nfoo2: baz\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "nested update",
|
||||
in: "foo:\n bar: baz\n",
|
||||
path: []string{"foo", "bar"},
|
||||
value: "qux",
|
||||
expectedOut: "foo:\n bar: qux\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "nested where parents doesn't exist yet",
|
||||
in: "",
|
||||
path: []string{"foo", "bar", "baz"},
|
||||
value: "qux",
|
||||
expectedOut: "foo:\n bar:\n baz: qux\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "don't rewrite file if value didn't change",
|
||||
in: "foo:\n bar: baz\n",
|
||||
path: []string{"foo", "bar"},
|
||||
value: "baz",
|
||||
expectedOut: "foo:\n bar: baz\n",
|
||||
expectedErr: "",
|
||||
},
|
||||
|
||||
// Error cases
|
||||
{
|
||||
name: "existing document is not a dictionary",
|
||||
in: "42\n",
|
||||
path: []string{"foo"},
|
||||
value: "bar",
|
||||
expectedOut: "42\n",
|
||||
expectedErr: "yaml document is not a dictionary",
|
||||
},
|
||||
{
|
||||
name: "trying to update a note that is not a scalar",
|
||||
in: "foo: [1, 2, 3]\n",
|
||||
path: []string{"foo"},
|
||||
value: "bar",
|
||||
expectedOut: "foo: [1, 2, 3]\n",
|
||||
expectedErr: "yaml node is not a scalar",
|
||||
},
|
||||
{
|
||||
name: "not all path elements are dictionaries",
|
||||
in: "foo:\n bar: [1, 2, 3]\n",
|
||||
path: []string{"foo", "bar", "baz"},
|
||||
value: "qux",
|
||||
expectedOut: "foo:\n bar: [1, 2, 3]\n",
|
||||
expectedErr: "yaml node in path is not a dictionary",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
out, actualErr := UpdateYamlValue([]byte(test.in), test.path, test.value)
|
||||
if test.expectedErr == "" {
|
||||
assert.NoError(t, actualErr)
|
||||
} else {
|
||||
assert.EqualError(t, actualErr, test.expectedErr)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expectedOut, string(out))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameYamlKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -389,6 +282,55 @@ foo:
|
|||
}
|
||||
}
|
||||
|
||||
func TestRemoveKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
key string
|
||||
expectedOut string
|
||||
expectedRemovedKey string
|
||||
expectedRemovedValue string
|
||||
}{
|
||||
{
|
||||
name: "Key not present",
|
||||
in: "foo: 1",
|
||||
key: "bar",
|
||||
},
|
||||
{
|
||||
name: "Key present",
|
||||
in: "foo: 1\nbar: 2\nbaz: 3\n",
|
||||
key: "bar",
|
||||
expectedOut: "foo: 1\nbaz: 3\n",
|
||||
expectedRemovedKey: "bar",
|
||||
expectedRemovedValue: "2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
node := unmarshalForTest(t, test.in)
|
||||
removedKey, removedValue := RemoveKey(node.Content[0], test.key)
|
||||
if test.expectedOut == "" {
|
||||
unmodifiedOriginal := unmarshalForTest(t, test.in)
|
||||
assert.Equal(t, unmodifiedOriginal, node)
|
||||
} else {
|
||||
result := marshalForTest(t, &node)
|
||||
assert.Equal(t, test.expectedOut, result)
|
||||
}
|
||||
if test.expectedRemovedKey == "" {
|
||||
assert.Nil(t, removedKey)
|
||||
} else {
|
||||
assert.Equal(t, test.expectedRemovedKey, removedKey.Value)
|
||||
}
|
||||
if test.expectedRemovedValue == "" {
|
||||
assert.Nil(t, removedValue)
|
||||
} else {
|
||||
assert.Equal(t, test.expectedRemovedValue, removedValue.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalForTest(t *testing.T, input string) yaml.Node {
|
||||
t.Helper()
|
||||
var node yaml.Node
|
||||
|
|
|
@ -96,10 +96,6 @@
|
|||
"git fetch {{.Form.Remote}} {{.Form.Branch}} \u0026\u0026 git checkout FETCH_HEAD"
|
||||
]
|
||||
},
|
||||
"subprocess": {
|
||||
"type": "boolean",
|
||||
"description": "If true, run the command in a subprocess (e.g. if the command requires user input)"
|
||||
},
|
||||
"prompts": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/CustomCommandPrompt"
|
||||
|
@ -118,17 +114,20 @@
|
|||
"type": "string",
|
||||
"description": "Label for the custom command when displayed in the keybindings menu"
|
||||
},
|
||||
"stream": {
|
||||
"type": "boolean",
|
||||
"description": "If true, stream the command's output to the Command Log panel"
|
||||
},
|
||||
"showOutput": {
|
||||
"type": "boolean",
|
||||
"description": "If true, show the command's output in a popup within Lazygit"
|
||||
"output": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"terminal",
|
||||
"log",
|
||||
"logWithPty",
|
||||
"popup"
|
||||
],
|
||||
"description": "Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup."
|
||||
},
|
||||
"outputTitle": {
|
||||
"type": "string",
|
||||
"description": "The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title."
|
||||
"description": "The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title."
|
||||
},
|
||||
"after": {
|
||||
"$ref": "#/$defs/CustomCommandAfterHook",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue