mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
912 lines
29 KiB
Go
912 lines
29 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/samber/lo"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestWorkingTreeStageFile(t *testing.T) {
|
|
runner := oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"add", "--", "test.txt"}, "", nil)
|
|
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
|
|
|
|
assert.NoError(t, instance.StageFile("test.txt"))
|
|
runner.CheckForMissingCalls()
|
|
}
|
|
|
|
func TestWorkingTreeStageFiles(t *testing.T) {
|
|
runner := oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"add", "--", "test.txt", "test2.txt"}, "", nil)
|
|
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
|
|
|
|
assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}, nil))
|
|
runner.CheckForMissingCalls()
|
|
}
|
|
|
|
func TestWorkingTreeUnstageFile(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
reset bool
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Remove an untracked file from staging",
|
|
reset: false,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"rm", "--cached", "--force", "--", "test.txt"}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
testName: "Remove a tracked file from staging",
|
|
reset: true,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "HEAD", "--", "test.txt"}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
s.test(instance.UnStageFile([]string{"test.txt"}, s.reset))
|
|
})
|
|
}
|
|
}
|
|
|
|
// these tests don't cover everything, in part because we already have an integration
|
|
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
|
|
// when the 'what' is what matters
|
|
func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
file *models.File
|
|
removedFileErr error
|
|
runner *oscommands.FakeCmdObjRunner
|
|
expectedError string
|
|
expectedRemovedFiles []string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "An error occurred when resetting",
|
|
file: &models.File{
|
|
Path: "test",
|
|
HasStagedChanges: true,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--", "test"}, "", errors.New("error")),
|
|
expectedError: "error",
|
|
},
|
|
{
|
|
testName: "An error occurred when removing file",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: false,
|
|
Added: true,
|
|
},
|
|
removedFileErr: errors.New("an error occurred when removing file"),
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedError: "an error occurred when removing file",
|
|
expectedRemovedFiles: []string{"test"},
|
|
},
|
|
{
|
|
testName: "An error occurred with checkout",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: true,
|
|
HasStagedChanges: false,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "test"}, "", errors.New("error")),
|
|
expectedError: "error",
|
|
},
|
|
{
|
|
testName: "Checkout only",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: true,
|
|
HasStagedChanges: false,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
|
|
},
|
|
{
|
|
testName: "Reset and checkout staged changes",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: true,
|
|
HasStagedChanges: true,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
|
|
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
|
|
},
|
|
{
|
|
testName: "Reset and checkout merge conflicts",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: true,
|
|
HasMergeConflicts: true,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
|
|
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
|
|
},
|
|
{
|
|
testName: "Reset and remove",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: false,
|
|
Added: true,
|
|
HasStagedChanges: true,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil),
|
|
expectedRemovedFiles: []string{"test"},
|
|
},
|
|
{
|
|
testName: "Remove only",
|
|
file: &models.File{
|
|
Path: "test",
|
|
Tracked: false,
|
|
Added: true,
|
|
HasStagedChanges: false,
|
|
},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"test"},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
var removedFiles []string
|
|
removeFile := func(path string) error {
|
|
removedFiles = append(removedFiles, path)
|
|
return s.removedFileErr
|
|
}
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, removeFile: removeFile})
|
|
err := instance.DiscardAllFileChanges(s.file)
|
|
|
|
if s.expectedError == "" {
|
|
assert.Nil(t, err)
|
|
} else {
|
|
assert.Equal(t, s.expectedError, err.Error())
|
|
}
|
|
assert.Equal(t, s.expectedRemovedFiles, removedFiles)
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeDiff(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
file *models.File
|
|
plain bool
|
|
cached bool
|
|
ignoreWhitespace bool
|
|
contextSize uint64
|
|
similarityThreshold int
|
|
runner *oscommands.FakeCmdObjRunner
|
|
}
|
|
|
|
const expectedResult = "pretend this is an actual git diff"
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Default case",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: true,
|
|
},
|
|
plain: false,
|
|
cached: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "cached",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: true,
|
|
},
|
|
plain: false,
|
|
cached: true,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--cached", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "plain",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: true,
|
|
},
|
|
plain: true,
|
|
cached: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=never", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "File not tracked and file has no staged changes",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: false,
|
|
},
|
|
plain: false,
|
|
cached: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "Default case (ignore whitespace)",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: true,
|
|
},
|
|
plain: false,
|
|
cached: false,
|
|
ignoreWhitespace: true,
|
|
contextSize: 3,
|
|
similarityThreshold: 50,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=3", "--ignore-all-space", "--find-renames=50%", "--submodule", "--color=always", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "Show diff with custom context size",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: true,
|
|
},
|
|
plain: false,
|
|
cached: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 17,
|
|
similarityThreshold: 50,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=17", "--find-renames=50%", "--submodule", "--color=always", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "Show diff with custom similarity threshold",
|
|
file: &models.File{
|
|
Path: "test.txt",
|
|
HasStagedChanges: false,
|
|
Tracked: true,
|
|
},
|
|
plain: false,
|
|
cached: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
similarityThreshold: 33,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "diff", "--no-ext-diff", "--unified=3", "--find-renames=33%", "--submodule", "--color=always", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
userConfig := config.GetDefaultConfig()
|
|
userConfig.Git.IgnoreWhitespaceInDiffView = s.ignoreWhitespace
|
|
userConfig.Git.DiffContextSize = s.contextSize
|
|
userConfig.Git.RenameSimilarityThreshold = s.similarityThreshold
|
|
repoPaths := RepoPaths{
|
|
worktreePath: "/path/to/worktree",
|
|
}
|
|
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
|
|
result := instance.WorktreeFileDiff(s.file, s.plain, s.cached)
|
|
assert.Equal(t, expectedResult, result)
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeShowFileDiff(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
from string
|
|
to string
|
|
reverse bool
|
|
fileName string
|
|
previousPath string
|
|
plain bool
|
|
ignoreWhitespace bool
|
|
contextSize uint64
|
|
runner *oscommands.FakeCmdObjRunner
|
|
}
|
|
|
|
const expectedResult = "pretend this is an actual git diff"
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Default case",
|
|
from: "1234567890",
|
|
to: "0987654321",
|
|
reverse: false,
|
|
fileName: "test.txt",
|
|
plain: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "Show diff with custom context size",
|
|
from: "1234567890",
|
|
to: "0987654321",
|
|
reverse: false,
|
|
fileName: "test.txt",
|
|
plain: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 123,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--unified=123", "--find-renames=50%", "--submodule", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "Default case (ignore whitespace)",
|
|
from: "1234567890",
|
|
to: "0987654321",
|
|
reverse: false,
|
|
fileName: "test.txt",
|
|
plain: false,
|
|
ignoreWhitespace: true,
|
|
contextSize: 3,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--unified=3", "--ignore-all-space", "--find-renames=50%", "--submodule", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
|
},
|
|
{
|
|
testName: "Renamed file passes both paths so the rename is detected",
|
|
from: "1234567890",
|
|
to: "0987654321",
|
|
reverse: false,
|
|
fileName: "new.txt",
|
|
previousPath: "old.txt",
|
|
plain: false,
|
|
ignoreWhitespace: false,
|
|
contextSize: 3,
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "1234567890", "0987654321", "--", "new.txt", "old.txt"}, expectedResult, nil),
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
userConfig := config.GetDefaultConfig()
|
|
userConfig.Git.IgnoreWhitespaceInDiffView = s.ignoreWhitespace
|
|
userConfig.Git.DiffContextSize = s.contextSize
|
|
repoPaths := RepoPaths{
|
|
worktreePath: "/path/to/worktree",
|
|
}
|
|
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
|
|
|
|
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, s.fileName, s.previousPath, s.plain)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedResult, result)
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeCheckoutFile(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
commitHash string
|
|
fileName string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "typical case",
|
|
commitHash: "11af912",
|
|
fileName: "test999.txt",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
{
|
|
testName: "returns error if there is one",
|
|
commitHash: "11af912",
|
|
fileName: "test999.txt",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", errors.New("error")),
|
|
test: func(err error) {
|
|
assert.Error(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
|
|
s.test(instance.CheckoutFile(s.commitHash, s.fileName))
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
file *models.File
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "valid case",
|
|
file: &models.File{Path: "test.txt"},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "test.txt"}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
s.test(instance.DiscardUnstagedFileChanges(s.file))
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
// testNode implements IFileNode for unit tests.
|
|
type testNode struct {
|
|
children []*testNode
|
|
path string
|
|
file *models.File // non-nil only for file nodes
|
|
}
|
|
|
|
func (n *testNode) ForEachFile(cb func(*models.File) error) error {
|
|
if n.file != nil {
|
|
return cb(n.file)
|
|
}
|
|
for _, child := range n.children {
|
|
if err := child.ForEachFile(cb); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *testNode) GetFilePathsMatching(test func(*models.File) bool) []string {
|
|
if n.file != nil {
|
|
if test(n.file) {
|
|
return []string{n.path}
|
|
}
|
|
return nil
|
|
}
|
|
return lo.FlatMap(n.children, func(child *testNode, _ int) []string {
|
|
return child.GetFilePathsMatching(test)
|
|
})
|
|
}
|
|
|
|
func (n *testNode) GetPath() string { return n.path }
|
|
func (n *testNode) GetFile() *models.File { return n.file }
|
|
|
|
func TestWorkingTreeDiscardAllDirChanges(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
nodes []IFileNode
|
|
runner *oscommands.FakeCmdObjRunner
|
|
dirsWithRemainingFiles []string // dirs where isDirEmpty returns false
|
|
expectedRemovedFiles []string
|
|
expectedRemovedDirs []string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "multiple regular tracked files batched into a single checkout call",
|
|
nodes: []IFileNode{&testNode{
|
|
children: []*testNode{
|
|
{path: "a.txt", file: &models.File{Path: "a.txt", Tracked: true}},
|
|
{path: "b.txt", file: &models.File{Path: "b.txt", Tracked: true}},
|
|
{path: "c.txt", file: &models.File{Path: "c.txt", Tracked: true}},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "a.txt", "b.txt", "c.txt"}, "", nil),
|
|
},
|
|
{
|
|
testName: "staged files batched into a single reset then a single checkout",
|
|
nodes: []IFileNode{&testNode{
|
|
children: []*testNode{
|
|
{path: "a.txt", file: &models.File{Path: "a.txt", Tracked: true, HasStagedChanges: true}},
|
|
{path: "b.txt", file: &models.File{Path: "b.txt", Tracked: true, HasStagedChanges: true}},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--", "a.txt", "b.txt"}, "", nil).
|
|
ExpectGitArgs([]string{"checkout", "--", "a.txt", "b.txt"}, "", nil),
|
|
},
|
|
{
|
|
testName: "added files with no staged changes are removed from disk without any git call",
|
|
nodes: []IFileNode{&testNode{
|
|
children: []*testNode{
|
|
{path: "new1.txt", file: &models.File{Path: "new1.txt", Added: true}},
|
|
{path: "new2.txt", file: &models.File{Path: "new2.txt", Added: true}},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"new1.txt", "new2.txt"},
|
|
},
|
|
{
|
|
testName: "files from multiple nodes are batched into a single git call",
|
|
nodes: []IFileNode{
|
|
&testNode{
|
|
path: "dir1",
|
|
children: []*testNode{
|
|
{path: "dir1/a.txt", file: &models.File{Path: "dir1/a.txt", Tracked: true}},
|
|
{path: "dir1/b.txt", file: &models.File{Path: "dir1/b.txt", Added: true}},
|
|
},
|
|
},
|
|
&testNode{
|
|
path: "dir2",
|
|
children: []*testNode{
|
|
{path: "dir2/c.txt", file: &models.File{Path: "dir2/c.txt", Tracked: true}},
|
|
{path: "dir2/d.txt", file: &models.File{Path: "dir2/d.txt", Added: true}},
|
|
},
|
|
},
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "dir1/a.txt", "dir2/c.txt"}, "", nil),
|
|
dirsWithRemainingFiles: []string{"dir1", "dir2"}, // tracked files a.txt / c.txt remain
|
|
expectedRemovedFiles: []string{"dir1/b.txt", "dir2/d.txt"},
|
|
},
|
|
{
|
|
testName: "empty parent directory is removed after all its added files are deleted",
|
|
nodes: []IFileNode{&testNode{
|
|
path: "dir",
|
|
children: []*testNode{
|
|
{
|
|
path: "dir/newdir",
|
|
children: []*testNode{
|
|
{path: "dir/newdir/a.txt", file: &models.File{Path: "dir/newdir/a.txt", Added: true}},
|
|
{path: "dir/newdir/b.txt", file: &models.File{Path: "dir/newdir/b.txt", Added: true}},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
dirsWithRemainingFiles: []string{"dir"}, // assume there are other tracked files in dir
|
|
expectedRemovedFiles: []string{"dir/newdir/a.txt", "dir/newdir/b.txt"},
|
|
expectedRemovedDirs: []string{"dir/newdir"},
|
|
},
|
|
{
|
|
testName: "nested empty directories are removed bottom-up",
|
|
nodes: []IFileNode{&testNode{
|
|
path: "newdir",
|
|
children: []*testNode{
|
|
{
|
|
path: "newdir/sub",
|
|
children: []*testNode{
|
|
{path: "newdir/sub/file.txt", file: &models.File{Path: "newdir/sub/file.txt", Added: true}},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"newdir/sub/file.txt"},
|
|
expectedRemovedDirs: []string{"newdir/sub", "newdir"},
|
|
},
|
|
{
|
|
testName: "empty directory is NOT removed when individual file nodes are selected",
|
|
nodes: []IFileNode{
|
|
&testNode{path: "newdir/a.txt", file: &models.File{Path: "newdir/a.txt", Added: true}},
|
|
&testNode{path: "newdir/b.txt", file: &models.File{Path: "newdir/b.txt", Added: true}},
|
|
},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"newdir/a.txt", "newdir/b.txt"},
|
|
// newdir becomes empty but was not selected as a directory node, so it is not removed
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
var removedFiles []string
|
|
removeFile := func(path string) error {
|
|
removedFiles = append(removedFiles, path)
|
|
return nil
|
|
}
|
|
isDirEmpty := func(path string) (bool, error) { return !lo.Contains(s.dirsWithRemainingFiles, path), nil }
|
|
var removedDirs []string
|
|
removeDir := func(path string) error {
|
|
removedDirs = append(removedDirs, path)
|
|
return nil
|
|
}
|
|
instance := buildWorkingTreeCommands(commonDeps{
|
|
runner: s.runner,
|
|
removeFile: removeFile,
|
|
isDirEmpty: isDirEmpty,
|
|
removeDir: removeDir,
|
|
})
|
|
err := instance.DiscardAllDirChanges(s.nodes)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, s.expectedRemovedFiles, removedFiles)
|
|
assert.Equal(t, s.expectedRemovedDirs, removedDirs)
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeDiscardUnstagedDirChanges(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
nodes []IFileNode
|
|
runner *oscommands.FakeCmdObjRunner
|
|
dirsWithRemainingFiles []string // dirs where isDirEmpty returns false
|
|
expectedRemovedFiles []string
|
|
expectedRemovedDirs []string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "directory node: removes untracked files and checks out tracked files by path, not by directory",
|
|
nodes: []IFileNode{&testNode{
|
|
path: "dir",
|
|
children: []*testNode{
|
|
{path: "dir/tracked1.txt", file: &models.File{Path: "dir/tracked1.txt", Tracked: true}},
|
|
{path: "dir/tracked2.txt", file: &models.File{Path: "dir/tracked2.txt", Tracked: true}},
|
|
{path: "dir/new.txt", file: &models.File{Path: "dir/new.txt", Tracked: false}},
|
|
},
|
|
}},
|
|
// Must checkout the individual files, not "dir" — otherwise a filter would be ignored.
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "dir/tracked1.txt", "dir/tracked2.txt"}, "", nil),
|
|
dirsWithRemainingFiles: []string{"dir"}, // tracked files remain in dir
|
|
expectedRemovedFiles: []string{"dir/new.txt"},
|
|
},
|
|
{
|
|
testName: "directory node: staged-but-not-committed file (Tracked=false, HasStagedChanges=true) is left alone; purely untracked file is removed",
|
|
nodes: []IFileNode{&testNode{
|
|
path: "dir",
|
|
children: []*testNode{
|
|
// Staged new files: not removed from disk, but checked out in
|
|
// case they also have unstaged changes on top (AM status).
|
|
{path: "dir/staged-new1.txt", file: &models.File{Path: "dir/staged-new1.txt", Tracked: false, Added: true, HasStagedChanges: true}},
|
|
{path: "dir/staged-new2.txt", file: &models.File{Path: "dir/staged-new2.txt", Tracked: false, Added: true, HasStagedChanges: true}},
|
|
// Purely untracked file: removed from disk, not checked out.
|
|
{path: "dir/untracked.txt", file: &models.File{Path: "dir/untracked.txt", Tracked: false, Added: true, HasStagedChanges: false}},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "dir/staged-new1.txt", "dir/staged-new2.txt"}, "", nil),
|
|
dirsWithRemainingFiles: []string{"dir"}, // staged files remain in dir
|
|
expectedRemovedFiles: []string{"dir/untracked.txt"},
|
|
},
|
|
{
|
|
testName: "file node: added and unstaged file is removed from disk",
|
|
nodes: []IFileNode{&testNode{
|
|
path: "new.txt",
|
|
file: &models.File{Path: "new.txt", Added: true, HasStagedChanges: false},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"new.txt"},
|
|
},
|
|
{
|
|
testName: "files from multiple nodes are batched into a single checkout call",
|
|
nodes: []IFileNode{
|
|
&testNode{
|
|
path: "dir1",
|
|
children: []*testNode{
|
|
{path: "dir1/tracked.txt", file: &models.File{Path: "dir1/tracked.txt", Tracked: true}},
|
|
{path: "dir1/untracked.txt", file: &models.File{Path: "dir1/untracked.txt", Tracked: false}},
|
|
},
|
|
},
|
|
&testNode{
|
|
path: "dir2",
|
|
children: []*testNode{
|
|
{path: "dir2/tracked.txt", file: &models.File{Path: "dir2/tracked.txt", Tracked: true}},
|
|
{path: "dir2/untracked.txt", file: &models.File{Path: "dir2/untracked.txt", Tracked: false}},
|
|
},
|
|
},
|
|
},
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "dir1/tracked.txt", "dir2/tracked.txt"}, "", nil),
|
|
dirsWithRemainingFiles: []string{"dir1", "dir2"}, // tracked files remain
|
|
expectedRemovedFiles: []string{"dir1/untracked.txt", "dir2/untracked.txt"},
|
|
},
|
|
{
|
|
testName: "empty untracked directory is removed after its files are deleted",
|
|
nodes: []IFileNode{&testNode{
|
|
path: "newdir",
|
|
children: []*testNode{
|
|
{path: "newdir/a.txt", file: &models.File{Path: "newdir/a.txt", Tracked: false}},
|
|
{path: "newdir/b.txt", file: &models.File{Path: "newdir/b.txt", Tracked: false}},
|
|
},
|
|
}},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"newdir/a.txt", "newdir/b.txt"},
|
|
expectedRemovedDirs: []string{"newdir"},
|
|
},
|
|
{
|
|
testName: "empty directory is NOT removed when individual file nodes are selected",
|
|
nodes: []IFileNode{
|
|
&testNode{path: "newdir/a.txt", file: &models.File{Path: "newdir/a.txt", Tracked: false}},
|
|
&testNode{path: "newdir/b.txt", file: &models.File{Path: "newdir/b.txt", Tracked: false}},
|
|
},
|
|
runner: oscommands.NewFakeRunner(t),
|
|
expectedRemovedFiles: []string{"newdir/a.txt", "newdir/b.txt"},
|
|
// newdir becomes empty but was not selected as a directory node, so it is not removed
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
var removedFiles []string
|
|
removeFile := func(path string) error {
|
|
removedFiles = append(removedFiles, path)
|
|
return nil
|
|
}
|
|
isDirEmpty := func(path string) (bool, error) { return !lo.Contains(s.dirsWithRemainingFiles, path), nil }
|
|
var removedDirs []string
|
|
removeDir := func(path string) error {
|
|
removedDirs = append(removedDirs, path)
|
|
return nil
|
|
}
|
|
instance := buildWorkingTreeCommands(commonDeps{
|
|
runner: s.runner,
|
|
removeFile: removeFile,
|
|
isDirEmpty: isDirEmpty,
|
|
removeDir: removeDir,
|
|
})
|
|
assert.NoError(t, instance.DiscardUnstagedDirChanges(s.nodes))
|
|
s.runner.CheckForMissingCalls()
|
|
assert.Equal(t, s.expectedRemovedFiles, removedFiles)
|
|
assert.Equal(t, s.expectedRemovedDirs, removedDirs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "valid case",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"checkout", "--", "."}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
s.test(instance.DiscardAnyUnstagedFileChanges())
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeRemoveUntrackedFiles(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "valid case",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"clean", "-fd"}, "", nil),
|
|
test: func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
s.test(instance.RemoveUntrackedFiles())
|
|
s.runner.CheckForMissingCalls()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeResetHard(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
ref string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
test func(error)
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
"valid case",
|
|
"HEAD",
|
|
oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"reset", "--hard", "HEAD"}, "", nil),
|
|
func(err error) {
|
|
assert.NoError(t, err)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
s.test(instance.ResetHard(s.ref))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkingTreeCommands_AllRepoFiles(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
runner *oscommands.FakeCmdObjRunner
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "no files",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"ls-files", "-z"}, "", nil),
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "two files",
|
|
runner: oscommands.NewFakeRunner(t).
|
|
ExpectGitArgs([]string{"ls-files", "-z"}, "dir/file1.txt\x00dir2/file2.go\x00", nil),
|
|
expected: []string{"dir/file1.txt", "dir2/file2.go"},
|
|
},
|
|
}
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
|
|
result, err := instance.AllRepoFiles()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, s.expected, result)
|
|
})
|
|
}
|
|
}
|