diff options
author | Andrew Hynes <andrewjhynes@gmail.com> | 2022-11-01 16:08:34 -0230 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-01 16:08:34 -0230 |
commit | a47e72892aa9ea5860869a914905cae76d3bd94e (patch) | |
tree | ed837c4d62b825cdd1aa6667baf957258b603bd4 /pkg | |
parent | 69040f094d2fb0b31c902ff4911296960db672c3 (diff) | |
parent | c4e71356bbb0bff7ba4d12d4b86f2959f32fed47 (diff) |
Merge branch 'master' into stash-untracked-changes
Diffstat (limited to 'pkg')
31 files changed, 301 insertions, 641 deletions
diff --git a/pkg/cheatsheet/check.go b/pkg/cheatsheet/check.go index 6123e4aa5..5c4df97ef 100644 --- a/pkg/cheatsheet/check.go +++ b/pkg/cheatsheet/check.go @@ -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 { diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 3b9d9c2d1..437db154d 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -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) { diff --git a/pkg/commands/git_commands/stash.go b/pkg/commands/git_commands/stash.go index 40e205a67..85056cd5c 100644 --- a/pkg/commands/git_commands/stash.go +++ b/pkg/commands/git_commands/stash.go @@ -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 +} diff --git a/pkg/commands/git_commands/stash_test.go b/pkg/commands/git_commands/stash_test.go index b41f7815b..1a4e50822 100644 --- a/pkg/commands/git_commands/stash_test.go +++ b/pkg/commands/git_commands/stash_test.go @@ -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) + }) + } +} diff --git a/pkg/commands/loaders/stash.go b/pkg/commands/loaders/stash.go index 66cfeaa3e..4c9d16a23 100644 --- a/pkg/commands/loaders/stash.go +++ b/pkg/commands/loaders/stash.go @@ -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) }) } diff --git a/pkg/commands/loaders/stash_test.go b/pkg/commands/loaders/stash_test.go index e7999e861..707be090e 100644 --- a/pkg/commands/loaders/stash_test.go +++ b/pkg/commands/loaders/stash_test.go @@ -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{ diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index f3ff1befb..a90b100d5 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -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", diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 89236ae5c..ecff17893 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -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" diff --git a/pkg/gui/boxlayout/boxlayout.go b/pkg/gui/boxlayout/boxlayout.go deleted file mode 100644 index 4eb6f15e6..000000000 --- a/pkg/gui/boxlayout/boxlayout.go +++ /dev/null @@ -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 -} diff --git a/pkg/gui/boxlayout/boxlayout_test.go b/pkg/gui/boxlayout/boxlayout_test.go deleted file mode 100644 index c2c0bc9e4..000000000 --- a/pkg/gui/boxlayout/boxlayout_test.go +++ /dev/null @@ -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 rema |