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:
commit
f6fafc65ee
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -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:
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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().
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
64
pkg/integration/tests/patch_building/apply.go
Normal file
64
pkg/integration/tests/patch_building/apply.go
Normal 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"))
|
||||
},
|
||||
})
|
49
pkg/integration/tests/patch_building/apply_in_reverse.go
Normal file
49
pkg/integration/tests/patch_building/apply_in_reverse.go
Normal 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"))
|
||||
},
|
||||
})
|
@ -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"))
|
||||
|
||||
|
66
pkg/integration/tests/patch_building/move_to_index.go
Normal file
66
pkg/integration/tests/patch_building/move_to_index.go
Normal 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"))
|
||||
},
|
||||
})
|
@ -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`),
|
||||
)
|
||||
},
|
||||
})
|
@ -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()
|
||||
},
|
||||
})
|
70
pkg/integration/tests/patch_building/move_to_new_commit.go
Normal file
70
pkg/integration/tests/patch_building/move_to_new_commit.go
Normal 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)"),
|
||||
)
|
||||
},
|
||||
})
|
57
pkg/integration/tests/patch_building/remove_from_commit.go
Normal file
57
pkg/integration/tests/patch_building/remove_from_commit.go
Normal 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(),
|
||||
)
|
||||
},
|
||||
})
|
43
pkg/integration/tests/patch_building/reset_with_escape.go
Normal file
43
pkg/integration/tests/patch_building/reset_with_escape.go
Normal 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"))
|
||||
},
|
||||
})
|
42
pkg/integration/tests/patch_building/select_all_files.go
Normal file
42
pkg/integration/tests/patch_building/select_all_files.go
Normal 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"),
|
||||
)
|
||||
},
|
||||
})
|
167
pkg/integration/tests/patch_building/specific_selection.go
Normal file
167
pkg/integration/tests/patch_building/specific_selection.go
Normal 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`),
|
||||
)
|
||||
})
|
||||
},
|
||||
})
|
62
pkg/integration/tests/patch_building/start_new_patch.go
Normal file
62
pkg/integration/tests/patch_building/start_new_patch.go
Normal 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"))
|
||||
})
|
||||
},
|
||||
})
|
@ -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,
|
||||
|
@ -1 +0,0 @@
|
||||
test
|
@ -1 +0,0 @@
|
||||
ref: refs/heads/master
|
@ -1 +0,0 @@
|
||||
7a40dadc0814bf7f1418d005eae184848a9f1c94
|
@ -1,10 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[user]
|
||||
email = CI@example.com
|
||||
name = CI
|
@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
Binary file not shown.
@ -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
|
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
92571130f37c70766612048271f1d4dca63ef0b5
|
@ -1 +0,0 @@
|
||||
test1
|
@ -1,3 +0,0 @@
|
||||
firstline2
|
||||
secondline
|
||||
thirdline2
|
@ -1 +0,0 @@
|
||||
test3
|
@ -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}]}
|
@ -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"
|
@ -1 +0,0 @@
|
||||
{ "description": "", "speed": 7 }
|
@ -1 +0,0 @@
|
||||
test
|
@ -1 +0,0 @@
|
||||
ref: refs/heads/master
|
@ -1 +0,0 @@
|
||||
ad27dd25048bff07da92d2d9d829e4dd75472da4
|
@ -1,10 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[user]
|
||||
email = CI@example.com
|
||||
name = CI
|
@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
Binary file not shown.
@ -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
|
@ -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
|
@ -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
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 2c60d208ba3ec966b77ca756237843af7584cf93 CI <CI@example.com> 1617671483 +1000 On master: asd
|
Binary file not shown.
Binary file not shown.
@ -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÷
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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~,Ο
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
e474bc2d1712ed5fdf14fb7223392f1b0dcc8d37
|
@ -1 +0,0 @@
|
||||
2c60d208ba3ec966b77ca756237843af7584cf93
|
@ -1 +0,0 @@
|
||||
test1
|
@ -1,3 +0,0 @@
|
||||
firstline
|
||||
secondline
|
||||
thirdline2
|
@ -1 +0,0 @@
|
||||
test3
|
@ -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
Loading…
Reference in New Issue
Block a user