Skip to content

Commit

Permalink
Support staging/unstaging a range of files
Browse files Browse the repository at this point in the history
  • Loading branch information
jesseduffield committed Jan 10, 2024
1 parent f3d4299 commit 0d19776
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 65 deletions.
27 changes: 13 additions & 14 deletions pkg/commands/git_commands/working_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,20 @@ func (self *WorkingTreeCommands) UnstageAll() error {
// UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error {
for _, name := range fileNames {
var cmdArgs []string
if reset {
cmdArgs = NewGitCmd("reset").Arg("HEAD", "--", name).ToArgv()
} else {
cmdArgs = NewGitCmd("rm").Arg("--cached", "--force", "--", name).ToArgv()
}

err := self.cmd.New(cmdArgs).Run()
if err != nil {
return err
}
func (self *WorkingTreeCommands) UnStageFile(paths []string, tracked bool) error {
if tracked {
return self.UnstageTrackedFiles(paths)
} else {
return self.UnstageUntrackedFiles(paths)
}
return nil
}

func (self *WorkingTreeCommands) UnstageTrackedFiles(paths []string) error {
return self.cmd.New(NewGitCmd("reset").Arg("HEAD", "--").Arg(paths...).ToArgv()).Run()
}

func (self *WorkingTreeCommands) UnstageUntrackedFiles(paths []string) error {
return self.cmd.New(NewGitCmd("rm").Arg("--cached", "--force", "--").Arg(paths...).ToArgv()).Run()
}

func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
Expand Down
121 changes: 70 additions & 51 deletions pkg/gui/controllers/files_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)

type FilesController struct {
Expand Down Expand Up @@ -298,24 +300,28 @@ func (self *FilesController) optimisticUnstage(file *models.File) bool {
// the files panel. Then we'll immediately do a proper git status call
// so that if the optimistic rendering got something wrong, it's quickly
// corrected.
func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisticChangeFn func(*models.File) bool) error {
func (self *FilesController) optimisticChange(nodes []*filetree.FileNode, optimisticChangeFn func(*models.File) bool) error {
rerender := false
err := node.ForEachFile(func(f *models.File) error {
// can't act on the file itself: we need to update the original model file
for _, modelFile := range self.c.Model().Files {
if modelFile.Name == f.Name {
if optimisticChangeFn(modelFile) {
rerender = true

for _, node := range nodes {
err := node.ForEachFile(func(f *models.File) error {
// can't act on the file itself: we need to update the original model file
for _, modelFile := range self.c.Model().Files {
if modelFile.Name == f.Name {
if optimisticChangeFn(modelFile) {
rerender = true
}
break
}
break
}
}

return nil
})
if err != nil {
return err
return nil
})
if err != nil {
return err
}
}

if rerender {
if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil {
return err
Expand All @@ -331,56 +337,69 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error {
self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()

if node.IsFile() {
file := node.File

if file.HasUnstagedChanges {
self.c.LogAction(self.c.Tr.Actions.StageFile)

if err := self.optimisticChange(node, self.optimisticStage); err != nil {
return err
}
if self.context().IsSelectingRange() {
defer self.context().CancelRangeSelect()
}

if err := self.c.Git().WorkingTree.StageFile(file.Name); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
startIdx, endIdx := self.context().GetSelectionRange()

if err := self.optimisticChange(node, self.optimisticUnstage); err != nil {
return err
}
nodes := []*filetree.FileNode{}
for i := startIdx; i <= endIdx; i++ {
nodes = append(nodes, self.context().Get(i))
}

if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return self.c.Error(err)
}
}
} else {
for _, node := range nodes {
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if node.GetHasInlineMergeConflicts() {
return self.c.ErrorMsg(self.c.Tr.ErrStageDirWithInlineMergeConflicts)
}
}

if node.GetHasUnstagedChanges() {
self.c.LogAction(self.c.Tr.Actions.StageFile)
// If any node has unstaged changes, we'll stage all the selected nodes. Otherwise,
// we unstage all the selected nodes.
someNodesHaveUnstagedChanges := lo.SomeBy(nodes, func(node *filetree.FileNode) bool {
return node.GetHasUnstagedChanges()
})

if err := self.optimisticChange(node, self.optimisticStage); err != nil {
return err
}
toPaths := func(nodes []*filetree.FileNode) []string {
return lo.Map(nodes, func(node *filetree.FileNode, _ int) string {
return node.Path
})
}

if err := self.c.Git().WorkingTree.StageFile(node.Path); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)
if someNodesHaveUnstagedChanges {
self.c.LogAction(self.c.Tr.Actions.StageFile)

if err := self.optimisticChange(node, self.optimisticUnstage); err != nil {
return err
if err := self.optimisticChange(nodes, self.optimisticStage); err != nil {
return err
}

if err := self.c.Git().WorkingTree.StageFiles(toPaths(nodes)); err != nil {
return self.c.Error(err)
}
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageFile)

if err := self.optimisticChange(nodes, self.optimisticUnstage); err != nil {
return err
}

// need to partition the paths into tracked and untracked (where we assume directories are tracked). Then we'll run the commands separately.
trackedNodes, untrackedNodes := utils.Partition(nodes, func(node *filetree.FileNode) bool {
// We treat all directories as tracked. I'm not actually sure why we do this but
// it's been the existing behaviour for a while and nobody has complained
return !node.IsFile() || node.GetIsTracked()
})

if len(untrackedNodes) > 0 {
if err := self.c.Git().WorkingTree.UnstageUntrackedFiles(toPaths(untrackedNodes)); err != nil {
return self.c.Error(err)
}
}

// pretty sure it doesn't matter that we're always passing true here
if err := self.c.Git().WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
if len(trackedNodes) > 0 {
if err := self.c.Git().WorkingTree.UnstageTrackedFiles(toPaths(trackedNodes)); err != nil {
return self.c.Error(err)
}
}
Expand Down Expand Up @@ -491,7 +510,7 @@ func (self *FilesController) toggleStagedAllWithLock() error {
if root.GetHasUnstagedChanges() {
self.c.LogAction(self.c.Tr.Actions.StageAllFiles)

if err := self.optimisticChange(root, self.optimisticStage); err != nil {
if err := self.optimisticChange([]*filetree.FileNode{root}, self.optimisticStage); err != nil {
return err
}

Expand All @@ -501,7 +520,7 @@ func (self *FilesController) toggleStagedAllWithLock() error {
} else {
self.c.LogAction(self.c.Tr.Actions.UnstageAllFiles)

if err := self.optimisticChange(root, self.optimisticUnstage); err != nil {
if err := self.optimisticChange([]*filetree.FileNode{root}, self.optimisticUnstage); err != nil {
return err
}

Expand Down

0 comments on commit 0d19776

Please sign in to comment.