mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-25 18:55:28 -06:00
Merge branch 'master' into stash-untracked-changes
This commit is contained in:
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func Check() {
|
||||
dir := GetDir()
|
||||
dir := GetKeybindingsDir()
|
||||
tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet")
|
||||
err := os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -44,8 +44,8 @@ func CommandToRun() string {
|
||||
return "go run scripts/cheatsheet/main.go generate"
|
||||
}
|
||||
|
||||
func GetDir() string {
|
||||
return utils.GetLazygitRootDirectory() + "/docs/keybindings"
|
||||
func GetKeybindingsDir() string {
|
||||
return utils.GetLazyRootDirectory() + "/docs/keybindings"
|
||||
}
|
||||
|
||||
func generateAtDir(cheatsheetDir string) {
|
||||
@@ -75,7 +75,7 @@ func generateAtDir(cheatsheetDir string) {
|
||||
}
|
||||
|
||||
func Generate() {
|
||||
generateAtDir(GetDir())
|
||||
generateAtDir(GetKeybindingsDir())
|
||||
}
|
||||
|
||||
func writeString(file *os.File, str string) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
@@ -46,6 +47,19 @@ func (self *StashCommands) Save(message string) error {
|
||||
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Store(sha string, message string) error {
|
||||
trimmedMessage := strings.Trim(message, " \t")
|
||||
if len(trimmedMessage) > 0 {
|
||||
return self.cmd.New(fmt.Sprintf("git stash store %s -m %s", self.cmd.Quote(sha), self.cmd.Quote(trimmedMessage))).Run()
|
||||
}
|
||||
return self.cmd.New(fmt.Sprintf("git stash store %s", self.cmd.Quote(sha))).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Sha(index int) (string, error) {
|
||||
sha, _, err := self.cmd.New(fmt.Sprintf("git rev-parse refs/stash@{%d}", index)).DontLog().RunWithOutputs()
|
||||
return strings.Trim(sha, "\r\n"), err
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
|
||||
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index)
|
||||
|
||||
@@ -114,3 +128,20 @@ func (self *StashCommands) StashIncludeUntrackedChanges(message string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash save %s --include-untracked", self.cmd.Quote(message))).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Rename(index int, message string) error {
|
||||
sha, err := self.Sha(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.Drop(index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = self.Store(sha, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func TestStashDrop(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil)
|
||||
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "Dropped refs/stash@{1} (98e9cca532c37c766107093010c72e26f2c24c04)\n", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
assert.NoError(t, instance.Drop(1))
|
||||
@@ -44,6 +44,59 @@ func TestStashSave(t *testing.T) {
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestStashStore(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
sha string
|
||||
message string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Non-empty message",
|
||||
sha: "0123456789abcdef",
|
||||
message: "New stash name",
|
||||
expected: []string{"stash", "store", "0123456789abcdef", "-m", "New stash name"},
|
||||
},
|
||||
{
|
||||
testName: "Empty message",
|
||||
sha: "0123456789abcdef",
|
||||
message: "",
|
||||
expected: []string{"stash", "store", "0123456789abcdef"},
|
||||
},
|
||||
{
|
||||
testName: "Space message",
|
||||
sha: "0123456789abcdef",
|
||||
message: " ",
|
||||
expected: []string{"stash", "store", "0123456789abcdef"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expected, "", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
assert.NoError(t, instance.Store(s.sha, s.message))
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStashSha(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"rev-parse", "refs/stash@{5}"}, "14d94495194651adfd5f070590df566c11d28243\n", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
sha, err := instance.Sha(5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "14d94495194651adfd5f070590df566c11d28243", sha)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
@@ -79,3 +132,50 @@ func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStashRename(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
index int
|
||||
message string
|
||||
expectedShaCmd []string
|
||||
shaResult string
|
||||
expectedDropCmd []string
|
||||
expectedStoreCmd []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 3,
|
||||
message: "New message",
|
||||
expectedShaCmd: []string{"rev-parse", "refs/stash@{3}"},
|
||||
shaResult: "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd\n",
|
||||
expectedDropCmd: []string{"stash", "drop", "stash@{3}"},
|
||||
expectedStoreCmd: []string{"stash", "store", "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd", "-m", "New message"},
|
||||
},
|
||||
{
|
||||
testName: "Empty message",
|
||||
index: 4,
|
||||
message: "",
|
||||
expectedShaCmd: []string{"rev-parse", "refs/stash@{4}"},
|
||||
shaResult: "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd\n",
|
||||
expectedDropCmd: []string{"stash", "drop", "stash@{4}"},
|
||||
expectedStoreCmd: []string{"stash", "store", "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expectedShaCmd, s.shaResult, nil).
|
||||
ExpectGitArgs(s.expectedDropCmd, "", nil).
|
||||
ExpectGitArgs(s.expectedStoreCmd, "", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
err := instance.Rename(s.index, s.message)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ outer:
|
||||
}
|
||||
|
||||
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").DontLog().RunWithOutput()
|
||||
return slices.MapWithIndex(utils.SplitLines(rawString), func(line string, index int) *models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list -z --pretty='%gs'").DontLog().RunWithOutput()
|
||||
return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
|
||||
return self.stashEntryFromLine(line, index)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestGetStashEntries(t *testing.T) {
|
||||
"No stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git stash list --pretty='%gs'`, "", nil),
|
||||
Expect(`git stash list -z --pretty='%gs'`, "", nil),
|
||||
[]*models.StashEntry{},
|
||||
},
|
||||
{
|
||||
@@ -30,8 +30,8 @@ func TestGetStashEntries(t *testing.T) {
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git stash list --pretty='%gs'`,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
|
||||
`git stash list -z --pretty='%gs'`,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\x00WIP on master: bb86a3f update github template\x00",
|
||||
nil,
|
||||
),
|
||||
[]*models.StashEntry{
|
||||
|
||||
@@ -265,7 +265,8 @@ type KeybindingCommitsConfig struct {
|
||||
}
|
||||
|
||||
type KeybindingStashConfig struct {
|
||||
PopStash string `yaml:"popStash"`
|
||||
PopStash string `yaml:"popStash"`
|
||||
RenameStash string `yaml:"renameStash"`
|
||||
}
|
||||
|
||||
type KeybindingCommitFilesConfig struct {
|
||||
@@ -547,7 +548,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
ViewBisectOptions: "b",
|
||||
},
|
||||
Stash: KeybindingStashConfig{
|
||||
PopStash: "g",
|
||||
PopStash: "g",
|
||||
RenameStash: "r",
|
||||
},
|
||||
CommitFiles: KeybindingCommitFilesConfig{
|
||||
CheckoutCommitFile: "c",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
|
||||
"github.com/jesseduffield/lazycore/pkg/boxlayout"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
package boxlayout
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Dimensions struct {
|
||||
X0 int
|
||||
X1 int
|
||||
Y0 int
|
||||
Y1 int
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
ROW Direction = iota
|
||||
COLUMN
|
||||
)
|
||||
|
||||
// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space.
|
||||
// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
|
||||
// If a box represents a window, you can put the window name in the Window field.
|
||||
// When determining how to divvy-up the available height (for row children) or width (for column children), we first
|
||||
// give the boxes with a static `size` the space that they want. Then we apportion
|
||||
// the remaining space based on the weights of the dynamic boxes (you can't define
|
||||
// both size and weight at the same time: you gotta pick one). If there are two
|
||||
// boxes, one with weight 1 and the other with weight 2, the first one gets 33%
|
||||
// of the available space and the second one gets the remaining 66%
|
||||
|
||||
type Box struct {
|
||||
// Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother.
|
||||
Direction Direction
|
||||
|
||||
// function which takes the width and height assigned to the box and decides which orientation it will have
|
||||
ConditionalDirection func(width int, height int) Direction
|
||||
|
||||
Children []*Box
|
||||
|
||||
// function which takes the width and height assigned to the box and decides the layout of the children.
|
||||
ConditionalChildren func(width int, height int) []*Box
|
||||
|
||||
// Window refers to the name of the window this box represents, if there is one
|
||||
Window string
|
||||
|
||||
// static Size. If parent box's direction is ROW this refers to height, otherwise width
|
||||
Size int
|
||||
|
||||
// dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box
|
||||
// TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined
|
||||
Weight int
|
||||
}
|
||||
|
||||
func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
|
||||
children := root.getChildren(width, height)
|
||||
if len(children) == 0 {
|
||||
// leaf node
|
||||
if root.Window != "" {
|
||||
dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1}
|
||||
return map[string]Dimensions{root.Window: dimensionsForWindow}
|
||||
}
|
||||
return map[string]Dimensions{}
|
||||
}
|
||||
|
||||
direction := root.getDirection(width, height)
|
||||
|
||||
var availableSize int
|
||||
if direction == COLUMN {
|
||||
availableSize = width
|
||||
} else {
|
||||
availableSize = height
|
||||
}
|
||||
|
||||
sizes := calcSizes(children, availableSize)
|
||||
|
||||
result := map[string]Dimensions{}
|
||||
offset := 0
|
||||
for i, child := range children {
|
||||
boxSize := sizes[i]
|
||||
|
||||
var resultForChild map[string]Dimensions
|
||||
if direction == COLUMN {
|
||||
resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
|
||||
} else {
|
||||
resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
|
||||
}
|
||||
|
||||
result = mergeDimensionMaps(result, resultForChild)
|
||||
offset += boxSize
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func calcSizes(boxes []*Box, availableSpace int) []int {
|
||||
normalizedWeights := normalizeWeights(slices.Map(boxes, func(box *Box) int { return box.Weight }))
|
||||
|
||||
totalWeight := 0
|
||||
reservedSpace := 0
|
||||
for i, box := range boxes {
|
||||
if box.isStatic() {
|
||||
reservedSpace += box.Size
|
||||
} else {
|
||||
totalWeight += normalizedWeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
dynamicSpace := utils.Max(0, availableSpace-reservedSpace)
|
||||
|
||||
unitSize := 0
|
||||
extraSpace := 0
|
||||
if totalWeight > 0 {
|
||||
unitSize = dynamicSpace / totalWeight
|
||||
extraSpace = dynamicSpace % totalWeight
|
||||
}
|
||||
|
||||
result := make([]int, len(boxes))
|
||||
for i, box := range boxes {
|
||||
if box.isStatic() {
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
result[i] = utils.Min(availableSpace, box.Size)
|
||||
} else {
|
||||
result[i] = unitSize * normalizedWeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
// distribute the remainder across dynamic boxes.
|
||||
for extraSpace > 0 {
|
||||
for i, weight := range normalizedWeights {
|
||||
if weight > 0 {
|
||||
result[i]++
|
||||
extraSpace--
|
||||
normalizedWeights[i]--
|
||||
|
||||
if extraSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2.
|
||||
func normalizeWeights(weights []int) []int {
|
||||
if len(weights) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// to spare us some computation we'll exit early if any of our weights is 1
|
||||
if slices.Some(weights, func(weight int) bool { return weight == 1 }) {
|
||||
return weights
|
||||
}
|
||||
|
||||
// map weights to factorSlices and find the lowest common factor
|
||||
positiveWeights := slices.Filter(weights, func(weight int) bool { return weight > 0 })
|
||||
factorSlices := slices.Map(positiveWeights, func(weight int) []int { return calcFactors(weight) })
|
||||
commonFactors := factorSlices[0]
|
||||
for _, factors := range factorSlices {
|
||||
commonFactors = lo.Intersect(commonFactors, factors)
|
||||
}
|
||||
|
||||
if len(commonFactors) == 0 {
|
||||
return weights
|
||||
}
|
||||
|
||||
newWeights := slices.Map(weights, func(weight int) int { return weight / commonFactors[0] })
|
||||
|
||||
return normalizeWeights(newWeights)
|
||||
}
|
||||
|
||||
func calcFactors(n int) []int {
|
||||
factors := []int{}
|
||||
for i := 2; i <= n; i++ {
|
||||
if n%i == 0 {
|
||||
factors = append(factors, i)
|
||||
}
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
func (b *Box) isStatic() bool {
|
||||
return b.Size > 0
|
||||
}
|
||||
|
||||
func (b *Box) getDirection(width int, height int) Direction {
|
||||
if b.ConditionalDirection != nil {
|
||||
return b.ConditionalDirection(width, height)
|
||||
}
|
||||
return b.Direction
|
||||
}
|
||||
|
||||
func (b *Box) getChildren(width int, height int) []*Box {
|
||||
if b.ConditionalChildren != nil {
|
||||
return b.ConditionalChildren(width, height)
|
||||
}
|
||||
return b.Children
|
||||
}
|
||||
|
||||
func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
|
||||
result := map[string]Dimensions{}
|
||||
for _, dimensionMap := range []map[string]Dimensions{a, b} {
|
||||
for k, v := range dimensionMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package boxlayout
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestArrangeWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
root *Box
|
||||
x0 int
|
||||
y0 int
|
||||
width int
|
||||
height int
|
||||
test func(result map[string]Dimensions)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Empty box",
|
||||
root: &Box{},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(t, result, map[string]Dimensions{})
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with static and dynamic panel",
|
||||
root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic": {X0: 0, X1: 9, Y0: 1, Y1: 9},
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 0},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with static and two dynamic panels",
|
||||
root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 0},
|
||||
"dynamic1": {X0: 0, X1: 9, Y0: 1, Y1: 3},
|
||||
"dynamic2": {X0: 0, X1: 9, Y0: 4, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with COLUMN direction",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 0, Y0: 0, Y1: 9},
|
||||
"dynamic1": {X0: 1, X1: 3, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 4, X1: 9, Y0: 0, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with COLUMN direction only on wide boxes with narrow box",
|
||||
root: &Box{ConditionalDirection: func(width int, height int) Direction {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
return ROW
|
||||
}
|
||||
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 4,
|
||||
height: 4,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 1},
|
||||
"dynamic2": {X0: 0, X1: 3, Y0: 2, Y1: 3},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with COLUMN direction only on wide boxes with wide box",
|
||||
root: &Box{ConditionalDirection: func(width int, height int) Direction {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
return ROW
|
||||
}
|
||||
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
|
||||
// 5 / 2 = 2 remainder 1. That remainder goes to the first box.
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 5,
|
||||
height: 5,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 2, Y0: 0, Y1: 4},
|
||||
"dynamic2": {X0: 3, X1: 4, Y0: 0, Y1: 4},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with conditional children where box is wide",
|
||||
root: &Box{ConditionalChildren: func(width int, height int) []*Box {
|
||||
if width > 4 {
|
||||
return []*Box{{Window: "wide", Weight: 1}}
|
||||
} else {
|
||||
return []*Box{{Window: "narrow", Weight: 1}}
|
||||
}
|
||||
}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 5,
|
||||
height: 5,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"wide": {X0: 0, X1: 4, Y0: 0, Y1: 4},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with conditional children where box is narrow",
|
||||
root: &Box{ConditionalChildren: func(width int, height int) []*Box {
|
||||
if width > 4 {
|
||||
return []*Box{{Window: "wide", Weight: 1}}
|
||||
} else {
|
||||
return []*Box{{Window: "narrow", Weight: 1}}
|
||||
}
|
||||
}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 4,
|
||||
height: 4,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"narrow": {X0: 0, X1: 3, Y0: 0, Y1: 3},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with static child with size too large",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 9},
|
||||
// not sure if X0: 10, X1: 9 makes any sense, but testing this in the
|
||||
// actual GUI it seems harmless
|
||||
"dynamic1": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
// 10 total space minus 2 from the status box leaves us with 8.
|
||||
// Total weight is 3, 8 / 3 = 2 with 2 remainder.
|
||||
// We want to end up with 2, 3, 5 (one unit from remainder to each dynamic box)
|
||||
testName: "Distributing remainder across weighted boxes",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{{Size: 2, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 1, Y0: 0, Y1: 9}, // 2
|
||||
"dynamic1": {X0: 2, X1: 4, Y0: 0, Y1: 9}, // 3
|
||||
"dynamic2": {X0: 5, X1: 9, Y0: 0, Y1: 9}, // 5
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
// 9 total space.
|
||||
// total weight is 5, 9 / 5 = 1 with 4 remainder
|
||||
// we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last.
|
||||
// Reason being that we just give units to each box evenly and consider weight in subsequent passes.
|
||||
testName: "Distributing remainder across weighted boxes 2",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 2, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}, {Weight: 1, Window: "dynamic3"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 9,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4
|
||||
"dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3
|
||||
"dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
// 9 total space.
|
||||
// total weight is 5, 9 / 5 = 1 with 4 remainder
|
||||
// we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last.
|
||||
// Reason being that we just give units to each box evenly and consider weight in subsequent passes.
|
||||
testName: "Distributing remainder across weighted boxes with unnormalized weights",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 4, Window: "dynamic1"}, {Weight: 4, Window: "dynamic2"}, {Weight: 2, Window: "dynamic3"}}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 9,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4
|
||||
"dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3
|
||||
"dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Another distribution test",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{
|
||||
{Weight: 3, Window: "dynamic1"},
|
||||
{Weight: 1, Window: "dynamic2"},
|
||||
{Weight: 1, Window: "dynamic3"},
|
||||
}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 9,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 4, Y0: 0, Y1: 9}, // 5
|
||||
"dynamic2": {X0: 5, X1: 6, Y0: 0, Y1: 9}, // 2
|
||||
"dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Box with zero weight",
|
||||
root: &Box{Direction: COLUMN, Children: []*Box{
|
||||
{Weight: 1, Window: "dynamic1"},
|
||||
{Weight: 0, Window: "dynamic2"},
|
||||
}},
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
test: func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 9, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9}, // when X0 > X1, we will hide the window
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWeights(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
input []int
|
||||
expected []int
|
||||
}{
|
||||
{
|
||||
testName: "empty",
|
||||
input: []int{},
|
||||
expected: []int{},
|
||||
},
|
||||
{
|
||||
testName: "one item of value 1",
|
||||
input: []int{1},
|
||||
expected: []int{1},
|
||||
},
|
||||
{
|
||||
testName: "one item of value greater than 1",
|
||||
input: []int{2},
|
||||
expected: []int{1},
|
||||
},
|
||||
{
|
||||
testName: "slice contains 1",
|
||||
input: []int{2, 1},
|
||||
expected: []int{2, 1},
|
||||
},
|
||||
{
|
||||
testName: "slice contains 2 and 2",
|
||||
input: []int{2, 2},
|
||||
expected: []int{1, 1},
|
||||
},
|
||||
{
|
||||
testName: "no common multiple",
|
||||
input: []int{2, 3},
|
||||
expected: []int{2, 3},
|
||||
},
|
||||
{
|
||||
testName: "complex case",
|
||||
input: []int{10, 10, 20},
|
||||
expected: []int{1, 1, 2},
|
||||
},
|
||||
{
|
||||
testName: "when a zero weight is included it is ignored",
|
||||
input: []int{10, 10, 20, 0},
|
||||
expected: []int{1, 1, 2, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, normalizeWeights(s.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type StashController struct {
|
||||
@@ -44,6 +45,11 @@ func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
Handler: self.checkSelected(self.handleNewBranchOffStashEntry),
|
||||
Description: self.c.Tr.LcNewBranch,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Stash.RenameStash),
|
||||
Handler: self.checkSelected(self.handleRenameStashEntry),
|
||||
Description: self.c.Tr.LcRenameStash,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
@@ -139,3 +145,27 @@ func (self *StashController) postStashRefresh() error {
|
||||
func (self *StashController) handleNewBranchOffStashEntry(stashEntry *models.StashEntry) error {
|
||||
return self.helpers.Refs.NewBranch(stashEntry.RefName(), stashEntry.Description(), "")
|
||||
}
|
||||
|
||||
func (self *StashController) handleRenameStashEntry(stashEntry *models.StashEntry) error {
|
||||
message := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.RenameStashPrompt,
|
||||
map[string]string{
|
||||
"stashName": stashEntry.RefName(),
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: message,
|
||||
InitialContent: stashEntry.Name,
|
||||
HandleConfirm: func(response string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RenameStash)
|
||||
err := self.git.Stash.Rename(stashEntry.Index, response)
|
||||
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.context().SetSelectedLineIdx(0) // Select the renamed stash
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ func (gui *Gui) getSetTextareaTextFn(getView func() *gocui.View) func(string) {
|
||||
view := getView()
|
||||
view.ClearTextArea()
|
||||
view.TextArea.TypeString(text)
|
||||
_ = gui.resizePopupPanel(view, view.TextArea.GetContent())
|
||||
view.RenderTextArea()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,5 @@ func GetKey(key string) types.Key {
|
||||
} else if runeCount == 1 {
|
||||
return []rune(key)[0]
|
||||
}
|
||||
log.Fatal("Key empty for keybinding: " + strings.ToLower(key))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (gui *Gui) handleCreateOptionsMenu() error {
|
||||
OpensMenu: binding.OpensMenu,
|
||||
Label: binding.Description,
|
||||
OnPress: func() error {
|
||||
if binding.Key == nil {
|
||||
if binding.Handler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
COMMIT_ICON = "\ufc16" // ﰖ
|
||||
MERGE_COMMIT_ICON = "\ufb2c" // שּׁ
|
||||
DEFAULT_REMOTE_ICON = "\uf7a1" //
|
||||
STASH_ICON = "\uf01c" //
|
||||
)
|
||||
|
||||
type remoteIcon struct {
|
||||
@@ -59,3 +60,7 @@ func IconForRemote(remote *models.Remote) string {
|
||||
}
|
||||
return DEFAULT_REMOTE_ICON
|
||||
}
|
||||
|
||||
func IconForStash(stash *models.StashEntry) string {
|
||||
return STASH_ICON
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package presentation
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
@@ -19,5 +20,11 @@ func getStashEntryDisplayStrings(s *models.StashEntry, diffed bool) []string {
|
||||
if diffed {
|
||||
textStyle = theme.DiffTerminalColor
|
||||
}
|
||||
return []string{textStyle.Sprint(s.Name)}
|
||||
|
||||
res := make([]string, 0, 2)
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, textStyle.Sprint(icons.IconForStash(s)))
|
||||
}
|
||||
res = append(res, textStyle.Sprint(s.Name))
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@ func chineseTranslationSet() TranslationSet {
|
||||
SureApplyStashEntry: "您确定要应用此贮藏条目?",
|
||||
NoTrackedStagedFilesStash: "没有可以贮藏的已跟踪/暂存文件",
|
||||
StashChanges: "贮藏更改",
|
||||
LcRenameStash: "rename stash",
|
||||
RenameStashPrompt: "Rename stash: {{.stashName}}",
|
||||
OpenConfig: "打开配置文件",
|
||||
EditConfig: "编辑配置文件",
|
||||
ForcePush: "强制推送",
|
||||
@@ -530,6 +532,7 @@ func chineseTranslationSet() TranslationSet {
|
||||
UpdateRemote: "更新远程",
|
||||
ApplyPatch: "应用补丁",
|
||||
Stash: "贮藏 (Stash)",
|
||||
RenameStash: "Rename stash",
|
||||
RemoveSubmodule: "删除子模块",
|
||||
ResetSubmodule: "重置子模块",
|
||||
AddSubmodule: "添加子模块",
|
||||
|
||||
@@ -105,6 +105,8 @@ func dutchTranslationSet() TranslationSet {
|
||||
SureApplyStashEntry: "Weet je zeker dat je deze stash entry wil toepassen?",
|
||||
NoTrackedStagedFilesStash: "Je hebt geen tracked/staged bestanden om te laten stashen",
|
||||
StashChanges: "Stash veranderingen",
|
||||
LcRenameStash: "rename stash",
|
||||
RenameStashPrompt: "Rename stash: {{.stashName}}",
|
||||
NoChangedFiles: "Geen veranderde bestanden",
|
||||
OpenConfig: "open config bestand",
|
||||
EditConfig: "verander config bestand",
|
||||
|
||||
@@ -128,6 +128,8 @@ type TranslationSet struct {
|
||||
NoTrackedStagedFilesStash string
|
||||
NoFilesToStash string
|
||||
StashChanges string
|
||||
LcRenameStash string
|
||||
RenameStashPrompt string
|
||||
OpenConfig string
|
||||
EditConfig string
|
||||
ForcePush string
|
||||
@@ -603,6 +605,7 @@ type Actions struct {
|
||||
UpdateRemote string
|
||||
ApplyPatch string
|
||||
Stash string
|
||||
RenameStash string
|
||||
RemoveSubmodule string
|
||||
ResetSubmodule string
|
||||
AddSubmodule string
|
||||
@@ -771,6 +774,8 @@ func EnglishTranslationSet() TranslationSet {
|
||||
NoTrackedStagedFilesStash: "You have no tracked/staged files to stash",
|
||||
NoFilesToStash: "You have no files to stash",
|
||||
StashChanges: "Stash changes",
|
||||
LcRenameStash: "rename stash",
|
||||
RenameStashPrompt: "Rename stash: {{.stashName}}",
|
||||
OpenConfig: "open config file",
|
||||
EditConfig: "edit config file",
|
||||
ForcePush: "Force push",
|
||||
@@ -1230,6 +1235,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
UpdateRemote: "Update remote",
|
||||
ApplyPatch: "Apply patch",
|
||||
Stash: "Stash",
|
||||
RenameStash: "Rename stash",
|
||||
RemoveSubmodule: "Remove submodule",
|
||||
ResetSubmodule: "Reset submodule",
|
||||
AddSubmodule: "Add submodule",
|
||||
|
||||
@@ -130,6 +130,8 @@ func japaneseTranslationSet() TranslationSet {
|
||||
SureApplyStashEntry: "Stashを適用します。よろしいですか?",
|
||||
// NoTrackedStagedFilesStash: "You have no tracked/staged files to stash",
|
||||
StashChanges: "変更をStash",
|
||||
LcRenameStash: "Stashを変更",
|
||||
RenameStashPrompt: "Stash名を変更: {{.stashName}}",
|
||||
OpenConfig: "設定ファイルを開く",
|
||||
EditConfig: "設定ファイルを編集",
|
||||
ForcePush: "Force push",
|
||||
@@ -556,6 +558,7 @@ func japaneseTranslationSet() TranslationSet {
|
||||
UpdateRemote: "リモートを更新",
|
||||
ApplyPatch: "パッチを適用",
|
||||
Stash: "Stash",
|
||||
RenameStash: "Stash名を変更",
|
||||
RemoveSubmodule: "サブモジュールを削除",
|
||||
ResetSubmodule: "サブモジュールをリセット",
|
||||
AddSubmodule: "サブモジュールを追加",
|
||||
|
||||
@@ -131,6 +131,8 @@ func koreanTranslationSet() TranslationSet {
|
||||
SureApplyStashEntry: "정말로 Stash를 적용하시겠습니까?",
|
||||
NoTrackedStagedFilesStash: "You have no tracked/staged files to stash",
|
||||
StashChanges: "변경을 Stash",
|
||||
LcRenameStash: "rename stash",
|
||||
RenameStashPrompt: "Rename stash: {{.stashName}}",
|
||||
OpenConfig: "설정 파일 열기",
|
||||
EditConfig: "설정 파일 수정",
|
||||
ForcePush: "강제 푸시",
|
||||
@@ -561,6 +563,7 @@ func koreanTranslationSet() TranslationSet {
|
||||
UpdateRemote: "Update remote",
|
||||
ApplyPatch: "Apply patch",
|
||||
Stash: "Stash",
|
||||
RenameStash: "Rename stash",
|
||||
RemoveSubmodule: "서브모듈 삭제",
|
||||
ResetSubmodule: "서브모듈 Reset",
|
||||
AddSubmodule: "서브모듈 추가",
|
||||
|
||||
@@ -83,6 +83,8 @@ func polishTranslationSet() TranslationSet {
|
||||
SureDropStashEntry: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?",
|
||||
NoTrackedStagedFilesStash: "Nie masz śledzonych/zatwierdzonych plików do przechowania",
|
||||
StashChanges: "Przechowaj zmiany",
|
||||
LcRenameStash: "rename stash",
|
||||
RenameStashPrompt: "Rename stash: {{.stashName}}",
|
||||
OpenConfig: "otwórz konfigurację",
|
||||
EditConfig: "edytuj konfigurację",
|
||||
ForcePush: "Wymuś wysłanie",
|
||||
|
||||
@@ -84,3 +84,5 @@ go run pkg/integration/deprecated/cmd/tui/main.go
|
||||
```
|
||||
|
||||
The tests in the old format live in test/integration. In the old format, test definitions are co-located with the snapshots. The setup step is done in a `setup.sh` shell script and the `recording.json` file contains the recorded keypresses to be replayed during the test.
|
||||
|
||||
If you have rewritten an integration test under the new pattern, be sure to delete the old integration test directory.
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// This program lets you run integration tests from a TUI. See pkg/integration/README.md for more info.
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
var SLOW_KEY_PRESS_DELAY = 300
|
||||
|
||||
func RunTUI() {
|
||||
rootDir := utils.GetLazygitRootDirectory()
|
||||
rootDir := utils.GetLazyRootDirectory()
|
||||
testDir := filepath.Join(rootDir, "test", "integration")
|
||||
|
||||
app := newApp(testDir)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// this is the integration runner for the new and improved integration interface
|
||||
@@ -44,7 +44,7 @@ func RunTests(
|
||||
keyPressDelay int,
|
||||
maxAttempts int,
|
||||
) error {
|
||||
projectRootDir := utils.GetLazygitRootDirectory()
|
||||
projectRootDir := utils.GetLazyRootDirectory()
|
||||
err := os.Chdir(projectRootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -111,3 +111,8 @@ func (s *Shell) CreateNCommits(n int) *Shell {
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Shell) StashWithMessage(message string) *Shell {
|
||||
s.RunCommand(fmt.Sprintf(`git stash -m "%s"`, message))
|
||||
return s
|
||||
}
|
||||
|
||||
37
pkg/integration/tests/stash/rename.go
Normal file
37
pkg/integration/tests/stash/rename.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package stash
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Rename = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Try to rename the stash.",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
EmptyCommit("blah").
|
||||
CreateFileAndAdd("file-1", "change to stash1").
|
||||
StashWithMessage("foo").
|
||||
CreateFileAndAdd("file-2", "change to stash2").
|
||||
StashWithMessage("bar")
|
||||
},
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
input.SwitchToStashWindow()
|
||||
assert.CurrentViewName("stash")
|
||||
|
||||
assert.MatchSelectedLine(Equals("On master: bar"))
|
||||
input.NextItem()
|
||||
assert.MatchSelectedLine(Equals("On master: foo"))
|
||||
input.PressKeys(keys.Stash.RenameStash)
|
||||
assert.InPrompt()
|
||||
assert.MatchCurrentViewTitle(Equals("Rename stash: stash@{1}"))
|
||||
|
||||
input.Type(" baz")
|
||||
input.Confirm()
|
||||
|
||||
assert.MatchSelectedLine(Equals("On master: foo baz"))
|
||||
},
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/bisect"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/branch"
|
||||
@@ -41,6 +42,7 @@ var tests = []*components.IntegrationTest{
|
||||
custom_commands.FormPrompts,
|
||||
stash.Stash,
|
||||
stash.StashIncludingUntrackedFiles,
|
||||
stash.Rename,
|
||||
}
|
||||
|
||||
func GetTests() []*components.IntegrationTest {
|
||||
@@ -56,7 +58,7 @@ func GetTests() []*components.IntegrationTest {
|
||||
|
||||
missingTestNames := []string{}
|
||||
|
||||
if err := filepath.Walk(filepath.Join(utils.GetLazygitRootDirectory(), "pkg/integration/tests"), func(path string, info os.FileInfo, err error) error {
|
||||
if err := filepath.Walk(filepath.Join(utils.GetLazyRootDirectory(), "pkg/integration/tests"), func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && strings.HasSuffix(path, ".go") {
|
||||
// ignoring this current file
|
||||
if filepath.Base(path) == "tests.go" {
|
||||
|
||||
@@ -17,6 +17,14 @@ func SplitLines(multilineString string) []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
func SplitNul(str string) []string {
|
||||
if str == "" {
|
||||
return make([]string, 0)
|
||||
}
|
||||
str = strings.TrimSuffix(str, "\x00")
|
||||
return strings.Split(str, "\x00")
|
||||
}
|
||||
|
||||
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
|
||||
func NormalizeLinefeeds(str string) string {
|
||||
str = strings.Replace(str, "\r\n", "\n", -1)
|
||||
|
||||
@@ -36,6 +36,37 @@ func TestSplitLines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNul(t *testing.T) {
|
||||
type scenario struct {
|
||||
multilineString string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"",
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"\x00",
|
||||
[]string{
|
||||
"",
|
||||
},
|
||||
},
|
||||
{
|
||||
"hello world !\x00hello universe !\x00",
|
||||
[]string{
|
||||
"hello world !",
|
||||
"hello universe !",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, SplitNul(s.multilineString))
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeLinefeeds is a function.
|
||||
func TestNormalizeLinefeeds(t *testing.T) {
|
||||
type scenario struct {
|
||||
|
||||
@@ -135,30 +135,3 @@ func FilePath(skip int) string {
|
||||
_, path, _, _ := runtime.Caller(skip)
|
||||
return path
|
||||
}
|
||||
|
||||
// for our cheatsheet script and integration tests. Not to be confused with finding the
|
||||
// root directory of _any_ random repo.
|
||||
func GetLazygitRootDirectory() string {
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
_, err := os.Stat(filepath.Join(path, ".git"))
|
||||
|
||||
if err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path = filepath.Dir(path)
|
||||
|
||||
if path == "/" {
|
||||
log.Fatal("must run in lazygit folder or child folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user