mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-06 05:57:38 -04:00
An external diff command rides on the git command as a diff.external config, which means it is fixed when the command is built. That is before the layout pass, so the width the renderer is to lay out for isn't known yet, and the command can't be told about it. Pass it as GIT_EXTERNAL_DIFF from the render instead, where the width is known and where a stdin filter is already handed to git the same way. git ranks the variable exactly as it ranks the config, behind a per-path diff driver from .gitattributes, so a repository that defines one still gets it (verified on git 2.22.5 and 2.55). An empty command means the user wants their own git config to apply, so leave the variable unset for that; git takes it being set at all as an instruction. The renderer command leaves the git arguments, so it also leaves the key that says which diff a render is of. Cycling between two external diff renderers now keeps the view's place, the way cycling between two stdin filters already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
499 lines
16 KiB
Go
499 lines
16 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestCommitRewordCommit(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
summary string
|
|
description string
|
|
}
|
|
scenarios := []scenario{
|
|
{
|
|
"Single line reword",
|
|
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
|
|
"test",
|
|
"",
|
|
},
|
|
{
|
|
"Multi line reword",
|
|
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
|
|
"test",
|
|
"line 2\nline 3",
|
|
},
|
|
}
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
|
|
|
assert.NoError(t, instance.RewordLastCommit(s.summary, s.description).Run())
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitResetToCommit(t *testing.T) {
|
|
runner := oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--hard", "78976bc"}, "", nil)
|
|
|
|
instance := buildCommitCommands(commonDeps{runner: runner})
|
|
|
|
assert.NoError(t, instance.ResetToCommit("78976bc", "hard", []string{}))
|
|
runner.CheckForMissingCalls()
|
|
}
|
|
|
|
func TestCommitCommitCmdObj(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
summary string
|
|
forceSkipHooks bool
|
|
description string
|
|
configSignoff bool
|
|
configSkipHookPrefix string
|
|
expectedArgs []string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Commit",
|
|
summary: "test",
|
|
forceSkipHooks: false,
|
|
configSignoff: false,
|
|
configSkipHookPrefix: "",
|
|
expectedArgs: []string{"commit", "-m", "test"},
|
|
},
|
|
{
|
|
testName: "Commit with --no-verify flag < only prefix",
|
|
summary: "WIP: test",
|
|
forceSkipHooks: false,
|
|
configSignoff: false,
|
|
configSkipHookPrefix: "WIP",
|
|
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
|
|
},
|
|
{
|
|
testName: "Commit with --no-verify flag < skip flag and prefix",
|
|
summary: "WIP: test",
|
|
forceSkipHooks: true,
|
|
configSignoff: false,
|
|
configSkipHookPrefix: "WIP",
|
|
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
|
|
},
|
|
{
|
|
testName: "Commit with --no-verify flag < skip flag no prefix",
|
|
summary: "test",
|
|
forceSkipHooks: true,
|
|
configSignoff: false,
|
|
configSkipHookPrefix: "WIP",
|
|
expectedArgs: []string{"commit", "--no-verify", "-m", "test"},
|
|
},
|
|
{
|
|
testName: "Commit with multiline message",
|
|
summary: "line1",
|
|
forceSkipHooks: false,
|
|
description: "line2",
|
|
configSignoff: false,
|
|
configSkipHookPrefix: "",
|
|
expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"},
|
|
},
|
|
{
|
|
testName: "Commit with signoff",
|
|
summary: "test",
|
|
forceSkipHooks: false,
|
|
configSignoff: true,
|
|
configSkipHookPrefix: "",
|
|
expectedArgs: []string{"commit", "--signoff", "-m", "test"},
|
|
},
|
|
{
|
|
testName: "Commit with signoff and no-verify",
|
|
summary: "WIP: test",
|
|
forceSkipHooks: true,
|
|
configSignoff: true,
|
|
configSkipHookPrefix: "WIP",
|
|
expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
userConfig := config.GetDefaultConfig()
|
|
userConfig.Git.Commit.SignOff = s.configSignoff
|
|
userConfig.Git.SkipHookPrefix = s.configSkipHookPrefix
|
|
|
|
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil)
|
|
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
|
|
|
assert.NoError(t, instance.CommitCmdObj(s.summary, s.description, s.forceSkipHooks).Run())
|
|
runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitCommitEditorCmdObj(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
configSignoff bool
|
|
expected []string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Commit using editor",
|
|
configSignoff: false,
|
|
expected: []string{"commit"},
|
|
},
|
|
{
|
|
testName: "Commit with --signoff",
|
|
configSignoff: true,
|
|
expected: []string{"commit", "--signoff"},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
userConfig := config.GetDefaultConfig()
|
|
userConfig.Git.Commit.SignOff = s.configSignoff
|
|
|
|
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil)
|
|
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
|
|
|
assert.NoError(t, instance.CommitEditorCmdObj().Run())
|
|
runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitCreateFixupCommit(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
hash string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "valid case",
|
|
hash: "12345",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"commit", "--fixup=12345"}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
|
s.test(instance.CreateFixupCommit(s.hash))
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitCreateAmendCommit(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
originalSubject string
|
|
newSubject string
|
|
newDescription string
|
|
includeFileChanges bool
|
|
runner *oscommands.FakeCmdObjRunner
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "subject only",
|
|
originalSubject: "original subject",
|
|
newSubject: "new subject",
|
|
newDescription: "",
|
|
includeFileChanges: true,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject"}, "", nil),
|
|
},
|
|
{
|
|
testName: "subject and description",
|
|
originalSubject: "original subject",
|
|
newSubject: "new subject",
|
|
newDescription: "new description",
|
|
includeFileChanges: true,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject\n\nnew description"}, "", nil),
|
|
},
|
|
{
|
|
testName: "without file changes",
|
|
originalSubject: "original subject",
|
|
newSubject: "new subject",
|
|
newDescription: "",
|
|
includeFileChanges: false,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"commit", "-m", "amend! original subject", "-m", "new subject", "--only", "--allow-empty"}, "", nil),
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
|
err := instance.CreateAmendCommit(s.originalSubject, s.newSubject, s.newDescription, s.includeFileChanges)
|
|
assert.NoError(t, err)
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitShowCmdObj(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
filterPaths []string
|
|
contextSize uint64
|
|
similarityThreshold int
|
|
ignoreWhitespace bool
|
|
diffRendererConfig *config.DiffRendererConfig
|
|
expected []string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Default case without filter path",
|
|
filterPaths: []string{},
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
ignoreWhitespace: false,
|
|
diffRendererConfig: nil,
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
|
},
|
|
{
|
|
testName: "Default case with filter path",
|
|
filterPaths: []string{"file.txt"},
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
ignoreWhitespace: false,
|
|
diffRendererConfig: nil,
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--", "file.txt"},
|
|
},
|
|
{
|
|
testName: "Show diff with custom context size",
|
|
filterPaths: []string{},
|
|
contextSize: 77,
|
|
similarityThreshold: 50,
|
|
ignoreWhitespace: false,
|
|
diffRendererConfig: nil,
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--unified=77", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
|
},
|
|
{
|
|
testName: "Show diff with custom similarity threshold",
|
|
filterPaths: []string{},
|
|
contextSize: 3,
|
|
similarityThreshold: 33,
|
|
ignoreWhitespace: false,
|
|
diffRendererConfig: nil,
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--unified=3", "--find-renames=33%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
|
},
|
|
{
|
|
testName: "Show diff, ignoring whitespace",
|
|
filterPaths: []string{},
|
|
contextSize: 77,
|
|
similarityThreshold: 50,
|
|
ignoreWhitespace: true,
|
|
diffRendererConfig: nil,
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--unified=77", "--ignore-all-space", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
|
},
|
|
{
|
|
testName: "Show diff with external diff command",
|
|
filterPaths: []string{},
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
ignoreWhitespace: false,
|
|
diffRendererConfig: &config.DiffRendererConfig{Type: "extDiff", Command: "difft --color=always"},
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
|
},
|
|
{
|
|
testName: "Show diff using git's external diff config",
|
|
filterPaths: []string{},
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
ignoreWhitespace: false,
|
|
diffRendererConfig: &config.DiffRendererConfig{Type: "extDiff"},
|
|
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
userConfig := config.GetDefaultConfig()
|
|
if s.diffRendererConfig != nil {
|
|
userConfig.Git.DiffRenderers = []config.DiffRendererConfig{*s.diffRendererConfig}
|
|
}
|
|
userConfig.Git.IgnoreWhitespaceInDiffView = s.ignoreWhitespace
|
|
userConfig.Git.DiffContextSize = s.contextSize
|
|
userConfig.Git.RenameSimilarityThreshold = s.similarityThreshold
|
|
|
|
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil)
|
|
repoPaths := RepoPaths{
|
|
worktreePath: "/path/to/worktree",
|
|
}
|
|
instance := buildCommitCommands(commonDeps{userConfig: userConfig, appState: &config.AppState{}, runner: runner, repoPaths: &repoPaths})
|
|
|
|
assert.NoError(t, instance.ShowCmdObj("1234567890", s.filterPaths).Run())
|
|
runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCommitMsg(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
input string
|
|
expectedOutput string
|
|
}
|
|
scenarios := []scenario{
|
|
{
|
|
"empty",
|
|
``,
|
|
``,
|
|
},
|
|
{
|
|
"no line breaks (single line)",
|
|
`use generics to DRY up context code`,
|
|
`use generics to DRY up context code`,
|
|
},
|
|
{
|
|
"with line breaks",
|
|
`Merge pull request #1750 from mark2185/fix-issue-template
|
|
|
|
'git-rev parse' should be 'git rev-parse'`,
|
|
`Merge pull request #1750 from mark2185/fix-issue-template
|
|
|
|
'git-rev parse' should be 'git rev-parse'`,
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildCommitCommands(commonDeps{
|
|
runner: oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1", "deadbeef"}, s.input, nil),
|
|
})
|
|
|
|
output, err := instance.GetCommitMessage("deadbeef")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, s.expectedOutput, output)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCommitMessageFromHistory(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(string, error)
|
|
}
|
|
scenarios := []scenario{
|
|
{
|
|
"Empty message",
|
|
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"log", "-1", "--skip=2", "--pretty=%H"}, "", nil).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1"}, "", nil),
|
|
func(output string, err error) {
|
|
assert.Error(t, err)
|
|
},
|
|
},
|
|
{
|
|
"Default case to retrieve a commit in history",
|
|
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"log", "-1", "--skip=2", "--pretty=%H"}, "hash3 \n", nil).ExpectGitArgs([]string{"-c", "log.showsignature=false", "log", "--format=%B", "--max-count=1", "hash3"}, `use generics to DRY up context code`, nil),
|
|
func(output string, err error) {
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "use generics to DRY up context code", output)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
|
|
|
output, err := instance.GetCommitMessageFromHistory(2)
|
|
|
|
s.test(output, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddCoAuthorToMessage(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
message string
|
|
expectedResult string
|
|
}{
|
|
{
|
|
// This never happens, I think it isn't possible to create a commit
|
|
// with an empty message. Just including it for completeness.
|
|
name: "Empty message",
|
|
message: "",
|
|
expectedResult: "\n\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
{
|
|
name: "Just a subject, no body",
|
|
message: "Subject",
|
|
expectedResult: "Subject\n\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
{
|
|
name: "Subject and body",
|
|
message: "Subject\n\nBody",
|
|
expectedResult: "Subject\n\nBody\n\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
{
|
|
name: "Body already ending with a Co-authored-by line",
|
|
message: "Subject\n\nBody\n\nCo-authored-by: Jane Smith <jane@smith.com>",
|
|
expectedResult: "Subject\n\nBody\n\nCo-authored-by: Jane Smith <jane@smith.com>\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
}
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
result := AddCoAuthorToMessage(s.message, "John Doe <john@doe.com>")
|
|
assert.Equal(t, s.expectedResult, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddCoAuthorToDescription(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
description string
|
|
expectedResult string
|
|
}{
|
|
{
|
|
name: "Empty description",
|
|
description: "",
|
|
expectedResult: "Co-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
{
|
|
name: "Non-empty description",
|
|
description: "Body",
|
|
expectedResult: "Body\n\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
{
|
|
name: "Description already ending with a Co-authored-by line",
|
|
description: "Body\n\nCo-authored-by: Jane Smith <jane@smith.com>",
|
|
expectedResult: "Body\n\nCo-authored-by: Jane Smith <jane@smith.com>\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
{
|
|
name: "Description with trailing newlines",
|
|
description: "Body\n\n",
|
|
expectedResult: "Body\n\nCo-authored-by: John Doe <john@doe.com>",
|
|
},
|
|
}
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
result := AddCoAuthorToDescription(s.description, "John Doe <john@doe.com>")
|
|
assert.Equal(t, s.expectedResult, result)
|
|
})
|
|
}
|
|
}
|