Skip to content

Commit 7ccf17a

Browse files
committed
feat: batch file remove actions
1 parent d1d2bb2 commit 7ccf17a

File tree

3 files changed

+137
-69
lines changed

3 files changed

+137
-69
lines changed

pkg/commands/git_commands/working_tree.go

Lines changed: 113 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package git_commands
22

33
import (
44
"fmt"
5-
"os"
65
"path/filepath"
76
"regexp"
87
"strings"
@@ -116,64 +115,124 @@ func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File)
116115
return beforeFile, afterFile, nil
117116
}
118117

119-
// DiscardAllFileChanges directly
120-
func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error {
121-
if file.IsRename() {
122-
beforeFile, afterFile, err := self.BeforeAndAfterFileForRename(file)
123-
if err != nil {
124-
return err
118+
// DiscardAllFilesChanges discards changes for multiple files in batch
119+
func (self *WorkingTreeCommands) DiscardAllFilesChanges(files []*models.File) error {
120+
// Group files by their discard strategy
121+
var (
122+
aaStatusFiles []*models.File
123+
duStatusFiles []*models.File
124+
filesToReset []*models.File
125+
addedFilesToRemove []*models.File
126+
filesToCheckout []*models.File
127+
)
128+
129+
// Helper function to categorize a file into the appropriate group
130+
categorizeFile := func(file *models.File) {
131+
if file.ShortStatus == "AA" {
132+
aaStatusFiles = append(aaStatusFiles, file)
133+
return
125134
}
126135

127-
if err := self.DiscardAllFileChanges(beforeFile); err != nil {
128-
return err
136+
if file.ShortStatus == "DU" {
137+
duStatusFiles = append(duStatusFiles, file)
138+
return
129139
}
130140

131-
if err := self.DiscardAllFileChanges(afterFile); err != nil {
132-
return err
141+
// Track which files need to be reset first
142+
needsReset := file.HasStagedChanges || file.HasMergeConflicts
143+
if needsReset {
144+
filesToReset = append(filesToReset, file)
145+
}
146+
147+
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
148+
} else if file.Added {
149+
addedFilesToRemove = append(addedFilesToRemove, file)
150+
} else {
151+
filesToCheckout = append(filesToCheckout, file)
152+
}
153+
}
154+
155+
for _, file := range files {
156+
if file.IsRename() {
157+
// Get the before and after files for the rename and add them to the appropriate groups
158+
beforeFile, afterFile, err := self.BeforeAndAfterFileForRename(file)
159+
if err != nil {
160+
return err
161+
}
162+
categorizeFile(beforeFile)
163+
categorizeFile(afterFile)
164+
continue
133165
}
134166

135-
return nil
167+
categorizeFile(file)
136168
}
137169

138-
if file.ShortStatus == "AA" {
170+
// Batch reset files that need resetting
171+
if len(filesToReset) > 0 {
172+
paths := make([]string, len(filesToReset))
173+
for i, file := range filesToReset {
174+
paths[i] = file.Path
175+
}
139176
if err := self.cmd.New(
140-
NewGitCmd("checkout").Arg("--ours", "--", file.Path).ToArgv(),
177+
NewGitCmd("reset").Arg("--").Arg(paths...).ToArgv(),
141178
).Run(); err != nil {
142179
return err
143180
}
181+
}
144182

183+
// Batch remove DU status files
184+
if len(duStatusFiles) > 0 {
185+
paths := make([]string, len(duStatusFiles))
186+
for i, file := range duStatusFiles {
187+
paths[i] = file.Path
188+
}
145189
if err := self.cmd.New(
146-
NewGitCmd("add").Arg("--", file.Path).ToArgv(),
190+
NewGitCmd("rm").Arg("--").Arg(paths...).ToArgv(),
147191
).Run(); err != nil {
148192
return err
149193
}
150-
return nil
151-
}
152-
153-
if file.ShortStatus == "DU" {
154-
return self.cmd.New(
155-
NewGitCmd("rm").Arg("--", file.Path).ToArgv(),
156-
).Run()
157194
}
158195

159-
// if the file isn't tracked, we assume you want to delete it
160-
if file.HasStagedChanges || file.HasMergeConflicts {
196+
// Batch checkout --ours for AA status files
197+
if len(aaStatusFiles) > 0 {
198+
paths := make([]string, len(aaStatusFiles))
199+
for i, file := range aaStatusFiles {
200+
paths[i] = file.Path
201+
}
202+
if err := self.cmd.New(
203+
NewGitCmd("checkout").Arg("--ours", "--").Arg(paths...).ToArgv(),
204+
).Run(); err != nil {
205+
return err
206+
}
207+
// Stage them after checkout
161208
if err := self.cmd.New(
162-
NewGitCmd("reset").Arg("--", file.Path).ToArgv(),
209+
NewGitCmd("add").Arg("--").Arg(paths...).ToArgv(),
163210
).Run(); err != nil {
164211
return err
165212
}
166213
}
167214

168-
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
169-
return nil
215+
// Remove added files from filesystem
216+
for _, file := range addedFilesToRemove {
217+
if err := self.os.RemoveFile(file.Path); err != nil {
218+
return err
219+
}
170220
}
171221

172-
if file.Added {
173-
return self.os.RemoveFile(file.Path)
222+
// Batch checkout other files
223+
if len(filesToCheckout) > 0 {
224+
paths := make([]string, len(filesToCheckout))
225+
for i, file := range filesToCheckout {
226+
paths[i] = file.Path
227+
}
228+
if err := self.cmd.New(
229+
NewGitCmd("checkout").Arg("--").Arg(paths...).ToArgv(),
230+
).Run(); err != nil {
231+
return err
232+
}
174233
}
175234

176-
return self.DiscardUnstagedFileChanges(file)
235+
return nil
177236
}
178237

179238
type IFileNode interface {
@@ -184,55 +243,45 @@ type IFileNode interface {
184243
GetFile() *models.File
185244
}
186245

187-
func (self *WorkingTreeCommands) DiscardAllDirChanges(node IFileNode) error {
188-
// this could be more efficient but we would need to handle all the edge cases
189-
return node.ForEachFile(self.DiscardAllFileChanges)
190-
}
191-
192-
func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error {
193-
file := node.GetFile()
194-
if file == nil {
195-
if err := self.RemoveUntrackedDirFiles(node); err != nil {
196-
return err
197-
}
246+
// DiscardUnstagedFilesChanges discards unstaged changes for multiple files in batch
247+
func (self *WorkingTreeCommands) DiscardUnstagedFilesChanges(files []*models.File) error {
248+
var (
249+
addedFilesToRemove []*models.File
250+
trackedFilesToCheckout []*models.File
251+
)
198252

199-
cmdArgs := NewGitCmd("checkout").Arg("--", node.GetPath()).ToArgv()
200-
if err := self.cmd.New(cmdArgs).Run(); err != nil {
201-
return err
202-
}
203-
} else {
253+
for _, file := range files {
254+
// Only remove files that are added but not staged
204255
if file.Added && !file.HasStagedChanges {
205-
return self.os.RemoveFile(file.Path)
256+
addedFilesToRemove = append(addedFilesToRemove, file)
257+
} else {
258+
// Checkout tracked files to discard unstaged changes
259+
trackedFilesToCheckout = append(trackedFilesToCheckout, file)
206260
}
261+
}
207262

208-
if err := self.DiscardUnstagedFileChanges(file); err != nil {
263+
// Remove added files from filesystem
264+
for _, file := range addedFilesToRemove {
265+
if err := self.os.RemoveFile(file.Path); err != nil {
209266
return err
210267
}
211268
}
212269

213-
return nil
214-
}
215-
216-
func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
217-
untrackedFilePaths := node.GetFilePathsMatching(
218-
func(file *models.File) bool { return !file.GetIsTracked() },
219-
)
220-
221-
for _, path := range untrackedFilePaths {
222-
err := os.Remove(path)
223-
if err != nil {
270+
// Batch checkout tracked files
271+
if len(trackedFilesToCheckout) > 0 {
272+
paths := make([]string, len(trackedFilesToCheckout))
273+
for i, file := range trackedFilesToCheckout {
274+
paths[i] = file.Path
275+
}
276+
cmdArgs := NewGitCmd("checkout").Arg("--").Arg(paths...).ToArgv()
277+
if err := self.cmd.New(cmdArgs).Run(); err != nil {
224278
return err
225279
}
226280
}
227281

228282
return nil
229283
}
230284

231-
func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
232-
cmdArgs := NewGitCmd("checkout").Arg("--", file.Path).ToArgv()
233-
return self.cmd.New(cmdArgs).Run()
234-
}
235-
236285
// Escapes special characters in a filename for gitignore and exclude files
237286
func escapeFilename(filename string) string {
238287
re := regexp.MustCompile(`^[!#]|[\[\]*]`)

pkg/commands/git_commands/working_tree_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestWorkingTreeUnstageFile(t *testing.T) {
7070
// these tests don't cover everything, in part because we already have an integration
7171
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
7272
// when the 'what' is what matters
73-
func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
73+
func TestWorkingTreeDiscardAllFilesChanges(t *testing.T) {
7474
type scenario struct {
7575
testName string
7676
file *models.File
@@ -190,7 +190,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
190190
for _, s := range scenarios {
191191
t.Run(s.testName, func(t *testing.T) {
192192
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, removeFile: s.removeFile})
193-
err := instance.DiscardAllFileChanges(s.file)
193+
err := instance.DiscardAllFilesChanges([]*models.File{s.file})
194194

195195
if s.expectedError == "" {
196196
assert.Nil(t, err)
@@ -476,7 +476,7 @@ func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
476476
for _, s := range scenarios {
477477
t.Run(s.testName, func(t *testing.T) {
478478
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
479-
s.test(instance.DiscardUnstagedFileChanges(s.file))
479+
s.test(instance.DiscardUnstagedFilesChanges([]*models.File{s.file}))
480480
s.runner.CheckForMissingCalls()
481481
})
482482
}

pkg/gui/controllers/files_controller.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,12 +1382,21 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error {
13821382
defer self.context().CancelRangeSelect()
13831383
}
13841384

1385+
// Collect all files from the selected nodes
1386+
var files []*models.File
13851387
for _, node := range selectedNodes {
1386-
if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil {
1388+
if err := node.ForEachFile(func(file *models.File) error {
1389+
files = append(files, file)
1390+
return nil
1391+
}); err != nil {
13871392
return err
13881393
}
13891394
}
13901395

1396+
if err := self.c.Git().WorkingTree.DiscardAllFilesChanges(files); err != nil {
1397+
return err
1398+
}
1399+
13911400
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
13921401
return nil
13931402
},
@@ -1409,12 +1418,21 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error {
14091418
defer self.context().CancelRangeSelect()
14101419
}
14111420

1421+
// Collect all files from the selected nodes
1422+
var files []*models.File
14121423
for _, node := range selectedNodes {
1413-
if err := self.c.Git().WorkingTree.DiscardUnstagedDirChanges(node); err != nil {
1424+
if err := node.ForEachFile(func(file *models.File) error {
1425+
files = append(files, file)
1426+
return nil
1427+
}); err != nil {
14141428
return err
14151429
}
14161430
}
14171431

1432+
if err := self.c.Git().WorkingTree.DiscardUnstagedFilesChanges(files); err != nil {
1433+
return err
1434+
}
1435+
14181436
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
14191437
return nil
14201438
},
@@ -1475,3 +1493,4 @@ func (self *FilesController) isInTreeMode() *types.DisabledReason {
14751493

14761494
return nil
14771495
}
1496+

0 commit comments

Comments
 (0)