1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-10-20 19:12:29 +03:00
Files
lazygit/pkg/gui/controllers/workspace_reset_controller.go
Stefan Haller 40cc008a5a Avoid auto-stashing when only submodules are out of date
Stashing doesn't affect submodules, so if you have a working copy that has
out-of-date submodules but no other changes, and then you revert or paste a
commit (or invoke one of the many other lazygit commands that auto-stash, e.g.
undo), lazygit would previously try to stash changes (which did nothing, but
also didn't return an error), perform the operation, and then pop the stash
again. If no stashes existed before, then this would only cause a confusing
error popup ("error: refs/stash@{0} is not a valid reference"), but if there
were stashes, this would try to pop the newest one of these, which is very
undesirable and confusing.
2025-10-16 12:00:31 +02:00

283 lines
7.4 KiB
Go

package controllers
import (
"bytes"
"errors"
"fmt"
"math"
"math/rand"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// this is in its own file given that the workspace controller file is already quite long
func (self *FilesController) createResetMenu() error {
red := style.FgRed
nukeStr := "git reset --hard HEAD && git clean -fd"
if len(self.c.Model().Submodules) > 0 {
nukeStr = fmt.Sprintf("%s (%s)", nukeStr, self.c.Tr.AndResetSubmodules)
}
menuItems := []*types.MenuItem{
{
LabelColumns: []string{
self.c.Tr.DiscardAllChangesToAllFiles,
red.Sprint(nukeStr),
},
OnPress: func() error {
self.c.Confirm(
types.ConfirmOpts{
Title: self.c.Tr.Actions.NukeWorkingTree,
Prompt: self.c.Tr.NukeTreeConfirmation,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.NukeWorkingTree)
if err := self.c.Git().WorkingTree.ResetAndClean(); err != nil {
return err
}
if self.c.UserConfig().Gui.AnimateExplosion {
self.animateExplosion()
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
})
return nil
},
Key: 'x',
Tooltip: self.c.Tr.NukeDescription,
},
{
LabelColumns: []string{
self.c.Tr.DiscardAnyUnstagedChanges,
red.Sprint("git checkout -- ."),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedFileChanges)
if err := self.c.Git().WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'u',
},
{
LabelColumns: []string{
self.c.Tr.DiscardUntrackedFiles,
red.Sprint("git clean -fd"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RemoveUntrackedFiles)
if err := self.c.Git().WorkingTree.RemoveUntrackedFiles(); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'c',
},
{
LabelColumns: []string{
self.c.Tr.DiscardStagedChanges,
red.Sprint("stash staged and drop stash"),
},
Tooltip: self.c.Tr.DiscardStagedChangesDescription,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles)
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() {
return errors.New(self.c.Tr.NoTrackedStagedFilesStash)
}
if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil {
return err
}
if err := self.c.Git().Stash.DropNewest(); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'S',
},
{
LabelColumns: []string{
self.c.Tr.SoftReset,
red.Sprint("git reset --soft HEAD"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.SoftReset)
if err := self.c.Git().WorkingTree.ResetSoft("HEAD"); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 's',
},
{
LabelColumns: []string{
"mixed reset",
red.Sprint("git reset --mixed HEAD"),
},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.MixedReset)
if err := self.c.Git().WorkingTree.ResetMixed("HEAD"); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
Key: 'm',
},
{
LabelColumns: []string{
self.c.Tr.HardReset,
red.Sprint("git reset --hard HEAD"),
},
OnPress: func() error {
return self.c.ConfirmIf(helpers.IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules),
types.ConfirmOpts{
Title: self.c.Tr.Actions.HardReset,
Prompt: self.c.Tr.ResetHardConfirmation,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.HardReset)
if err := self.c.Git().WorkingTree.ResetHard("HEAD"); err != nil {
return err
}
self.c.Refresh(
types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}},
)
return nil
},
})
},
Key: 'h',
},
}
return self.c.Menu(types.CreateMenuOptions{Title: "", Items: menuItems})
}
func (self *FilesController) animateExplosion() {
self.Explode(self.c.Views().Files, func() {
self.c.PostRefreshUpdate(self.c.Contexts().Files)
})
}
// Animates an explosion within the view by drawing a bunch of flamey characters
func (self *FilesController) Explode(v *gocui.View, onDone func()) {
width := v.InnerWidth()
height := v.InnerHeight()
styles := []style.TextStyle{
style.FgLightWhite.SetBold(),
style.FgYellow.SetBold(),
style.FgRed.SetBold(),
style.FgBlue.SetBold(),
style.FgBlack.SetBold(),
}
self.c.OnWorker(func(_ gocui.Task) error {
max := 25
for i := range max {
image := getExplodeImage(width, height, i, max)
style := styles[(i*len(styles)/max)%len(styles)]
coloredImage := style.Sprint(image)
self.c.OnUIThread(func() error {
v.SetOrigin(0, 0)
v.SetContent(coloredImage)
return nil
})
time.Sleep(time.Millisecond * 20)
}
self.c.OnUIThread(func() error {
v.Clear()
onDone()
return nil
})
return nil
})
}
// Render an explosion in the given bounds.
func getExplodeImage(width int, height int, frame int, max int) string {
// Predefine the explosion symbols
explosionChars := []rune{'*', '.', '@', '#', '&', '+', '%'}
// Initialize a buffer to build our string
var buf bytes.Buffer
// Initialize RNG seed
random := rand.New(rand.NewSource(time.Now().UnixNano()))
// calculate the center of explosion
centerX, centerY := width/2, height/2
// calculate the max radius (hypotenuse of the view)
maxRadius := math.Hypot(float64(centerX), float64(centerY))
// calculate frame as a proportion of max, apply square root to create the non-linear effect
progress := math.Sqrt(float64(frame) / float64(max))
// calculate radius of explosion according to frame and max
radius := progress * maxRadius * 2
// introduce a new radius for the inner boundary of the explosion (the shockwave effect)
var innerRadius float64
if progress > 0.5 {
innerRadius = (progress - 0.5) * 2 * maxRadius
}
for y := range height {
for x := range width {
// calculate distance from center, scale x by 2 to compensate for character aspect ratio
distance := math.Hypot(float64(x-centerX), float64(y-centerY)*2)
// if distance is less than radius and greater than innerRadius, draw explosion char
if distance <= radius && distance >= innerRadius {
// Make placement random and less likely as explosion progresses
if random.Float64() > progress {
// Pick a random explosion char
char := explosionChars[random.Intn(len(explosionChars))]
buf.WriteRune(char)
} else {
buf.WriteRune(' ')
}
} else {
// If not explosion, then it's empty space
buf.WriteRune(' ')
}
}
// End of line
if y < height-1 {
buf.WriteRune('\n')
}
}
return buf.String()
}