1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-10 04:07:18 +02:00

Merge pull request #2475 from jesseduffield/migrate-patch-building-tests

This commit is contained in:
Jesse Duffield 2023-02-25 21:40:02 +11:00 committed by GitHub
commit f6fafc65ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
162 changed files with 1217 additions and 492 deletions

View File

@ -98,8 +98,9 @@ jobs:
restore-keys: |
${{runner.os}}-go-
- name: Test code
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
run: |
go test pkg/integration/clients/*.go
LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go
build:
runs-on: ubuntu-latest
env:

View File

@ -1,6 +1,8 @@
package mergeconflicts
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@ -188,9 +190,30 @@ func (s *State) ContentAfterConflictResolve(selection Selection) (bool, string,
func (s *State) GetSelectedLine() int {
conflict := s.currentConflict()
if conflict == nil {
// TODO: see why this is 1 and not 0
return 1
}
selection := s.Selection()
startIndex, _ := selection.bounds(conflict)
return startIndex + 1
}
func (s *State) GetSelectedRange() (int, int) {
conflict := s.currentConflict()
if conflict == nil {
return 0, 0
}
selection := s.Selection()
startIndex, endIndex := selection.bounds(conflict)
return startIndex, endIndex
}
func (s *State) PlainRenderSelected() string {
startIndex, endIndex := s.GetSelectedRange()
content := s.GetContent()
contentLines := utils.SplitLines(content)
return strings.Join(contentLines[startIndex:endIndex+1], "\n")
}

View File

@ -38,3 +38,9 @@ func (self *Actions) ConfirmDiscardLines() {
Content(Contains("Are you sure you want to delete the selected lines")).
Confirm()
}
func (self *Actions) SelectPatchOption(matcher *Matcher) {
self.t.GlobalPress(self.t.keys.Universal.CreatePatchOptionsMenu)
self.t.ExpectPopup().Menu().Title(Equals("Patch Options")).Select(matcher).Confirm()
}

View File

@ -11,7 +11,7 @@ func (self *AlertDriver) getViewDriver() *ViewDriver {
}
// asserts that the alert view has the expected title
func (self *AlertDriver) Title(expected *matcher) *AlertDriver {
func (self *AlertDriver) Title(expected *Matcher) *AlertDriver {
self.getViewDriver().Title(expected)
self.hasCheckedTitle = true
@ -20,7 +20,7 @@ func (self *AlertDriver) Title(expected *matcher) *AlertDriver {
}
// asserts that the alert view has the expected content
func (self *AlertDriver) Content(expected *matcher) *AlertDriver {
func (self *AlertDriver) Content(expected *Matcher) *AlertDriver {
self.getViewDriver().Content(expected)
self.hasCheckedContent = true

View File

@ -1,6 +1,7 @@
package components
import (
"os"
"time"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
@ -11,9 +12,17 @@ type assertionHelper struct {
}
// milliseconds we'll wait when an assertion fails.
var retryWaitTimes = []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000}
func retryWaitTimes() []int {
if os.Getenv("LONG_WAIT_BEFORE_FAIL") == "true" {
// CI has limited hardware, may be throttled, runs tests in parallel, etc, so we
// give it more leeway compared to when we're running things locally.
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000}
} else {
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100}
}
}
func (self *assertionHelper) matchString(matcher *matcher, context string, getValue func() string) {
func (self *assertionHelper) matchString(matcher *Matcher, context string, getValue func() string) {
self.assertWithRetries(func() (bool, string) {
value := getValue()
return matcher.context(context).test(value)
@ -22,7 +31,7 @@ func (self *assertionHelper) matchString(matcher *matcher, context string, getVa
func (self *assertionHelper) assertWithRetries(test func() (bool, string)) {
var message string
for _, waitTime := range retryWaitTimes {
for _, waitTime := range retryWaitTimes() {
time.Sleep(time.Duration(waitTime) * time.Millisecond)
var ok bool

View File

@ -9,7 +9,7 @@ func (self *CommitMessagePanelDriver) getViewDriver() *ViewDriver {
}
// asserts on the text initially present in the prompt
func (self *CommitMessagePanelDriver) InitialText(expected *matcher) *CommitMessagePanelDriver {
func (self *CommitMessagePanelDriver) InitialText(expected *Matcher) *CommitMessagePanelDriver {
self.getViewDriver().Content(expected)
return self

View File

@ -11,7 +11,7 @@ func (self *ConfirmationDriver) getViewDriver() *ViewDriver {
}
// asserts that the confirmation view has the expected title
func (self *ConfirmationDriver) Title(expected *matcher) *ConfirmationDriver {
func (self *ConfirmationDriver) Title(expected *Matcher) *ConfirmationDriver {
self.getViewDriver().Title(expected)
self.hasCheckedTitle = true
@ -20,7 +20,7 @@ func (self *ConfirmationDriver) Title(expected *matcher) *ConfirmationDriver {
}
// asserts that the confirmation view has the expected content
func (self *ConfirmationDriver) Content(expected *matcher) *ConfirmationDriver {
func (self *ConfirmationDriver) Content(expected *Matcher) *ConfirmationDriver {
self.getViewDriver().Content(expected)
self.hasCheckedContent = true

View File

@ -26,7 +26,7 @@ func (self *FileSystem) PathNotPresent(path string) {
}
// Asserts that the file at the given path has the given content
func (self *FileSystem) FileContent(path string, matcher *matcher) {
func (self *FileSystem) FileContent(path string, matcher *Matcher) {
self.assertWithRetries(func() (bool, string) {
_, err := os.Stat(path)
if os.IsNotExist(err) {

View File

@ -9,7 +9,7 @@ import (
)
// for making assertions on string values
type matcher struct {
type Matcher struct {
rules []matcherRule
// this is printed when there's an error so that it's clear what the context of the assertion is
@ -24,12 +24,12 @@ type matcherRule struct {
testFn func(string) (bool, string)
}
func NewMatcher(name string, testFn func(string) (bool, string)) *matcher {
func NewMatcher(name string, testFn func(string) (bool, string)) *Matcher {
rules := []matcherRule{{name: name, testFn: testFn}}
return &matcher{rules: rules}
return &Matcher{rules: rules}
}
func (self *matcher) name() string {
func (self *Matcher) name() string {
if len(self.rules) == 0 {
return "anything"
}
@ -40,7 +40,7 @@ func (self *matcher) name() string {
)
}
func (self *matcher) test(value string) (bool, string) {
func (self *Matcher) test(value string) (bool, string) {
for _, rule := range self.rules {
ok, message := rule.testFn(value)
if ok {
@ -57,7 +57,7 @@ func (self *matcher) test(value string) (bool, string) {
return true, ""
}
func (self *matcher) Contains(target string) *matcher {
func (self *Matcher) Contains(target string) *Matcher {
return self.appendRule(matcherRule{
name: fmt.Sprintf("contains '%s'", target),
testFn: func(value string) (bool, string) {
@ -71,7 +71,7 @@ func (self *matcher) Contains(target string) *matcher {
})
}
func (self *matcher) DoesNotContain(target string) *matcher {
func (self *Matcher) DoesNotContain(target string) *Matcher {
return self.appendRule(matcherRule{
name: fmt.Sprintf("does not contain '%s'", target),
testFn: func(value string) (bool, string) {
@ -80,7 +80,7 @@ func (self *matcher) DoesNotContain(target string) *matcher {
})
}
func (self *matcher) MatchesRegexp(target string) *matcher {
func (self *Matcher) MatchesRegexp(target string) *Matcher {
return self.appendRule(matcherRule{
name: fmt.Sprintf("matches regular expression '%s'", target),
testFn: func(value string) (bool, string) {
@ -93,7 +93,7 @@ func (self *matcher) MatchesRegexp(target string) *matcher {
})
}
func (self *matcher) Equals(target string) *matcher {
func (self *Matcher) Equals(target string) *Matcher {
return self.appendRule(matcherRule{
name: fmt.Sprintf("equals '%s'", target),
testFn: func(value string) (bool, string) {
@ -106,7 +106,7 @@ const IS_SELECTED_RULE_NAME = "is selected"
// special rule that is only to be used in the TopLines and Lines methods, as a way of
// asserting that a given line is selected.
func (self *matcher) IsSelected() *matcher {
func (self *Matcher) IsSelected() *Matcher {
return self.appendRule(matcherRule{
name: IS_SELECTED_RULE_NAME,
testFn: func(value string) (bool, string) {
@ -115,7 +115,7 @@ func (self *matcher) IsSelected() *matcher {
})
}
func (self *matcher) appendRule(rule matcherRule) *matcher {
func (self *Matcher) appendRule(rule matcherRule) *Matcher {
self.rules = append(self.rules, rule)
return self
@ -123,39 +123,43 @@ func (self *matcher) appendRule(rule matcherRule) *matcher {
// adds context so that if the matcher test(s) fails, we understand what we were trying to test.
// E.g. prefix: "Unexpected content in view 'files'."
func (self *matcher) context(prefix string) *matcher {
func (self *Matcher) context(prefix string) *Matcher {
self.prefix = prefix
return self
}
// if the matcher has an `IsSelected` rule, it returns true, along with the matcher after that rule has been removed
func (self *matcher) checkIsSelected() (bool, *matcher) {
check := lo.ContainsBy(self.rules, func(rule matcherRule) bool { return rule.name == IS_SELECTED_RULE_NAME })
func (self *Matcher) checkIsSelected() (bool, *Matcher) {
// copying into a new matcher in case we want to re-use the original later
newMatcher := &Matcher{}
*newMatcher = *self
self.rules = lo.Filter(self.rules, func(rule matcherRule, _ int) bool { return rule.name != IS_SELECTED_RULE_NAME })
check := lo.ContainsBy(newMatcher.rules, func(rule matcherRule) bool { return rule.name == IS_SELECTED_RULE_NAME })
return check, self
newMatcher.rules = lo.Filter(newMatcher.rules, func(rule matcherRule, _ int) bool { return rule.name != IS_SELECTED_RULE_NAME })
return check, newMatcher
}
// this matcher has no rules meaning it always passes the test. Use this
// when you don't care what value you're dealing with.
func Anything() *matcher {
return &matcher{}
func Anything() *Matcher {
return &Matcher{}
}
func Contains(target string) *matcher {
func Contains(target string) *Matcher {
return Anything().Contains(target)
}
func DoesNotContain(target string) *matcher {
func DoesNotContain(target string) *Matcher {
return Anything().DoesNotContain(target)
}
func MatchesRegexp(target string) *matcher {
func MatchesRegexp(target string) *Matcher {
return Anything().MatchesRegexp(target)
}
func Equals(target string) *matcher {
func Equals(target string) *Matcher {
return Anything().Equals(target)
}

View File

@ -10,7 +10,7 @@ func (self *MenuDriver) getViewDriver() *ViewDriver {
}
// asserts that the popup has the expected title
func (self *MenuDriver) Title(expected *matcher) *MenuDriver {
func (self *MenuDriver) Title(expected *Matcher) *MenuDriver {
self.getViewDriver().Title(expected)
self.hasCheckedTitle = true
@ -30,19 +30,19 @@ func (self *MenuDriver) Cancel() {
self.getViewDriver().PressEscape()
}
func (self *MenuDriver) Select(option *matcher) *MenuDriver {
func (self *MenuDriver) Select(option *Matcher) *MenuDriver {
self.getViewDriver().NavigateToListItem(option)
return self
}
func (self *MenuDriver) Lines(matchers ...*matcher) *MenuDriver {
func (self *MenuDriver) Lines(matchers ...*Matcher) *MenuDriver {
self.getViewDriver().Lines(matchers...)
return self
}
func (self *MenuDriver) TopLines(matchers ...*matcher) *MenuDriver {
func (self *MenuDriver) TopLines(matchers ...*Matcher) *MenuDriver {
self.getViewDriver().TopLines(matchers...)
return self

View File

@ -10,7 +10,7 @@ func (self *PromptDriver) getViewDriver() *ViewDriver {
}
// asserts that the popup has the expected title
func (self *PromptDriver) Title(expected *matcher) *PromptDriver {
func (self *PromptDriver) Title(expected *Matcher) *PromptDriver {
self.getViewDriver().Title(expected)
self.hasCheckedTitle = true
@ -19,7 +19,7 @@ func (self *PromptDriver) Title(expected *matcher) *PromptDriver {
}
// asserts on the text initially present in the prompt
func (self *PromptDriver) InitialText(expected *matcher) *PromptDriver {
func (self *PromptDriver) InitialText(expected *Matcher) *PromptDriver {
self.getViewDriver().Content(expected)
return self
@ -55,13 +55,13 @@ func (self *PromptDriver) checkNecessaryChecksCompleted() {
}
}
func (self *PromptDriver) SuggestionLines(matchers ...*matcher) *PromptDriver {
func (self *PromptDriver) SuggestionLines(matchers ...*Matcher) *PromptDriver {
self.t.Views().Suggestions().Lines(matchers...)
return self
}
func (self *PromptDriver) SuggestionTopLines(matchers ...*matcher) *PromptDriver {
func (self *PromptDriver) SuggestionTopLines(matchers ...*Matcher) *PromptDriver {
self.t.Views().Suggestions().TopLines(matchers...)
return self
@ -75,7 +75,7 @@ func (self *PromptDriver) ConfirmFirstSuggestion() {
PressEnter()
}
func (self *PromptDriver) ConfirmSuggestion(matcher *matcher) {
func (self *PromptDriver) ConfirmSuggestion(matcher *Matcher) {
self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions().
IsFocused().

View File

@ -12,7 +12,7 @@ func (self *SearchDriver) getViewDriver() *ViewDriver {
}
// asserts on the text initially present in the prompt
func (self *SearchDriver) InitialText(expected *matcher) *SearchDriver {
func (self *SearchDriver) InitialText(expected *Matcher) *SearchDriver {
self.getViewDriver().Content(expected)
return self

View File

@ -136,6 +136,10 @@ func (self *Shell) EmptyCommit(message string) *Shell {
return self.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message))
}
func (self *Shell) Revert(ref string) *Shell {
return self.RunCommand(fmt.Sprintf("git revert %s", ref))
}
func (self *Shell) CreateLightweightTag(name string, ref string) *Shell {
return self.RunCommand(fmt.Sprintf("git tag %s %s", name, ref))
}

View File

@ -2,7 +2,6 @@ package components
import (
"fmt"
"strings"
"time"
"github.com/atotto/clipboard"
@ -71,65 +70,6 @@ func (self *TestDriver) Shell() *Shell {
return self.shell
}
// this will look for a list item in the current panel and if it finds it, it will
// enter the keypresses required to navigate to it.
// The test will fail if:
// - the user is not in a list item
// - no list item is found containing the given text
// - multiple list items are found containing the given text in the initial page of items
//
// NOTE: this currently assumes that ViewBufferLines returns all the lines that can be accessed.
// If this changes in future, we'll need to update this code to first attempt to find the item
// in the current page and failing that, jump to the top of the view and iterate through all of it,
// looking for the item.
func (self *TestDriver) navigateToListItem(matcher *matcher) {
currentContext := self.gui.CurrentContext()
view := currentContext.GetView()
var matchIndex int
self.assertWithRetries(func() (bool, string) {
matchIndex = -1
var matches []string
lines := view.ViewBufferLines()
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
for i, line := range lines {
ok, _ := matcher.test(line)
if ok {
matches = append(matches, line)
matchIndex = i
}
}
if len(matches) > 1 {
return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n"))
} else if len(matches) == 0 {
return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
} else {
return true, ""
}
})
selectedLineIdx := view.SelectedLineIdx()
if selectedLineIdx == matchIndex {
self.Views().current().SelectedLine(matcher)
return
}
if selectedLineIdx < matchIndex {
for i := selectedLineIdx; i < matchIndex; i++ {
self.Views().current().SelectNextItem()
}
self.Views().current().SelectedLine(matcher)
return
} else {
for i := selectedLineIdx; i > matchIndex; i-- {
self.Views().current().SelectPreviousItem()
}
self.Views().current().SelectedLine(matcher)
return
}
}
// for making assertions on lazygit views
func (self *TestDriver) Views() *Views {
return &Views{t: self}
@ -140,11 +80,11 @@ func (self *TestDriver) ExpectPopup() *Popup {
return &Popup{t: self}
}
func (self *TestDriver) ExpectToast(matcher *matcher) {
func (self *TestDriver) ExpectToast(matcher *Matcher) {
self.Views().AppStatus().Content(matcher)
}
func (self *TestDriver) ExpectClipboard(matcher *matcher) {
func (self *TestDriver) ExpectClipboard(matcher *Matcher) {
self.assertWithRetries(func() (bool, string) {
text, err := clipboard.ReadAll()
if err != nil {

View File

@ -10,45 +10,12 @@ import (
type ViewDriver struct {
// context is prepended to any error messages e.g. 'context: "current view"'
context string
getView func() *gocui.View
t *TestDriver
getSelectedLinesFn func() ([]string, error)
}
// asserts that the view has the expected title
func (self *ViewDriver) Title(expected *matcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
actual := self.getView().Title
return expected.context(fmt.Sprintf("%s title", self.context)).test(actual)
})
return self
}
// asserts that the view has lines matching the given matchers. So if three matchers
// are passed, we only check the first three lines of the view.
// This method is convenient when you have a list of commits but you only want to
// assert on the first couple of commits.
func (self *ViewDriver) TopLines(matchers ...*matcher) *ViewDriver {
if len(matchers) < 1 {
self.t.fail("TopLines method requires at least one matcher. If you are trying to assert that there are no lines, use .IsEmpty()")
}
self.t.assertWithRetries(func() (bool, string) {
lines := self.getView().BufferLines()
return len(lines) >= len(matchers), fmt.Sprintf("unexpected number of lines in view. Expected at least %d, got %d", len(matchers), len(lines))
})
return self.assertLines(matchers...)
}
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line.
// If you only care about the top n lines, use the TopLines method instead.
func (self *ViewDriver) Lines(matchers ...*matcher) *ViewDriver {
self.LineCount(len(matchers))
return self.assertLines(matchers...)
context string
getView func() *gocui.View
t *TestDriver
getSelectedLinesFn func() ([]string, error)
getSelectedRangeFn func() (int, int, error)
getSelectedLineIdxFn func() (int, error)
}
func (self *ViewDriver) getSelectedLines() ([]string, error) {
@ -61,7 +28,114 @@ func (self *ViewDriver) getSelectedLines() ([]string, error) {
return self.getSelectedLinesFn()
}
func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver {
func (self *ViewDriver) getSelectedRange() (int, int, error) {
if self.getSelectedRangeFn == nil {
view := self.t.gui.View(self.getView().Name())
idx := view.SelectedLineIdx()
return idx, idx, nil
}
return self.getSelectedRangeFn()
}
// even if you have a selected range, there may still be a line within that range
// which the cursor points at. This function returns that line index.
func (self *ViewDriver) getSelectedLineIdx() (int, error) {
if self.getSelectedLineIdxFn == nil {
view := self.t.gui.View(self.getView().Name())
return view.SelectedLineIdx(), nil
}
return self.getSelectedLineIdxFn()
}
// asserts that the view has the expected title
func (self *ViewDriver) Title(expected *Matcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
actual := self.getView().Title
return expected.context(fmt.Sprintf("%s title", self.context)).test(actual)
})
return self
}
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line.
// If you only care about the top n lines, use the TopLines method instead.
func (self *ViewDriver) Lines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.LineCount(len(matchers))
return self.assertLines(0, matchers...)
}
// asserts that the view has lines matching the given matchers. So if three matchers
// are passed, we only check the first three lines of the view.
// This method is convenient when you have a list of commits but you only want to
// assert on the first couple of commits.
func (self *ViewDriver) TopLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
return self.assertLines(0, matchers...)
}
func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
self.t.assertWithRetries(func() (bool, string) {
content := self.getView().Buffer()
lines := strings.Split(content, "\n")
startIdx, endIdx, err := self.getSelectedRange()
for i := 0; i < len(lines)-len(matchers)+1; i++ {
matches := true
for j, matcher := range matchers {
checkIsSelected, matcher := matcher.checkIsSelected() // strip the IsSelected matcher out
lineIdx := i + j
ok, _ := matcher.test(lines[lineIdx])
if !ok {
matches = false
break
}
if checkIsSelected {
if err != nil {
matches = false
break
}
if lineIdx < startIdx || lineIdx > endIdx {
matches = false
break
}
}
}
if matches {
return true, ""
}
}
expectedContent := expectedContentFromMatchers(matchers)
return false, fmt.Sprintf(
"Expected the following to be contained in the staging panel:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----\nSelected range: %d-%d",
expectedContent,
content,
startIdx,
endIdx,
)
})
return self
}
// asserts on the lines that are selected in the view.
func (self *ViewDriver) SelectedLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
self.t.assertWithRetries(func() (bool, string) {
selectedLines, err := self.getSelectedLines()
if err != nil {
@ -76,7 +150,12 @@ func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver {
}
for i, line := range selectedLines {
ok, message := matchers[i].test(line)
checkIsSelected, matcher := matchers[i].checkIsSelected()
if checkIsSelected {
self.t.fail("You cannot use the IsSelected matcher with the SelectedLines method")
}
ok, message := matcher.test(line)
if !ok {
return false, fmt.Sprintf("Error: %s. Expected the following to be selected:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----", message, expectedContent, selectedContent)
}
@ -88,53 +167,51 @@ func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver {
return self
}
func (self *ViewDriver) ContainsLines(matchers ...*matcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
content := self.getView().Buffer()
lines := strings.Split(content, "\n")
for i := 0; i < len(lines)-len(matchers)+1; i++ {
matches := true
for j, matcher := range matchers {
ok, _ := matcher.test(lines[i+j])
if !ok {
matches = false
break
}
}
if matches {
return true, ""
}
}
expectedContent := expectedContentFromMatchers(matchers)
return false, fmt.Sprintf(
"Expected the following to be contained in the staging panel:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----",
expectedContent,
content,
)
})
return self
func (self *ViewDriver) validateMatchersPassed(matchers []*Matcher) {
if len(matchers) < 1 {
self.t.fail("'Lines' methods require at least one matcher to be passed as an argument. If you are trying to assert that there are no lines, use .IsEmpty()")
}
}
func (self *ViewDriver) assertLines(matchers ...*matcher) *ViewDriver {
func (self *ViewDriver) validateEnoughLines(matchers []*Matcher) {
self.t.assertWithRetries(func() (bool, string) {
lines := self.getView().BufferLines()
return len(lines) >= len(matchers), fmt.Sprintf("unexpected number of lines in view. Expected at least %d, got %d", len(matchers), len(lines))
})
}
func (self *ViewDriver) assertLines(offset int, matchers ...*Matcher) *ViewDriver {
view := self.getView()
for i, matcher := range matchers {
for matcherIndex, matcher := range matchers {
lineIdx := matcherIndex + offset
checkIsSelected, matcher := matcher.checkIsSelected()
self.t.matchString(matcher, fmt.Sprintf("Unexpected content in view '%s'.", view.Name()),
func() string {
return view.BufferLines()[i]
return view.BufferLines()[lineIdx]
},
)
if checkIsSelected {
self.t.assertWithRetries(func() (bool, string) {
lineIdx := view.SelectedLineIdx()
return lineIdx == i, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), i, lineIdx)
startIdx, endIdx, err := self.getSelectedRange()
if err != nil {
return false, err.Error()
}
if lineIdx < startIdx || lineIdx > endIdx {
if startIdx == endIdx {
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), lineIdx, startIdx)
} else {
lines, err := self.getSelectedLines()
if err != nil {
return false, err.Error()
}
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected line %d to be in range %d to %d. Selected lines:\n---\n%s\n---\n\nExpected line: '%s'", view.Name(), lineIdx, startIdx, endIdx, strings.Join(lines, "\n"), matcher.name())
}
}
return true, ""
})
}
}
@ -143,7 +220,7 @@ func (self *ViewDriver) assertLines(matchers ...*matcher) *ViewDriver {
}
// asserts on the content of the view i.e. the stuff within the view's frame.
func (self *ViewDriver) Content(matcher *matcher) *ViewDriver {
func (self *ViewDriver) Content(matcher *Matcher) *ViewDriver {
self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context),
func() string {
return self.getView().Buffer()
@ -153,40 +230,27 @@ func (self *ViewDriver) Content(matcher *matcher) *ViewDriver {
return self
}
// asserts on the selected line of the view
func (self *ViewDriver) SelectedLine(matcher *matcher) *ViewDriver {
// asserts on the selected line of the view. If your view has multiple lines selected,
// but also has a concept of a cursor position, this will assert on the line that
// the cursor is on. Otherwise it will assert on the first line of the selection.
func (self *ViewDriver) SelectedLine(matcher *Matcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
selectedLines, err := self.getSelectedLines()
selectedLineIdx, err := self.getSelectedLineIdx()
if err != nil {
return false, err.Error()
}
if len(selectedLines) == 0 {
return false, "No line selected. Expected exactly one line to be selected"
} else if len(selectedLines) > 1 {
return false, fmt.Sprintf(
"Multiple lines selected. Expected only a single line to be selected. Selected lines:\n---\n%s\n---\n\nExpected line: %s",
strings.Join(selectedLines, "\n"),
matcher.name(),
)
viewLines := self.getView().BufferLines()
if selectedLineIdx >= len(viewLines) {
return false, fmt.Sprintf("%s: Expected view to have at least %d lines, but it only has %d", self.context, selectedLineIdx+1, len(viewLines))
}
value := selectedLines[0]
value := viewLines[selectedLineIdx]
return matcher.context(fmt.Sprintf("%s: Unexpected selected line.", self.context)).test(value)
})
self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected selected line.", self.context),
func() string {
selectedLines, err := self.getSelectedLines()
if err != nil {
self.t.gui.Fail(err.Error())
return "<failed to obtain selected line>"
}
return selectedLines[0]
},
)
return self
}
@ -298,10 +362,63 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
return self.Press(self.t.keys.Universal.Return)
}
func (self *ViewDriver) NavigateToListItem(matcher *matcher) *ViewDriver {
// this will look for a list item in the current panel and if it finds it, it will
// enter the keypresses required to navigate to it.
// The test will fail if:
// - the user is not in a list item
// - no list item is found containing the given text
// - multiple list items are found containing the given text in the initial page of items
//
// NOTE: this currently assumes that BufferLines returns all the lines that can be accessed.
// If this changes in future, we'll need to update this code to first attempt to find the item
// in the current page and failing that, jump to the top of the view and iterate through all of it,
// looking for the item.
func (self *ViewDriver) NavigateToListItem(matcher *Matcher) *ViewDriver {
self.IsFocused()
self.t.navigateToListItem(matcher)
view := self.getView()
var matchIndex int
self.t.assertWithRetries(func() (bool, string) {
matchIndex = -1
var matches []string
lines := view.BufferLines()
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
for i, line := range lines {
ok, _ := matcher.test(line)
if ok {
matches = append(matches, line)
matchIndex = i
}
}
if len(matches) > 1 {
return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n"))
} else if len(matches) == 0 {
return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
} else {
return true, ""
}
})
selectedLineIdx, err := self.getSelectedLineIdx()
if err != nil {
self.t.fail(err.Error())
return self
}
if selectedLineIdx == matchIndex {
self.SelectedLine(matcher)
} else if selectedLineIdx < matchIndex {
for i := selectedLineIdx; i < matchIndex; i++ {
self.SelectNextItem()
}
self.SelectedLine(matcher)
} else {
for i := selectedLineIdx; i > matchIndex; i-- {
self.SelectPreviousItem()
}
self.SelectedLine(matcher)
}
return self
}
@ -349,8 +466,13 @@ func (self *ViewDriver) Tap(f func()) *ViewDriver {
return self
}
func expectedContentFromMatchers(matchers []*matcher) string {
return strings.Join(lo.Map(matchers, func(matcher *matcher, _ int) string {
// This purely exists as a convenience method for those who hate the trailing periods in multi-line method chains
func (self *ViewDriver) Self() *ViewDriver {
return self
}
func expectedContentFromMatchers(matchers []*Matcher) string {
return strings.Join(lo.Map(matchers, func(matcher *Matcher, _ int) string {
return matcher.name()
}), "\n")
}

View File

@ -40,34 +40,97 @@ func (self *Views) Secondary() *ViewDriver {
}
func (self *Views) regularView(viewName string) *ViewDriver {
return self.newStaticViewDriver(viewName, nil)
return self.newStaticViewDriver(viewName, nil, nil, nil)
}
func (self *Views) patchExplorerViewByName(viewName string) *ViewDriver {
return self.newStaticViewDriver(viewName, func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := state.PlainRenderSelected()
// the above method returns a string with a trailing newline so we need to remove that before splitting
selectedLines := strings.Split(strings.TrimSuffix(selectedContent, "\n"), "\n")
return selectedLines, nil
})
return self.newStaticViewDriver(
viewName,
func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := state.PlainRenderSelected()
// the above method returns a string with a trailing newline so we need to remove that before splitting
selectedLines := strings.Split(strings.TrimSuffix(selectedContent, "\n"), "\n")
return selectedLines, nil
},
func() (int, int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return 0, 0, errors.New("Expected patch explorer to be activated")
}
startIdx, endIdx := state.SelectedRange()
return startIdx, endIdx, nil
},
func() (int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return 0, errors.New("Expected patch explorer to be activated")
}
return state.GetSelectedLineIdx(), nil
},
)
}
// 'static' because it'll always refer to the same view, as opposed to the 'main' view which could actually be
// one of several views, or the 'current' view which depends on focus.
func (self *Views) newStaticViewDriver(viewName string, getSelectedLinesFn func() ([]string, error)) *ViewDriver {
func (self *Views) newStaticViewDriver(
viewName string,
getSelectedLinesFn func() ([]string, error),
getSelectedLineRangeFn func() (int, int, error),
getSelectedLineIdxFn func() (int, error),
) *ViewDriver {
return &ViewDriver{
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.t.gui.View(viewName) },
getSelectedLinesFn: getSelectedLinesFn,
t: self.t,
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.t.gui.View(viewName) },
getSelectedLinesFn: getSelectedLinesFn,
getSelectedRangeFn: getSelectedLineRangeFn,
getSelectedLineIdxFn: getSelectedLineIdxFn,
t: self.t,
}
}
func (self *Views) MergeConflicts() *ViewDriver {
viewName := "mergeConflicts"
return self.newStaticViewDriver(
viewName,
func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := strings.Split(state.PlainRenderSelected(), "\n")
return selectedContent, nil
},
func() (int, int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return 0, 0, errors.New("Expected patch explorer to be activated")
}
startIdx, endIdx := state.GetSelectedRange()
return startIdx, endIdx, nil
},
// there is no concept of a cursor in the merge conflicts panel so we just return the start of the selection
func() (int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return 0, errors.New("Expected patch explorer to be activated")
}
startIdx, _ := state.GetSelectedRange()
return startIdx, nil
},
)
}
func (self *Views) Commits() *ViewDriver {
return self.regularView("commits")
}
@ -158,10 +221,6 @@ func (self *Views) Suggestions() *ViewDriver {
return self.regularView("suggestions")
}
func (self *Views) MergeConflicts() *ViewDriver {
return self.regularView("mergeConflicts")
}
func (self *Views) Search() *ViewDriver {
return self.regularView("search")
}

View File

@ -0,0 +1,64 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var Apply = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Apply a custom patch",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("branch-a")
shell.CreateFileAndAdd("file1", "first line\n")
shell.Commit("first commit")
shell.NewBranch("branch-b")
shell.UpdateFileAndAdd("file1", "first line\nsecond line\n")
shell.Commit("update")
shell.Checkout("branch-a")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("branch-a").IsSelected(),
Contains("branch-b"),
).
Press(keys.Universal.NextItem).
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains("update").IsSelected(),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("M file1").IsSelected(),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Views().PatchBuildingSecondary().Content(Contains("second line"))
t.Actions().SelectPatchOption(MatchesRegexp(`apply patch$`))
t.Views().Files().
Focus().
Lines(
Contains("file1").IsSelected(),
)
t.Views().Main().
Content(Contains("second line"))
},
})

View File

@ -0,0 +1,49 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ApplyInReverse = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Apply a custom patch in reverse",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content\n")
shell.CreateFileAndAdd("file2", "file2 content\n")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("file2"),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Views().PatchBuildingSecondary().Content(Contains("+file1 content"))
t.Actions().SelectPatchOption(Contains("apply patch in reverse"))
t.Views().Files().
Focus().
Lines(
Contains("D").Contains("file1").IsSelected(),
)
t.Views().Main().
Content(Contains("-file1 content"))
},
})

View File

@ -8,7 +8,7 @@ import (
var CopyPatchToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Create a patch from the commits and copy the patch to clipbaord.",
ExtraCmdArgs: "",
Skip: true,
Skip: true, // skipping because CI doesn't have clipboard functionality
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("branch-a")
@ -40,11 +40,7 @@ var CopyPatchToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(Contains("building patch"))
t.Views().
CommitFiles().
Press(keys.Universal.CreatePatchOptionsMenu)
t.ExpectPopup().Menu().Title(Equals("Patch Options")).Select(Contains("copy patch to clipboard")).Confirm()
t.Actions().SelectPatchOption(Contains("copy patch to clipboard"))
t.ExpectToast(Contains("Patch copied to clipboard"))

View File

@ -0,0 +1,66 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToIndex = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to the index",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content\n")
shell.CreateFileAndAdd("file2", "file2 content\n")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("file2"),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Views().PatchBuildingSecondary().Content(Contains("+file1 content"))
t.Actions().SelectPatchOption(Contains("move patch out into index"))
t.Views().Files().
Lines(
Contains("A").Contains("file1"),
)
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file2").IsSelected(),
).
PressEscape()
t.Views().Main().
Content(Contains("+file2 content"))
t.Views().Commits().
Lines(
Contains("first commit").IsSelected(),
)
t.Views().Files().
Focus()
t.Views().Main().
Content(Contains("file1 content"))
},
})

View File

@ -0,0 +1,98 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToIndexPartial = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to the index. This is different from the MoveToIndex test in that we're only selecting a partial patch from a file",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "first line\nsecond line\nthird line\n")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "first line2\nsecond line\nthird line2\n")
shell.Commit("second commit")
shell.CreateFileAndAdd("file2", "file1 content")
shell.Commit("third commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("third commit").IsSelected(),
Contains("second commit"),
Contains("first commit"),
).
NavigateToListItem(Contains("second commit")).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
ContainsLines(
Contains(`-first line`).IsSelected(),
Contains(`+first line2`),
Contains(` second line`),
Contains(`-third line`),
Contains(`+third line2`),
).
PressPrimaryAction().
SelectNextItem().
PressPrimaryAction().
Tap(func() {
t.Views().Information().Content(Contains("building patch"))
t.Views().PatchBuildingSecondary().
ContainsLines(
Contains(`-first line`),
Contains(`+first line2`),
Contains(` second line`),
Contains(` third line`),
)
t.Actions().SelectPatchOption(Contains("move patch out into index"))
t.Views().Files().
Lines(
Contains("M").Contains("file1"),
)
})
// Focus is automatically returned to the commit files panel. Arguably it shouldn't be.
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1"),
)
t.Views().Main().
ContainsLines(
Contains(` first line`),
Contains(` second line`),
Contains(`-third line`),
Contains(`+third line2`),
)
t.Views().Files().
Focus()
t.Views().Main().
ContainsLines(
Contains(`-first line`),
Contains(`+first line2`),
Contains(` second line`),
Contains(` third line2`),
)
},
})

View File

@ -0,0 +1,89 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToIndexWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to the index, causing a conflict",
ExtraCmdArgs: "",
Skip: true, // Skipping until https://github.com/jesseduffield/lazygit/pull/2471 is merged
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "file1 content with old changes")
shell.Commit("second commit")
shell.UpdateFileAndAdd("file1", "file1 content with new changes")
shell.Commit("third commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("third commit").IsSelected(),
Contains("second commit"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Actions().SelectPatchOption(Contains("move patch out into index"))
t.Actions().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
Lines(
Contains("UU").Contains("file1"),
).
PressPrimaryAction()
t.Views().MergeConflicts().
IsFocused().
ContainsLines(
Contains("<<<<<<< HEAD").IsSelected(),
Contains("file1 content").IsSelected(),
Contains("=======").IsSelected(),
Contains("file1 content with new changes"),
Contains(">>>>>>>"),
).
PressPrimaryAction()
t.Actions().ContinueOnConflictsResolved()
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("Applied patch to 'file1' with conflicts")).
Confirm()
t.Views().Files().
IsFocused().
Lines(
Contains("UU").Contains("file1"),
).
PressEnter()
t.Views().MergeConflicts().
TopLines(
Contains("<<<<<<< ours"),
Contains("file1 content"),
Contains("======="),
Contains("file1 content with old changes"),
Contains(">>>>>>> theirs"),
).
IsFocused()
},
})

View File

@ -0,0 +1,70 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToNewCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to a new commit",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "file1 content with old changes")
shell.Commit("second commit")
shell.UpdateFileAndAdd("file1", "file1 content with new changes")
shell.Commit("third commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("third commit").IsSelected(),
Contains("second commit"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Actions().SelectPatchOption(Contains("move patch into new commit"))
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressEscape()
t.Views().Commits().
IsFocused().
Lines(
Contains("third commit"),
Contains(`Split from "second commit"`).IsSelected(),
Contains("second commit"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
// the original commit has no more files in it
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("(none)"),
)
},
})

View File

@ -0,0 +1,57 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RemoveFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Remove a custom patch from a commit",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content\n")
shell.CreateFileAndAdd("file2", "file2 content\n")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("file2"),
).
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Views().PatchBuildingSecondary().Content(Contains("+file1 content"))
t.Actions().SelectPatchOption(Contains("remove patch from original commit"))
t.Views().Files().IsEmpty()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file2").IsSelected(),
).
PressEscape()
t.Views().Main().
Content(Contains("+file2 content"))
t.Views().Commits().
Lines(
Contains("first commit").IsSelected(),
)
},
})

View File

@ -0,0 +1,43 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ResetWithEscape = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Reset a custom patch with the escape keybinding",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressPrimaryAction().
Tap(func() {
t.Views().Information().Content(Contains("building patch"))
}).
PressEscape()
// hitting escape at the top level will reset the patch
t.Views().Commits().
IsFocused().
PressEscape()
t.Views().Information().Content(DoesNotContain("building patch"))
},
})

View File

@ -0,0 +1,42 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SelectAllFiles = NewIntegrationTest(NewIntegrationTestArgs{
Description: "All all files of a commit to a custom patch with the 'a' keybinding",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content")
shell.CreateFileAndAdd("file2", "file2 content")
shell.CreateFileAndAdd("file3", "file3 content")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("file2"),
Contains("file3"),
).
Press(keys.Files.ToggleStagedAll)
t.Views().Information().Content(Contains("building patch"))
t.Views().Secondary().Content(
Contains("file1").Contains("file3").Contains("file3"),
)
},
})

View File

@ -0,0 +1,167 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Build a custom patch with a specific selection of lines, adding individual lines, as well as a range and hunk, and adding a file directly",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("hunk-file", "1a\n1b\n1c\n1d\n1e\n1f\n1g\n1h\n1i\n1j\n1k\n1l\n1m\n1n\n1o\n1p\n1q\n1r\n1s\n1t\n1u\n1v\n1w\n1x\n1y\n1z\n")
shell.Commit("first commit")
// making changes in two separate places for the sake of having two hunks
shell.UpdateFileAndAdd("hunk-file", "aa\n1b\ncc\n1d\n1e\n1f\n1g\n1h\n1i\n1j\n1k\n1l\n1m\n1n\n1o\n1p\n1q\n1r\n1s\ntt\nuu\nvv\n1w\n1x\n1y\n1z\n")
shell.CreateFileAndAdd("line-file", "2a\n2b\n2c\n2d\n2e\n2f\n2g\n2h\n2i\n2j\n2k\n2l\n2m\n2n\n2o\n2p\n2q\n2r\n2s\n2t\n2u\n2v\n2w\n2x\n2y\n2z\n")
shell.CreateFileAndAdd("direct-file", "direct file content")
shell.Commit("second commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("second commit").IsSelected(),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("direct-file").IsSelected(),
Contains("hunk-file"),
Contains("line-file"),
).
PressPrimaryAction().
Tap(func() {
t.Views().Information().Content(Contains("building patch"))
t.Views().Secondary().Content(Contains("direct file content"))
}).
NavigateToListItem(Contains("hunk-file")).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
SelectedLines(
Contains("-1a"),
).
Press(keys.Main.ToggleSelectHunk).
SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(`-1a`),
Contains(`+aa`),
Contains(` 1b`),
Contains(`-1c`),
Contains(`+cc`),
Contains(` 1d`),
Contains(` 1e`),
Contains(` 1f`),
).
PressPrimaryAction().
// unlike in the staging panel, we don't remove lines from the patch building panel
// upon 'adding' them. So the same lines will be selected
SelectedLines(
Contains(`@@ -1,6 +1,6 @@`),
Contains(`-1a`),
Contains(`+aa`),
Contains(` 1b`),
Contains(`-1c`),
Contains(`+cc`),
Contains(` 1d`),
Contains(` 1e`),
Contains(` 1f`),
).
Tap(func() {
t.Views().Information().Content(Contains("building patch"))
t.Views().Secondary().Content(
// when we're inside the patch building panel, we only show the patch
// in the secondary panel that relates to the selected file
DoesNotContain("direct file content").
Contains("@@ -1,6 +1,6 @@").
Contains(" 1f"),
)
}).
PressEscape()
t.Views().CommitFiles().
IsFocused().
NavigateToListItem(Contains("line-file")).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
// hunk is selected because selection mode persists across files
ContainsLines(
Contains("@@ -0,0 +1,26 @@").IsSelected(),
).
Press(keys.Main.ToggleSelectHunk).
SelectedLines(
Contains("+2a"),
).
PressPrimaryAction().
NavigateToListItem(Contains("+2c")).
Press(keys.Main.ToggleDragSelect).
NavigateToListItem(Contains("+2e")).
PressPrimaryAction().
NavigateToListItem(Contains("+2g")).
PressPrimaryAction().
Tap(func() {
t.Views().Information().Content(Contains("building patch"))
t.Views().Secondary().ContainsLines(
Contains("+2a"),
Contains("+2c"),
Contains("+2d"),
Contains("+2e"),
Contains("+2g"),
)
}).
PressEscape().
Tap(func() {
t.Views().Secondary().ContainsLines(
// direct-file patch
Contains(`diff --git a/direct-file b/direct-file`),
Contains(`new file mode 100644`),
Contains(`index`),
Contains(`--- /dev/null`),
Contains(`+++ b/direct-file`),
Contains(`@@ -0,0 +1 @@`),
Contains(`+direct file content`),
Contains(`\ No newline at end of file`),
// hunk-file patch
Contains(`diff --git a/hunk-file b/hunk-file`),
Contains(`index`),
Contains(`--- a/hunk-file`),
Contains(`+++ b/hunk-file`),
Contains(`@@ -1,6 +1,6 @@`),
Contains(`-1a`),
Contains(`+aa`),
Contains(` 1b`),
Contains(`-1c`),
Contains(`+cc`),
Contains(` 1d`),
Contains(` 1e`),
Contains(` 1f`),
// line-file patch
Contains(`diff --git a/line-file b/line-file`),
Contains(`new file mode 100644`),
Contains(`index`),
Contains(`--- /dev/null`),
Contains(`+++ b/line-file`),
Contains(`@@ -0,0 +1,5 @@`),
Contains(`+2a`),
Contains(`+2c`),
Contains(`+2d`),
Contains(`+2e`),
Contains(`+2g`),
)
})
},
})

View File

@ -0,0 +1,62 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var StartNewPatch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Attempt to add a file from another commit to a patch, then agree to start a new patch",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content")
shell.Commit("first commit")
shell.CreateFileAndAdd("file2", "file2 content")
shell.Commit("second commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("second commit").IsSelected(),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file2").IsSelected(),
).
PressPrimaryAction().
Tap(func() {
t.Views().Information().Content(Contains("building patch"))
t.Views().Secondary().Content(Contains("file2"))
}).
PressEscape()
t.Views().Commits().
IsFocused().
NavigateToListItem(Contains("first commit")).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressPrimaryAction().
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Contains("Discard Patch")).
Content(Contains("You can only build a patch from one commit/stash-entry at a time. Discard current patch?")).
Confirm()
t.Views().Secondary().Content(Contains("file1").DoesNotContain("file2"))
})
},
})

View File

@ -94,7 +94,18 @@ var tests = []*components.IntegrationTest{
interactive_rebase.SwapWithConflict,
misc.ConfirmOnQuit,
misc.InitialOpen,
patch_building.Apply,
patch_building.ApplyInReverse,
patch_building.CopyPatchToClipboard,
patch_building.MoveToIndex,
patch_building.MoveToIndexPartial,
patch_building.MoveToIndexWithConflict,
patch_building.MoveToNewCommit,
patch_building.RemoveFromCommit,
patch_building.ResetWithEscape,
patch_building.SelectAllFiles,
patch_building.SpecificSelection,
patch_building.StartNewPatch,
reflog.Checkout,
reflog.CherryPick,
reflog.Patch,

View File

@ -1 +0,0 @@
ref: refs/heads/master

View File

@ -1 +0,0 @@
7a40dadc0814bf7f1418d005eae184848a9f1c94

View File

@ -1,10 +0,0 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
email = CI@example.com
name = CI

View File

@ -1 +0,0 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -1,7 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.DS_Store

View File

@ -1,10 +0,0 @@
0000000000000000000000000000000000000000 01ed22faef05591076721466e07fb10962642887 CI <CI@example.com> 1617671463 +1000 commit (initial): myfile1
01ed22faef05591076721466e07fb10962642887 922fc2ed1965fe8436ce7837c634379f14faf3c3 CI <CI@example.com> 1617671463 +1000 commit: myfile2
922fc2ed1965fe8436ce7837c634379f14faf3c3 7a40dadc0814bf7f1418d005eae184848a9f1c94 CI <CI@example.com> 1617671463 +1000 commit: myfile2 update
7a40dadc0814bf7f1418d005eae184848a9f1c94 2479abfe7bd6b64a753d3c3797f614bbb422f627 CI <CI@example.com> 1617671463 +1000 commit: myfile3
2479abfe7bd6b64a753d3c3797f614bbb422f627 922fc2ed1965fe8436ce7837c634379f14faf3c3 CI <CI@example.com> 1617671469 +1000 rebase -i (start): checkout 922fc2ed1965fe8436ce7837c634379f14faf3c3
922fc2ed1965fe8436ce7837c634379f14faf3c3 7a40dadc0814bf7f1418d005eae184848a9f1c94 CI <CI@example.com> 1617671469 +1000 rebase -i: fast-forward
7a40dadc0814bf7f1418d005eae184848a9f1c94 244cec6fa9704d5dc61fc5e60faba4125dfe3baa CI <CI@example.com> 1617671469 +1000 commit (amend): myfile2 update
244cec6fa9704d5dc61fc5e60faba4125dfe3baa c69e35e8cae5688bbfcf8278c20ab43c1b8dbae3 CI <CI@example.com> 1617671469 +1000 rebase -i (pick): myfile3
c69e35e8cae5688bbfcf8278c20ab43c1b8dbae3 c69e35e8cae5688bbfcf8278c20ab43c1b8dbae3 CI <CI@example.com> 1617671469 +1000 rebase -i (finish): returning to refs/heads/master
c69e35e8cae5688bbfcf8278c20ab43c1b8dbae3 92571130f37c70766612048271f1d4dca63ef0b5 CI <CI@example.com> 1617671472 +1000 commit: test

View File

@ -1,6 +0,0 @@
0000000000000000000000000000000000000000 01ed22faef05591076721466e07fb10962642887 CI <CI@example.com> 1617671463 +1000 commit (initial): myfile1
01ed22faef05591076721466e07fb10962642887 922fc2ed1965fe8436ce7837c634379f14faf3c3 CI <CI@example.com> 1617671463 +1000 commit: myfile2
922fc2ed1965fe8436ce7837c634379f14faf3c3 7a40dadc0814bf7f1418d005eae184848a9f1c94 CI <CI@example.com> 1617671463 +1000 commit: myfile2 update
7a40dadc0814bf7f1418d005eae184848a9f1c94 2479abfe7bd6b64a753d3c3797f614bbb422f627 CI <CI@example.com> 1617671463 +1000 commit: myfile3
2479abfe7bd6b64a753d3c3797f614bbb422f627 c69e35e8cae5688bbfcf8278c20ab43c1b8dbae3 CI <CI@example.com> 1617671469 +1000 rebase -i (finish): refs/heads/master onto 922fc2ed1965fe8436ce7837c634379f14faf3c3
c69e35e8cae5688bbfcf8278c20ab43c1b8dbae3 92571130f37c70766612048271f1d4dca63ef0b5 CI <CI@example.com> 1617671472 +1000 commit: test

View File

@ -1 +0,0 @@
92571130f37c70766612048271f1d4dca63ef0b5

View File

@ -1,3 +0,0 @@
firstline2
secondline
thirdline2

View File

@ -1 +0,0 @@
{"KeyEvents":[{"Timestamp":768,"Mod":0,"Key":259,"Ch":0},{"Timestamp":1016,"Mod":0,"Key":259,"Ch":0},{"Timestamp":1360,"Mod":0,"Key":258,"Ch":0},{"Timestamp":1752,"Mod":0,"Key":13,"Ch":13},{"Timestamp":2256,"Mod":0,"Key":13,"Ch":13},{"Timestamp":2880,"Mod":0,"Key":256,"Ch":32},{"Timestamp":3248,"Mod":0,"Key":258,"Ch":0},{"Timestamp":3416,"Mod":0,"Key":256,"Ch":32},{"Timestamp":4224,"Mod":2,"Key":16,"Ch":16},{"Timestamp":4616,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4840,"Mod":0,"Key":258,"Ch":0},{"Timestamp":5080,"Mod":0,"Key":258,"Ch":0},{"Timestamp":5312,"Mod":0,"Key":258,"Ch":0},{"Timestamp":5592,"Mod":0,"Key":13,"Ch":13},{"Timestamp":6569,"Mod":0,"Key":260,"Ch":0},{"Timestamp":6945,"Mod":0,"Key":260,"Ch":0},{"Timestamp":7561,"Mod":0,"Key":256,"Ch":99},{"Timestamp":7887,"Mod":0,"Key":256,"Ch":116},{"Timestamp":7935,"Mod":0,"Key":256,"Ch":101},{"Timestamp":8152,"Mod":0,"Key":256,"Ch":115},{"Timestamp":8192,"Mod":0,"Key":256,"Ch":116},{"Timestamp":8648,"Mod":0,"Key":13,"Ch":13},{"Timestamp":9400,"Mod":0,"Key":256,"Ch":113}],"ResizeEvents":[{"Timestamp":0,"Width":127,"Height":35}]}

View File

@ -1,26 +0,0 @@
#!/bin/sh
set -e
cd $1
git init
git config user.email "CI@example.com"
git config user.name "CI"
echo test1 > myfile1
git add .
git commit -am "myfile1"
echo firstline > myfile2
echo secondline >> myfile2
echo thirdline >> myfile2
git add .
git commit -am "myfile2"
echo firstline2 > myfile2
echo secondline >> myfile2
echo thirdline2 >> myfile2
git commit -am "myfile2 update"
echo test3 > myfile3
git add .
git commit -am "myfile3"

View File

@ -1 +0,0 @@
{ "description": "", "speed": 7 }

View File

@ -1 +0,0 @@
ref: refs/heads/master

View File

@ -1 +0,0 @@
ad27dd25048bff07da92d2d9d829e4dd75472da4

View File

@ -1,10 +0,0 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
email = CI@example.com
name = CI

View File

@ -1 +0,0 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

@ -1,7 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.DS_Store

View File

@ -1,6 +0,0 @@
0000000000000000000000000000000000000000 9a939087472cfaf305396d4b177ee888ced193d9 CI <CI@example.com> 1617671474 +1000 commit (initial): myfile1
9a939087472cfaf305396d4b177ee888ced193d9 5063202049f1980e035c390732a7e6da8783357f CI <CI@example.com> 1617671474 +1000 commit: myfile2
5063202049f1980e035c390732a7e6da8783357f d0ec73019f9c5e426c9b37fa58757855367580a5 CI <CI@example.com> 1617671474 +1000 commit: myfile2 update
d0ec73019f9c5e426c9b37fa58757855367580a5 ad27dd25048bff07da92d2d9d829e4dd75472da4 CI <CI@example.com> 1617671474 +1000 commit: myfile3
ad27dd25048bff07da92d2d9d829e4dd75472da4 ad27dd25048bff07da92d2d9d829e4dd75472da4 CI <CI@example.com> 1617671483 +1000 reset: moving to HEAD
ad27dd25048bff07da92d2d9d829e4dd75472da4 e474bc2d1712ed5fdf14fb7223392f1b0dcc8d37 CI <CI@example.com> 1617671494 +1000 commit: test

View File

@ -1,5 +0,0 @@
0000000000000000000000000000000000000000 9a939087472cfaf305396d4b177ee888ced193d9 CI <CI@example.com> 1617671474 +1000 commit (initial): myfile1
9a939087472cfaf305396d4b177ee888ced193d9 5063202049f1980e035c390732a7e6da8783357f CI <CI@example.com> 1617671474 +1000 commit: myfile2
5063202049f1980e035c390732a7e6da8783357f d0ec73019f9c5e426c9b37fa58757855367580a5 CI <CI@example.com> 1617671474 +1000 commit: myfile2 update
d0ec73019f9c5e426c9b37fa58757855367580a5 ad27dd25048bff07da92d2d9d829e4dd75472da4 CI <CI@example.com> 1617671474 +1000 commit: myfile3
ad27dd25048bff07da92d2d9d829e4dd75472da4 e474bc2d1712ed5fdf14fb7223392f1b0dcc8d37 CI <CI@example.com> 1617671494 +1000 commit: test

View File

@ -1 +0,0 @@
0000000000000000000000000000000000000000 2c60d208ba3ec966b77ca756237843af7584cf93 CI <CI@example.com> 1617671483 +1000 On master: asd

View File

@ -1,2 +0,0 @@
x�Ï»jA …áÔûê Akn!ƒ+Wy�¤!�Œ×¬Ç�ÇÏ6é§úùš£ë_——¹¹ƒ¹+KUÔ”£DmDV¸x”î�µTJhŠË]6¿M Ù,DäÒzÇlRƒ«VBu6Ë‘s0á?OÒ’¤àV27ªJT©IhH˜2“
ïkyÎÏuƒËÞ/׳ÿȸû«®ã”(§L\Žp D\öºŸ˜þO¾|Ü`Èc÷o û“¯J÷

View File

@ -1,3 +0,0 @@
x�ΝΑ
Β0 €aΟ}�άIfL*�;ν1Ί6ΕΑBeTΠ·w�ΰυηƒ?7χ¥±ϊfh’+&™υj±0E)‘†DUyζs®)_†�ήύΩ6'Έ�ΣΓ>Ι_«�rσ;���+Γ‘1μu�tϋ“�Φe5
?2~,Ο

View File

@ -1 +0,0 @@
e474bc2d1712ed5fdf14fb7223392f1b0dcc8d37

View File

@ -1 +0,0 @@
2c60d208ba3ec966b77ca756237843af7584cf93

View File

@ -1,3 +0,0 @@
firstline
secondline
thirdline2

View File

@ -1 +0,0 @@
{"KeyEvents":[{"Timestamp":539,"Mod":0,"Key":259,"Ch":0},{"Timestamp":692,"Mod":0,"Key":259,"Ch":0},{"Timestamp":963,"Mod":0,"Key":258,"Ch":0},{"Timestamp":1267,"Mod":0,"Key":13,"Ch":13},{"Timestamp":2075,"Mod":0,"Key":256,"Ch":32},{"Timestamp":3027,"Mod":2,"Key":16,"Ch":16},{"Timestamp":3699,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4139,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4299,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4755,"Mod":0,"Key":257,"Ch":0},{"Timestamp":5092,"Mod":0,"Key":13,"Ch":13},{"Timestamp":5666,"Mod":0,"Key":256,"Ch":96},{"Timestamp":6067,"Mod":0,"Key":260,"Ch":0},{"Timestamp":6412,"Mod":0,"Key":260,"Ch":0},{"Timestamp":7699,"Mod":0,"Key":256,"Ch":115},{"Timestamp":8083,"Mod":0,"Key":256,"Ch":97},{"Timestamp":8163,"Mod":0,"Key":256,"Ch":115},{"Timestamp":8282,"Mod":0,"Key":256,"Ch":100},{"Timestamp":8635,"Mod":0,"Key":13,"Ch":13},{"Timestamp":9035,"Mod":0,"Key":259,"Ch":0},{"Timestamp":9244,"Mod":0,"Key":259,"Ch":0},{"Timestamp":9450,"Mod":0,"Key":259,"Ch":0},{"Timestamp":10340,"Mod":0,"Key":13,"Ch":13},{"Timestamp":10787,"Mod":0,"Key":13,"Ch":13},{"Timestamp":12266,"Mod":0,"Key":13,"Ch":13},{"Timestamp":12731,"Mod":0,"Key":256,"Ch":32},{"Timestamp":12979,"Mod":0,"Key":258,"Ch":0},{"Timestamp":13219,"Mod":0,"Key":256,"Ch":32},{"Timestamp":13966,"Mod":0,"Key":27,"Ch":0},{"Timestamp":15315,"Mod":2,"Key":16,"Ch":16},{"Timestamp":16083,"Mod":0,"Key":258,"Ch":0},{"Timestamp":16355,"Mod":0,"Key":258,"Ch":0},{"Timestamp":16683,"Mod":0,"Key":257,"Ch":0},{"Timestamp":17131,"Mod":0,"Key":13,"Ch":13},{"Timestamp":17699,"Mod":0,"Key":260,"Ch":0},{"Timestamp":17947,"Mod":0,"Key":260,"Ch":0},{"Timestamp":18115,"Mod":0,"Key":260,"Ch":0},{"Timestamp":18667,"Mod":0,"Key":256,"Ch":99},{"Timestamp":19051,"Mod":0,"Key":256,"Ch":116},{"Timestamp":19123,"Mod":0,"Key":256,"Ch":101},{"Timestamp":19339,"Mod":0,"Key":256,"Ch":115},{"Timestamp":19379,"Mod":0,"Key":256,"Ch":116},{"Timestamp":19651,"Mod":0,"Key":13,"Ch":13},{"Timestamp":20091,"Mod":0,"Key":256,"Ch":113}],"ResizeEvents":[{"Timestamp":0,"Width":272,"Height":74}]}

Some files were not shown because too many files have changed in this diff Show More