mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-10-20 19:12:29 +03:00
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.
283 lines
7.4 KiB
Go
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()
|
|
}
|