1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-03 13:21:56 +02:00

move code from main into app package to allow test to be injected

This commit is contained in:
Jesse Duffield 2022-08-09 21:27:12 +10:00
parent d890238c7b
commit ba96baee32
68 changed files with 1096 additions and 482 deletions

22
main.go
View File

@ -6,6 +6,8 @@ import (
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/integration"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@ -24,8 +26,9 @@ var (
func main() {
cliArgs := parseCliArgsAndEnvVars()
buildInfo := getBuildInfo()
integrationTest := getIntegrationTest()
app.Start(cliArgs, buildInfo, nil)
app.Start(cliArgs, buildInfo, integrationTest)
}
func parseCliArgsAndEnvVars() *app.CliArgs {
@ -129,3 +132,20 @@ func getBuildInfo() *app.BuildInfo {
return buildInfo
}
func getIntegrationTest() integrationTypes.IntegrationTest {
integrationTestName := os.Getenv("LAZYGIT_TEST_NAME")
if integrationTestName == "" {
return nil
}
// unsetting so that if we run lazygit in as a 'daemon' we don't think we're trying to run a test again
os.Unsetenv("LAZYGIT_TEST_NAME")
for _, candidateTest := range integration.Tests {
if candidateTest.Name() == integrationTestName {
return candidateTest
}
}
panic("Could not find integration test with name: " + integrationTestName)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
@ -22,7 +23,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
)
@ -41,7 +41,7 @@ type App struct {
func Run(
config config.AppConfigurer,
common *common.Common,
startArgs types.StartArgs,
startArgs appTypes.StartArgs,
) {
app, err := NewApp(config, common)
@ -217,7 +217,7 @@ func (app *App) setupRepo() (bool, error) {
return false, nil
}
func (app *App) Run(startArgs types.StartArgs) error {
func (app *App) Run(startArgs appTypes.StartArgs) error {
err := app.Gui.RunAndHandleError(startArgs)
return err
}

View File

@ -10,9 +10,9 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/logs"
"gopkg.in/yaml.v3"
@ -40,12 +40,7 @@ type BuildInfo struct {
BuildSource string
}
// only used when running integration tests
type TestConfig struct {
Test integrationTypes.Test
}
func Start(cliArgs *CliArgs, buildInfo *BuildInfo, test integrationTypes.Test) {
func Start(cliArgs *CliArgs, buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTest) {
if cliArgs.RepoPath != "" {
if cliArgs.WorkTree != "" || cliArgs.GitDir != "" {
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
@ -118,8 +113,8 @@ func Start(cliArgs *CliArgs, buildInfo *BuildInfo, test integrationTypes.Test) {
log.Fatal(err.Error())
}
if test != nil {
test.SetupConfig(appConfig)
if integrationTest != nil {
integrationTest.SetupConfig(appConfig)
}
common, err := NewCommon(appConfig)
@ -134,23 +129,23 @@ func Start(cliArgs *CliArgs, buildInfo *BuildInfo, test integrationTypes.Test) {
parsedGitArg := parseGitArg(cliArgs.GitArg)
Run(appConfig, common, types.NewStartArgs(cliArgs.FilterPath, parsedGitArg, test))
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
}
func parseGitArg(gitArg string) types.GitArg {
typedArg := types.GitArg(gitArg)
func parseGitArg(gitArg string) appTypes.GitArg {
typedArg := appTypes.GitArg(gitArg)
// using switch so that linter catches when a new git arg value is defined but not handled here
switch typedArg {
case types.GitArgNone, types.GitArgStatus, types.GitArgBranch, types.GitArgLog, types.GitArgStash:
case appTypes.GitArgNone, appTypes.GitArgStatus, appTypes.GitArgBranch, appTypes.GitArgLog, appTypes.GitArgStash:
return typedArg
}
permittedValues := []string{
string(types.GitArgStatus),
string(types.GitArgBranch),
string(types.GitArgLog),
string(types.GitArgStash),
string(appTypes.GitArgStatus),
string(appTypes.GitArgBranch),
string(appTypes.GitArgLog),
string(appTypes.GitArgStash),
}
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",

View File

@ -1,4 +1,8 @@
package types
package app
import (
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
// StartArgs is the struct that represents some things we want to do on program start
type StartArgs struct {
@ -7,7 +11,7 @@ type StartArgs struct {
// GitArg determines what context we open in
GitArg GitArg
// integration test (only relevant when invoking lazygit in the context of an integration test)
Test Test
IntegrationTest integrationTypes.IntegrationTest
}
type GitArg string
@ -20,10 +24,10 @@ const (
GitArgStash GitArg = "stash"
)
func NewStartArgs(filterPath string, gitArg GitArg, test Test) StartArgs {
func NewStartArgs(filterPath string, gitArg GitArg, test integrationTypes.IntegrationTest) StartArgs {
return StartArgs{
FilterPath: filterPath,
GitArg: gitArg,
Test: test,
FilterPath: filterPath,
GitArg: gitArg,
IntegrationTest: test,
}
}

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/jesseduffield/gocui"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
@ -31,6 +32,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
@ -213,7 +215,7 @@ const (
COMPLETE
)
func (gui *Gui) onNewRepo(startArgs types.StartArgs, reuseState bool) error {
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
var err error
gui.git, err = commands.NewGitCommand(
gui.Common,
@ -245,7 +247,7 @@ func (gui *Gui) onNewRepo(startArgs types.StartArgs, reuseState bool) error {
// it gets a bit confusing to land back in the status panel when visiting a repo
// you've already switched from. There's no doubt some easy way to make the UX
// optimal for all cases but I'm too lazy to think about what that is right now
func (gui *Gui) resetState(startArgs types.StartArgs, reuseState bool) {
func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) {
currentDir, err := os.Getwd()
if reuseState {
@ -300,28 +302,28 @@ func (gui *Gui) resetState(startArgs types.StartArgs, reuseState bool) {
gui.RepoStateMap[Repo(currentDir)] = gui.State
}
func initialScreenMode(startArgs types.StartArgs) WindowMaximisation {
if startArgs.FilterPath != "" || startArgs.GitArg != types.GitArgNone {
func initialScreenMode(startArgs appTypes.StartArgs) WindowMaximisation {
if startArgs.FilterPath != "" || startArgs.GitArg != appTypes.GitArgNone {
return SCREEN_HALF
} else {
return SCREEN_NORMAL
}
}
func initialContext(contextTree *context.ContextTree, startArgs types.StartArgs) types.IListContext {
func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartArgs) types.IListContext {
var initialContext types.IListContext = contextTree.Files
if startArgs.FilterPath != "" {
initialContext = contextTree.LocalCommits
} else if startArgs.GitArg != types.GitArgNone {
} else if startArgs.GitArg != appTypes.GitArgNone {
switch startArgs.GitArg {
case types.GitArgStatus:
case appTypes.GitArgStatus:
initialContext = contextTree.Files
case types.GitArgBranch:
case appTypes.GitArgBranch:
initialContext = contextTree.Branches
case types.GitArgLog:
case appTypes.GitArgLog:
initialContext = contextTree.LocalCommits
case types.GitArgStash:
case appTypes.GitArgStash:
initialContext = contextTree.Stash
default:
panic("unhandled git arg")
@ -417,7 +419,7 @@ var RuneReplacements = map[rune]string{
graph.CommitSymbol: "o",
}
func (gui *Gui) initGocui(headless bool, test types.Test) (*gocui.Gui, error) {
func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest) (*gocui.Gui, error) {
recordEvents := RecordingEvents()
playMode := gocui.NORMAL
if recordEvents {
@ -476,8 +478,8 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
}
// Run: setup the gui with keybindings and start the mainloop
func (gui *Gui) Run(startArgs types.StartArgs) error {
g, err := gui.initGocui(Headless(), startArgs.Test)
func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
g, err := gui.initGocui(Headless(), startArgs.IntegrationTest)
if err != nil {
return err
}
@ -492,7 +494,7 @@ func (gui *Gui) Run(startArgs types.StartArgs) error {
})
deadlock.Opts.Disable = !gui.Debug
gui.handleTestMode(startArgs.Test)
gui.handleTestMode(startArgs.IntegrationTest)
gui.g.OnSearchEscape = gui.onSearchEscape
if err := gui.Config.ReloadUserConfig(); err != nil {
@ -553,7 +555,7 @@ func (gui *Gui) Run(startArgs types.StartArgs) error {
return gui.g.MainLoop()
}
func (gui *Gui) RunAndHandleError(startArgs types.StartArgs) error {
func (gui *Gui) RunAndHandleError(startArgs appTypes.StartArgs) error {
gui.stopChan = make(chan struct{})
return utils.SafeWithError(func() error {
if err := gui.Run(startArgs); err != nil {

View File

@ -9,17 +9,18 @@ import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
// this gives our integration test a way of interacting with the gui for sending keypresses
// and reading state.
type GuiAdapterImpl struct {
type GuiAdapter struct {
gui *Gui
}
var _ types.GuiAdapter = &GuiAdapterImpl{}
var _ integrationTypes.GuiAdapter = &GuiAdapter{}
func (self *GuiAdapterImpl) PressKey(keyStr string) {
func (self *GuiAdapter) PressKey(keyStr string) {
key := keybindings.GetKey(keyStr)
var r rune
@ -38,19 +39,19 @@ func (self *GuiAdapterImpl) PressKey(keyStr string) {
)
}
func (self *GuiAdapterImpl) Keys() config.KeybindingConfig {
func (self *GuiAdapter) Keys() config.KeybindingConfig {
return self.gui.Config.GetUserConfig().Keybinding
}
func (self *GuiAdapterImpl) CurrentContext() types.Context {
func (self *GuiAdapter) CurrentContext() types.Context {
return self.gui.c.CurrentContext()
}
func (self *GuiAdapterImpl) Model() *types.Model {
func (self *GuiAdapter) Model() *types.Model {
return self.gui.State.Model
}
func (self *GuiAdapterImpl) Fail(message string) {
func (self *GuiAdapter) Fail(message string) {
self.gui.g.Close()
// need to give the gui time to close
time.Sleep(time.Millisecond * 100)
@ -58,15 +59,15 @@ func (self *GuiAdapterImpl) Fail(message string) {
}
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
func (self *GuiAdapterImpl) Log(message string) {
func (self *GuiAdapter) Log(message string) {
self.gui.c.Log.Warn(message)
}
// logs in the actual UI (in the commands panel)
func (self *GuiAdapterImpl) LogUI(message string) {
func (self *GuiAdapter) LogUI(message string) {
self.gui.c.LogAction(message)
}
func (self *GuiAdapterImpl) CheckedOutRef() *models.Branch {
func (self *GuiAdapter) CheckedOutRef() *models.Branch {
return self.gui.helpers.Refs.GetCheckedOutRef()
}

View File

@ -9,6 +9,7 @@ import (
"sync"
"github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
@ -152,7 +153,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
gui.Mutexes.RefreshingFilesMutex.Lock()
defer gui.Mutexes.RefreshingFilesMutex.Unlock()
return gui.onNewRepo(types.StartArgs{}, reuse)
return gui.onNewRepo(appTypes.StartArgs{}, reuse)
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,

View File

@ -9,20 +9,20 @@ import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type IntegrationTest interface {
Run(guiAdapter *GuiAdapterImpl)
Run(guiAdapter *GuiAdapter)
}
func (gui *Gui) handleTestMode(test types.Test) {
func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) {
if test != nil {
go func() {
time.Sleep(time.Millisecond * 100)
test.Run(&GuiAdapterImpl{gui: gui})
test.Run(&GuiAdapter{gui: gui})
gui.g.Update(func(*gocui.Gui) error {
return gocui.ErrQuit

View File

@ -1,27 +0,0 @@
package types
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
)
type Test interface {
Run(GuiAdapter)
SetupConfig(config *config.AppConfig)
}
// this is the interface through which our integration tests interact with the lazygit gui
type GuiAdapter interface {
PressKey(string)
Keys() config.KeybindingConfig
CurrentContext() Context
Model() *Model
Fail(message string)
// These two log methods are for the sake of debugging while testing. There's no need to actually
// commit any logging.
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
Log(message string)
// logs in the actual UI (in the commands panel)
LogUI(message string)
CheckedOutRef() *models.Branch
}

11
pkg/integration/README.md Normal file
View File

@ -0,0 +1,11 @@
# Integration Tests
There's a lot happening in this package so it's worth a proper explanation.
This package is for integration testing: that is, actually running a real lazygit session and having a robot pretend to be a human user and then making assertions that everything works as expected.
There are three ways to invoke a test:
1. go run pkg/integration/runner/main.go commit/new_branch
2. go test pkg/integration/integration_test.go
3.

View File

@ -1,4 +1,4 @@
package integration
package deprecated
import (
"encoding/json"

View File

@ -1,14 +1,18 @@
//go:build !windows
// +build !windows
package gui
package deprecated
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strconv"
"testing"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/creack/pty"
"github.com/stretchr/testify/assert"
)
@ -41,7 +45,7 @@ func TestOld(t *testing.T) {
t.Skip("Skipping integration tests in short mode")
}
mode := integration.GetModeFromEnv()
mode := GetModeFromEnv()
speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
@ -49,10 +53,10 @@ func TestOld(t *testing.T) {
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
testNumber := 0
err := integration.RunTests(
err := RunTests(
t.Logf,
runCmdHeadless,
func(test *integration.Test, f func(*testing.T) error) {
func(test *Test, f func(*testing.T) error) {
defer func() { testNumber += 1 }()
if testNumber%parallelTotal != parallelIndex {
return
@ -74,3 +78,29 @@ func TestOld(t *testing.T) {
assert.NoError(t, err)
}
func tryConvert(numStr string, defaultVal int) int {
num, err := strconv.Atoi(numStr)
if err != nil {
return defaultVal
}
return num
}
func runCmdHeadless(cmd *exec.Cmd) error {
cmd.Env = append(
cmd.Env,
"HEADLESS=true",
"TERM=xterm",
)
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
if err != nil {
return err
}
_, _ = io.Copy(ioutil.Discard, f)
return f.Close()
}

View File

@ -7,11 +7,11 @@ import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
"github.com/stretchr/testify/assert"
)
// Deprecated: This file is part of the old way of doing things. See test/runner_new/main.go for the new way
// Deprecated: This file is part of the old way of doing things. See pkg/integration/runner/main.go for the new way
// see docs/Integration_Tests.md
// This file can be invoked directly, but you might find it easier to go through
@ -22,15 +22,15 @@ import (
// as an env var.
func main() {
mode := integration.GetModeFromEnv()
mode := deprecated.GetModeFromEnv()
speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true"
selectedTestName := os.Args[1]
err := integration.RunTests(
err := deprecated.RunTests(
log.Printf,
runCmdInTerminal,
func(test *integration.Test, f func(*testing.T) error) {
func(test *deprecated.Test, f func(*testing.T) error) {
if selectedTestName != "" && test.Name != selectedTestName {
return
}

View File

@ -10,21 +10,23 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// Deprecated. See lazy_integration for the new approach.
// this program lets you manage integration tests in a TUI. See docs/Integration_Tests.md for more info.
type App struct {
tests []*integration.Test
tests []*deprecated.Test
itemIdx int
testDir string
editing bool
g *gocui.Gui
}
func (app *App) getCurrentTest() *integration.Test {
func (app *App) getCurrentTest() *deprecated.Test {
if len(app.tests) > 0 {
return app.tests[app.itemIdx]
}
@ -49,7 +51,7 @@ func (app *App) refreshTests() {
}
func (app *App) loadTests() {
tests, err := integration.LoadTests(app.testDir)
tests, err := deprecated.LoadTests(app.testDir)
if err != nil {
log.Panicln(err)
}
@ -61,7 +63,7 @@ func (app *App) loadTests() {
}
func main() {
rootDir := integration.GetRootDirectory()
rootDir := deprecated.GetRootDirectory()
testDir := filepath.Join(rootDir, "test", "integration")
app := &App{testDir: testDir}

View File

@ -1,29 +0,0 @@
package integration
import (
"os"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/integration/integration_tests"
"github.com/jesseduffield/lazygit/pkg/integration/types"
)
// NEW integration test format stuff
func IntegrationTestName() string {
return os.Getenv("LAZYGIT_TEST_NAME")
}
func PlayingIntegrationTest() bool {
return IntegrationTestName() != ""
}
func CurrentIntegrationTest() (types.Test, bool) {
if !PlayingIntegrationTest() {
return nil, false
}
return slices.Find(integration_tests.Tests, func(test types.Test) bool {
return test.Name() == IntegrationTestName()
})
}

View File

@ -9,13 +9,17 @@ import (
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
type AssertImpl struct {
gui types.GuiAdapter
// through this struct we assert on the state of the lazygit gui
type Assert struct {
gui integrationTypes.GuiAdapter
}
var _ integrationTypes.Assert = &AssertImpl{}
func NewAssert(gui integrationTypes.GuiAdapter) *Assert {
return &Assert{gui: gui}
}
func (self *AssertImpl) WorkingTreeFileCount(expectedCount int) {
func (self *Assert) WorkingTreeFileCount(expectedCount int) {
self.assertWithRetries(func() (bool, string) {
actualCount := len(self.gui.Model().Files)
@ -26,7 +30,7 @@ func (self *AssertImpl) WorkingTreeFileCount(expectedCount int) {
})
}
func (self *AssertImpl) CommitCount(expectedCount int) {
func (self *Assert) CommitCount(expectedCount int) {
self.assertWithRetries(func() (bool, string) {
actualCount := len(self.gui.Model().Commits)
@ -37,7 +41,7 @@ func (self *AssertImpl) CommitCount(expectedCount int) {
})
}
func (self *AssertImpl) HeadCommitMessage(expectedMessage string) {
func (self *Assert) HeadCommitMessage(expectedMessage string) {
self.assertWithRetries(func() (bool, string) {
if len(self.gui.Model().Commits) == 0 {
return false, "Expected at least one commit to be present"
@ -55,21 +59,21 @@ func (self *AssertImpl) HeadCommitMessage(expectedMessage string) {
})
}
func (self *AssertImpl) CurrentViewName(expectedViewName string) {
func (self *Assert) CurrentViewName(expectedViewName string) {
self.assertWithRetries(func() (bool, string) {
actual := self.gui.CurrentContext().GetView().Name()
return actual == expectedViewName, fmt.Sprintf("Expected current view name to be '%s', but got '%s'", expectedViewName, actual)
})
}
func (self *AssertImpl) CurrentBranchName(expectedViewName string) {
func (self *Assert) CurrentBranchName(expectedViewName string) {
self.assertWithRetries(func() (bool, string) {
actual := self.gui.CheckedOutRef().Name
return actual == expectedViewName, fmt.Sprintf("Expected current branch name to be '%s', but got '%s'", expectedViewName, actual)
})
}
func (self *AssertImpl) InListContext() {
func (self *Assert) InListContext() {
self.assertWithRetries(func() (bool, string) {
currentContext := self.gui.CurrentContext()
_, ok := currentContext.(types.IListContext)
@ -77,14 +81,14 @@ func (self *AssertImpl) InListContext() {
})
}
func (self *AssertImpl) SelectedLineContains(text string) {
func (self *Assert) SelectedLineContains(text string) {
self.assertWithRetries(func() (bool, string) {
line := self.gui.CurrentContext().GetView().SelectedLine()
return strings.Contains(line, text), fmt.Sprintf("Expected selected line to contain '%s', but got '%s'", text, line)
})
}
func (self *AssertImpl) assertWithRetries(test func() (bool, string)) {
func (self *Assert) assertWithRetries(test func() (bool, string)) {
waitTimes := []int{0, 1, 5, 10, 200, 500, 1000}
var message string
@ -101,6 +105,7 @@ func (self *AssertImpl) assertWithRetries(test func() (bool, string)) {
self.Fail(message)
}
func (self *AssertImpl) Fail(message string) {
// for when you just want to fail the test yourself
func (self *Assert) Fail(message string) {
self.gui.Fail(message)
}

View File

@ -10,15 +10,15 @@ import (
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
type InputImpl struct {
gui types.GuiAdapter
type Impl struct {
gui integrationTypes.GuiAdapter
keys config.KeybindingConfig
assert integrationTypes.Assert
assert *Assert
pushKeyDelay int
}
func NewInputImpl(gui types.GuiAdapter, keys config.KeybindingConfig, assert integrationTypes.Assert, pushKeyDelay int) *InputImpl {
return &InputImpl{
func NewInput(gui integrationTypes.GuiAdapter, keys config.KeybindingConfig, assert *Assert, pushKeyDelay int) *Impl {
return &Impl{
gui: gui,
keys: keys,
assert: assert,
@ -26,93 +26,106 @@ func NewInputImpl(gui types.GuiAdapter, keys config.KeybindingConfig, assert int
}
}
var _ integrationTypes.Input = &InputImpl{}
func (self *InputImpl) PressKeys(keyStrs ...string) {
// key is something like 'w' or '<space>'. It's best not to pass a direct value,
// but instead to go through the default user config to get a more meaningful key name
func (self *Impl) PressKeys(keyStrs ...string) {
for _, keyStr := range keyStrs {
self.pressKey(keyStr)
}
}
func (self *InputImpl) pressKey(keyStr string) {
func (self *Impl) pressKey(keyStr string) {
self.Wait(self.pushKeyDelay)
self.gui.PressKey(keyStr)
}
func (self *InputImpl) SwitchToStatusWindow() {
func (self *Impl) SwitchToStatusWindow() {
self.pressKey(self.keys.Universal.JumpToBlock[0])
}
func (self *InputImpl) SwitchToFilesWindow() {
func (self *Impl) SwitchToFilesWindow() {
self.pressKey(self.keys.Universal.JumpToBlock[1])
}
func (self *InputImpl) SwitchToBranchesWindow() {
func (self *Impl) SwitchToBranchesWindow() {
self.pressKey(self.keys.Universal.JumpToBlock[2])
}
func (self *InputImpl) SwitchToCommitsWindow() {
func (self *Impl) SwitchToCommitsWindow() {
self.pressKey(self.keys.Universal.JumpToBlock[3])
}
func (self *InputImpl) SwitchToStashWindow() {
func (self *Impl) SwitchToStashWindow() {
self.pressKey(self.keys.Universal.JumpToBlock[4])
}
func (self *InputImpl) Type(content string) {
func (self *Impl) Type(content string) {
for _, char := range content {
self.pressKey(string(char))
}
}
func (self *InputImpl) Confirm() {
// i.e. pressing enter
func (self *Impl) Confirm() {
self.pressKey(self.keys.Universal.Confirm)
}
func (self *InputImpl) Cancel() {
// i.e. pressing escape
func (self *Impl) Cancel() {
self.pressKey(self.keys.Universal.Return)
}
func (self *InputImpl) Select() {
// i.e. pressing space
func (self *Impl) Select() {
self.pressKey(self.keys.Universal.Select)
}
func (self *InputImpl) NextItem() {
// i.e. pressing down arrow
func (self *Impl) NextItem() {
self.pressKey(self.keys.Universal.NextItem)
}
func (self *InputImpl) PreviousItem() {
// i.e. pressing up arrow
func (self *Impl) PreviousItem() {
self.pressKey(self.keys.Universal.PrevItem)
}
func (self *InputImpl) ContinueMerge() {
func (self *Impl) ContinueMerge() {
self.PressKeys(self.keys.Universal.CreateRebaseOptionsMenu)
self.assert.SelectedLineContains("continue")
self.Confirm()
}
func (self *InputImpl) ContinueRebase() {
func (self *Impl) ContinueRebase() {
self.ContinueMerge()
}
func (self *InputImpl) Wait(milliseconds int) {
// for when you want to allow lazygit to process something before continuing
func (self *Impl) Wait(milliseconds int) {
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
}
func (self *InputImpl) LogUI(message string) {
func (self *Impl) LogUI(message string) {
self.gui.LogUI(message)
}
func (self *InputImpl) Log(message string) {
func (self *Impl) Log(message string) {
self.gui.LogUI(message)
}
// 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 *InputImpl) NavigateToListItemContainingText(text string) {
func (self *Impl) NavigateToListItemContainingText(text string) {
self.assert.InListContext()
currentContext := self.gui.CurrentContext().(types.IListContext)

View File

@ -5,16 +5,20 @@ import (
"io/ioutil"
"os"
"github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/mgutz/str"
)
type ShellImpl struct{}
// this is for running shell commands, mostly for the sake of setting up the repo
// but you can also run the commands from within lazygit to emulate things happening
// in the background.
type Shell struct{}
var _ types.Shell = &ShellImpl{}
func NewShell() *Shell {
return &Shell{}
}
func (s *ShellImpl) RunCommand(cmdStr string) types.Shell {
func (s *Shell) RunCommand(cmdStr string) *Shell {
args := str.ToArgv(cmdStr)
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
@ -27,7 +31,7 @@ func (s *ShellImpl) RunCommand(cmdStr string) types.Shell {
return s
}
func (s *ShellImpl) CreateFile(path string, content string) types.Shell {
func (s *Shell) CreateFile(path string, content string) *Shell {
err := ioutil.WriteFile(path, []byte(content), 0o644)
if err != nil {
panic(fmt.Sprintf("error creating file: %s\n%s", path, err))
@ -36,33 +40,37 @@ func (s *ShellImpl) CreateFile(path string, content string) types.Shell {
return s
}
func (s *ShellImpl) NewBranch(name string) types.Shell {
func (s *Shell) NewBranch(name string) *Shell {
return s.RunCommand("git checkout -b " + name)
}
func (s *ShellImpl) GitAdd(path string) types.Shell {
func (s *Shell) GitAdd(path string) *Shell {
return s.RunCommand(fmt.Sprintf("git add \"%s\"", path))
}
func (s *ShellImpl) GitAddAll() types.Shell {
func (s *Shell) GitAddAll() *Shell {
return s.RunCommand("git add -A")
}
func (s *ShellImpl) Commit(message string) types.Shell {
func (s *Shell) Commit(message string) *Shell {
return s.RunCommand(fmt.Sprintf("git commit -m \"%s\"", message))
}
func (s *ShellImpl) EmptyCommit(message string) types.Shell {
func (s *Shell) EmptyCommit(message string) *Shell {
return s.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message))
}
func (s *ShellImpl) CreateFileAndAdd(fileName string, fileContents string) types.Shell {
// convenience method for creating a file and adding it
func (s *Shell) CreateFileAndAdd(fileName string, fileContents string) *Shell {
return s.
CreateFile(fileName, fileContents).
GitAdd(fileName)
}
func (s *ShellImpl) CreateNCommits(n int) types.Shell {
// creates commits 01, 02, 03, ..., n with a new file in each
// The reason for padding with zeroes is so that it's easier to do string
// matches on the commit messages when there are many of them
func (s *Shell) CreateNCommits(n int) *Shell {
for i := 1; i <= n; i++ {
s.CreateFileAndAdd(
fmt.Sprintf("file%02d.txt", i),

View File

@ -6,37 +6,40 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/config"
guiTypes "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/integration/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type TestImpl struct {
// Test describes an integration tests that will be run against the lazygit gui.
type Test struct {
name string
description string
extraCmdArgs string
skip bool
setupRepo func(shell types.Shell)
setupRepo func(shell *Shell)
setupConfig func(config *config.AppConfig)
run func(
shell types.Shell,
input types.Input,
assert types.Assert,
shell *Shell,
input *Impl,
assert *Assert,
keys config.KeybindingConfig,
)
}
var _ integrationTypes.IntegrationTest = &Test{}
type NewTestArgs struct {
Description string
SetupRepo func(shell types.Shell)
SetupRepo func(shell *Shell)
SetupConfig func(config *config.AppConfig)
Run func(shell types.Shell, input types.Input, assert types.Assert, keys config.KeybindingConfig)
Run func(shell *Shell, input *Impl, assert *Assert, keys config.KeybindingConfig)
ExtraCmdArgs string
Skip bool
}
func NewTest(args NewTestArgs) *TestImpl {
return &TestImpl{
func NewTest(args NewTestArgs) *Test {
return &Test{
name: testNameFromFilePath(),
description: args.Description,
extraCmdArgs: args.ExtraCmdArgs,
@ -47,45 +50,48 @@ func NewTest(args NewTestArgs) *TestImpl {
}
}
var _ types.Test = (*TestImpl)(nil)
func (self *TestImpl) Name() string {
func (self *Test) Name() string {
return self.name
}
func (self *TestImpl) Description() string {
func (self *Test) Description() string {
return self.description
}
func (self *TestImpl) ExtraCmdArgs() string {
func (self *Test) ExtraCmdArgs() string {
return self.extraCmdArgs
}
func (self *TestImpl) Skip() bool {
func (self *Test) Skip() bool {
return self.skip
}
func (self *TestImpl) SetupConfig(config *config.AppConfig) {
func (self *Test) SetupConfig(config *config.AppConfig) {
self.setupConfig(config)
}
func (self *TestImpl) SetupRepo(shell types.Shell) {
func (self *Test) SetupRepo(shell *Shell) {
self.setupRepo(shell)
}
// I want access to all contexts, the model, the ability to press a key, the ability to log,
func (self *TestImpl) Run(gui guiTypes.GuiAdapter) {
shell := &ShellImpl{}
assert := &AssertImpl{gui: gui}
func (self *Test) Run(gui integrationTypes.GuiAdapter) {
shell := NewShell()
assert := NewAssert(gui)
keys := gui.Keys()
input := NewInputImpl(gui, keys, assert, KeyPressDelay())
input := NewInput(gui, keys, assert, KeyPressDelay())
self.run(shell, input, assert, keys)
if KeyPressDelay() > 0 {
// the dev would want to see the final state if they're running in slow mode
input.Wait(2000)
}
}
func testNameFromFilePath() string {
path := utils.FilePath(3)
name := strings.Split(path, "integration/integration_tests/")[1]
name := strings.Split(path, "integration/tests/")[1]
return name[:len(name)-len(".go")]
}

View File

@ -4,26 +4,49 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/integration/integration_tests"
"github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/stretchr/testify/assert"
)
// this is the integration runner for the new and improved integration interface
var Tests = tests.Tests
type Mode int
const (
// Default: if a snapshot test fails, the we'll be asked whether we want to update it
ASK_TO_UPDATE_SNAPSHOT = iota
// fails the test if the snapshots don't match
CHECK_SNAPSHOT
// runs the test and updates the snapshot
UPDATE_SNAPSHOT
// This just makes use of the setup step of the test to get you into
// a lazygit session. Then you'll be able to do whatever you want. Useful
// when you want to test certain things without needing to manually set
// up the situation yourself.
// fails the test if the snapshots don't match
SANDBOX
)
type (
logf func(format string, formatArgs ...interface{})
)
func RunTestsNew(
logf func(format string, formatArgs ...interface{}),
logf logf,
runCmd func(cmd *exec.Cmd) error,
fnWrapper func(test types.Test, f func() error),
fnWrapper func(test *helpers.Test, f func() error),
mode Mode,
onFail func(expected string, actual string, prefix string),
includeSkipped bool,
) error {
rootDir := GetRootDirectory()
@ -40,7 +63,7 @@ func RunTestsNew(
return err
}
for _, test := range integration_tests.Tests {
for _, test := range Tests {
test := test
fnWrapper(test, func() error { //nolint: thelper
@ -66,62 +89,55 @@ func RunTestsNew(
configDir := filepath.Join(testPath, "used_config")
err = runLazygit(test, testPath, rootDir)
cmd, err := getLazygitCommandNew(test, testPath, rootDir)
if err != nil {
return err
}
if mode == UPDATE_SNAPSHOT {
// create/update snapshot
err = oscommands.CopyDir(actualDir, expectedDir)
if err != nil {
return err
}
if err := renameSpecialPaths(expectedDir); err != nil {
return err
}
logf("%s", "updated snapshot")
} else {
if err := validateSameRepos(expectedDir, actualDir); err != nil {
return err
}
// iterate through each repo in the expected dir and comparet to the corresponding repo in the actual dir
expectedFiles, err := ioutil.ReadDir(expectedDir)
if err != nil {
return err
}
for _, f := range expectedFiles {
if !f.IsDir() {
return errors.New("unexpected file (as opposed to directory) in integration test 'expected' directory")
}
// get corresponding file name from actual dir
actualRepoPath := filepath.Join(actualDir, f.Name())
expectedRepoPath := filepath.Join(expectedDir, f.Name())
actualRepo, expectedRepo, err := generateSnapshots(actualRepoPath, expectedRepoPath)
if err != nil {
return err
}
if expectedRepo != actualRepo {
// get the log file and print it
bytes, err := ioutil.ReadFile(filepath.Join(configDir, "development.log"))
if err != nil {
return err
}
logf("%s", string(bytes))
onFail(expectedRepo, actualRepo, f.Name())
}
}
err = runCmd(cmd)
if err != nil {
return err
}
logf("test passed: %s", test.Name())
switch mode {
case UPDATE_SNAPSHOT:
if err := updateSnapshot(logf, actualDir, expectedDir); err != nil {
return err
}
logf("Test passed: %s", test.Name())
case CHECK_SNAPSHOT:
if err := compareSnapshots(logf, configDir, actualDir, expectedDir, test.Name()); err != nil {
return err
}
logf("Test passed: %s", test.Name())
case ASK_TO_UPDATE_SNAPSHOT:
if _, err := os.Stat(expectedDir); os.IsNotExist(err) {
if err := updateSnapshot(logf, actualDir, expectedDir); err != nil {
return err
}
logf("No existing snapshot found for %s. Created snapshot.", test.Name())
return nil
}
if err := compareSnapshots(logf, configDir, actualDir, expectedDir, test.Name()); err != nil {
logf("%s", err)
// prompt user whether to update the snapshot (Y/N)
if promptUserToUpdateSnapshot() {
if err := updateSnapshot(logf, actualDir, expectedDir); err != nil {
return err
}
logf("Snapshot updated: %s", test.Name())
} else {
return err
}
}
logf("Test passed: %s", test.Name())
case SANDBOX:
logf("Session exited")
}
return nil
})
@ -130,12 +146,95 @@ func RunTestsNew(
return nil
}
func createFixtureNew(test types.Test, actualDir string, rootDir string) error {
func promptUserToUpdateSnapshot() bool {
fmt.Println("Test failed. Update snapshot? (y/n)")
var input string
fmt.Scanln(&input)
return input == "y"
}
func updateSnapshot(logf logf, actualDir string, expectedDir string) error {
// create/update snapshot
err := oscommands.CopyDir(actualDir, expectedDir)
if err != nil {
return err
}
if err := renameSpecialPaths(expectedDir); err != nil {
return err
}
return err
}
func compareSnapshots(logf logf, configDir string, actualDir string, expectedDir string, testName string) error {
// there are a couple of reasons we're not generating the snapshot in expectedDir directly:
// Firstly we don't want to have to revert our .git file back to .git_keep.
// Secondly, the act of calling git commands like 'git status' actually changes the index
// for some reason, and we don't want to leave your lazygit working tree dirty as a result.
expectedDirCopy := filepath.Join(os.TempDir(), "expected_dir_test", testName)
err := oscommands.CopyDir(expectedDir, expectedDirCopy)
if err != nil {
return err
}
defer func() {
err := os.RemoveAll(expectedDirCopy)
if err != nil {
panic(err)
}
}()
if err := restoreSpecialPaths(expectedDirCopy); err != nil {
return err
}
err = validateSameRepos(expectedDirCopy, actualDir)
if err != nil {
return err
}
// iterate through each repo in the expected dir and comparet to the corresponding repo in the actual dir
expectedFiles, err := ioutil.ReadDir(expectedDirCopy)
if err != nil {
return err
}
for _, f := range expectedFiles {
if !f.IsDir() {
return errors.New("unexpected file (as opposed to directory) in integration test 'expected' directory")
}
// get corresponding file name from actual dir
actualRepoPath := filepath.Join(actualDir, f.Name())
expectedRepoPath := filepath.Join(expectedDirCopy, f.Name())
actualRepo, expectedRepo, err := generateSnapshots(actualRepoPath, expectedRepoPath)
if err != nil {
return err
}
if expectedRepo != actualRepo {
// get the log file and print it
bytes, err := ioutil.ReadFile(filepath.Join(configDir, "development.log"))
if err != nil {
return err
}
logf("%s", string(bytes))
return errors.New(getDiff(f.Name(), actualRepo, expectedRepo))
}
}
return nil
}
func createFixtureNew(test *helpers.Test, actualDir string, rootDir string) error {
if err := os.Chdir(actualDir); err != nil {
panic(err)
}
shell := &helpers.ShellImpl{}
shell := helpers.NewShell()
shell.RunCommand("git init")
shell.RunCommand(`git config user.email "CI@example.com"`)
shell.RunCommand(`git config user.name "CI"`)
@ -150,7 +249,9 @@ func createFixtureNew(test types.Test, actualDir string, rootDir string) error {
return nil
}
func runLazygit(test types.Test, testPath string, rootDir string) error {
func getLazygitCommandNew(test *helpers.Test, testPath string, rootDir string) (*exec.Cmd, error) {
osCommand := oscommands.NewDummyOSCommand()
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
actualRepoDir := filepath.Join(testPath, "actual", "repo")
@ -158,38 +259,278 @@ func runLazygit(test types.Test, testPath string, rootDir string) error {
err := os.RemoveAll(configDir)
if err != nil {
return err
return nil, err
}
err = oscommands.CopyDir(templateConfigDir, configDir)
if err != nil {
return nil, err
}
cmdStr := fmt.Sprintf("%s -debug --use-config-dir=%s --path=%s %s", tempLazygitPath(), configDir, actualRepoDir, test.ExtraCmdArgs())
cmdObj := osCommand.Cmd.New(cmdStr)
cmdObj.AddEnvVars(fmt.Sprintf("LAZYGIT_TEST_NAME=%s", test.Name()))
return cmdObj.GetCmd(), nil
}
func GetModeFromEnv() Mode {
switch os.Getenv("MODE") {
case "", "ask":
return ASK_TO_UPDATE_SNAPSHOT
case "check":
return CHECK_SNAPSHOT
case "updateSnapshot":
return UPDATE_SNAPSHOT
case "sandbox":
return SANDBOX
default:
log.Fatalf("unknown test mode: %s, must be one of [test, record, updateSnapshot, sandbox]", os.Getenv("MODE"))
panic("unreachable")
}
}
func GetRootDirectory() string {
path, err := os.Getwd()
if err != nil {
panic(err)
}
for {
_, err := os.Stat(filepath.Join(path, ".git"))
if err == nil {
return path
}
if !os.IsNotExist(err) {
panic(err)
}
path = filepath.Dir(path)
if path == "/" {
log.Fatal("must run in lazygit folder or child folder")
}
}
}
func tempLazygitPath() string {
return filepath.Join("/tmp", "lazygit", "test_lazygit")
}
func generateSnapshots(actualDir string, expectedDir string) (string, string, error) {
actual, err := generateSnapshot(actualDir)
if err != nil {
return "", "", err
}
expected, err := generateSnapshot(expectedDir)
if err != nil {
return "", "", err
}
return actual, expected, nil
}
// note that we don't actually store this snapshot in the lazygit repo.
// Instead we store the whole expected git repo of our test, so that
// we can easily change what we want to compare without needing to regenerate
// snapshots for each test.
func generateSnapshot(dir string) (string, error) {
osCommand := oscommands.NewDummyOSCommand()
_, err := os.Stat(filepath.Join(dir, ".git"))
if err != nil {
return "git directory not found", nil
}
snapshot := ""
cmdStrs := []string{
`remote show -n origin`, // remote branches
// TODO: find a way to bring this back without breaking tests
// `ls-remote origin`,
`status`, // file tree
`log --pretty=%B|%an|%ae -p -1`, // log
`tag -n`, // tags
`stash list`, // stash
`submodule foreach 'git status'`, // submodule status
`submodule foreach 'git log --pretty=%B -p -1'`, // submodule log
`submodule foreach 'git tag -n'`, // submodule tags
`submodule foreach 'git stash list'`, // submodule stash
}
for _, cmdStr := range cmdStrs {
// ignoring error for now. If there's an error it could be that there are no results
output, _ := osCommand.Cmd.New(fmt.Sprintf("git -C %s %s", dir, cmdStr)).RunWithOutput()
snapshot += fmt.Sprintf("git %s:\n%s\n", cmdStr, output)
}
snapshot += "files in repo:\n"
err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.IsDir() {
if f.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
bytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}
relativePath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
snapshot += fmt.Sprintf("path: %s\ncontent:\n%s\n", relativePath, string(bytes))
return nil
})
if err != nil {
return "", err
}
return snapshot, nil
}
func getPathsToRename(dir string, needle string, contains string) []string {
pathsToRename := []string{}
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.Name() == needle && (contains == "" || strings.Contains(path, contains)) {
pathsToRename = append(pathsToRename, path)
}
return nil
})
if err != nil {
panic(err)
}
return pathsToRename
}
var specialPathMappings = []struct{ original, new, contains string }{
// git refuses to track .git or .gitmodules in subdirectories so we need to rename them
{".git", ".git_keep", ""},
{".gitmodules", ".gitmodules_keep", ""},
// we also need git to ignore the contents of our test gitignore files so that
// we actually commit files that are ignored within the test.
{".gitignore", "lg_ignore_file", ""},
// this is the .git/info/exclude file. We're being a little more specific here
// so that we don't accidentally mess with some other file named 'exclude' in the test.
{"exclude", "lg_exclude_file", ".git/info/exclude"},
}
func renameSpecialPaths(dir string) error {
for _, specialPath := range specialPathMappings {
for _, path := range getPathsToRename(dir, specialPath.original, specialPath.contains) {
err := os.Rename(path, filepath.Join(filepath.Dir(path), specialPath.new))
if err != nil {
return err
}
}
}
return nil
}
func restoreSpecialPaths(dir string) error {
for _, specialPath := range specialPathMappings {
for _, path := range getPathsToRename(dir, specialPath.new, specialPath.contains) {
err := os.Rename(path, filepath.Join(filepath.Dir(path), specialPath.original))
if err != nil {
return err
}
}
}
return nil
}
// validates that the actual and expected dirs have the same repo names (doesn't actually check the contents of the repos)
func validateSameRepos(expectedDir string, actualDir string) error {
// iterate through each repo in the expected dir and compare to the corresponding repo in the actual dir
expectedFiles, err := ioutil.ReadDir(expectedDir)
if err != nil {
return err
}
// TODO: support test.ExtraCmdArgs in some form.
cliArgs := &app.CliArgs{
Debug: true,
UseConfigDir: configDir,
RepoPath: actualRepoDir,
var actualFiles []os.FileInfo
actualFiles, err = ioutil.ReadDir(actualDir)
if err != nil {
return err
}
buildInfo := &app.BuildInfo{
Commit: "1234abc",
Date: "2020-01-01",
Version: "1.0.0",
BuildSource: "unknown",
expectedFileNames := slices.Map(expectedFiles, getFileName)
actualFileNames := slices.Map(actualFiles, getFileName)
if !slices.Equal(expectedFileNames, actualFileNames) {
return fmt.Errorf("expected and actual repo dirs do not match: expected: %s, actual: %s", expectedFileNames, actualFileNames)
}
return convertPanicToError(func() { app.Start(cliArgs, buildInfo, test) })
}
func convertPanicToError(f func()) (err error) { //nolint: nakedret
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("Lazygit panicked. Stacktrace:: \n" + string(debug.Stack()))
}
}()
f()
return nil
}
func getFileName(f os.FileInfo) string {
return f.Name()
}
func findOrCreateDir(path string) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(path, 0o777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
}
func prepareIntegrationTestDir(actualDir string) {
// remove contents of integration test directory
dir, err := ioutil.ReadDir(actualDir)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(actualDir, 0o777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
for _, d := range dir {
os.RemoveAll(filepath.Join(actualDir, d.Name()))
}
}
func getDiff(prefix string, expected string, actual string) string {
mockT := &MockTestingT{}
assert.Equal(mockT, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
return mockT.err
}
type MockTestingT struct {
err string
}
func (self *MockTestingT) Errorf(format string, args ...interface{}) {
self.err += fmt.Sprintf(format, args...)
}

View File

@ -1,13 +1,12 @@
//go:build !windows
// +build !windows
package gui
package integration
// this is the new way of running tests. See pkg/integration/integration_tests/commit.go
// for an example
import (
"fmt"
"io"
"io/ioutil"
"os"
@ -16,27 +15,26 @@ import (
"testing"
"github.com/creack/pty"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/stretchr/testify/assert"
)
func Test(t *testing.T) {
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
mode := integration.GetModeFromEnv()
mode := GetModeFromEnv()
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
testNumber := 0
err := integration.RunTestsNew(
err := RunTestsNew(
t.Logf,
runCmdHeadless,
func(test types.Test, f func() error) {
func(test *helpers.Test, f func() error) {
defer func() { testNumber += 1 }()
if testNumber%parallelTotal != parallelIndex {
return
@ -48,9 +46,6 @@ func Test(t *testing.T) {
})
},
mode,
func(expected string, actual string, prefix string) {
assert.Equal(t, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
},
includeSkipped,
)

View File

@ -1,19 +0,0 @@
package integration_tests
import (
"github.com/jesseduffield/lazygit/pkg/integration/integration_tests/branch"
"github.com/jesseduffield/lazygit/pkg/integration/integration_tests/commit"
"github.com/jesseduffield/lazygit/pkg/integration/integration_tests/interactive_rebase"
"github.com/jesseduffield/lazygit/pkg/integration/types"
)
// Here is where we lists the actual tests that will run. When you create a new test,
// be sure to add it to this list.
var Tests = []types.Test{
commit.Commit,
commit.NewBranch,
branch.Suggestions,
interactive_rebase.One,
}

View File

@ -1,15 +1,12 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/integration/integration_tests"
"github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/stretchr/testify/assert"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
)
// see docs/Integration_Tests.md
@ -28,7 +25,7 @@ func main() {
// check if our given test name actually exists
if selectedTestName != "" {
found := false
for _, test := range integration_tests.Tests {
for _, test := range integration.Tests {
if test.Name() == selectedTestName {
found = true
break
@ -42,7 +39,7 @@ func main() {
err := integration.RunTestsNew(
log.Printf,
runCmdInTerminal,
func(test types.Test, f func() error) {
func(test *helpers.Test, f func() error) {
if selectedTestName != "" && test.Name() != selectedTestName {
return
}
@ -51,9 +48,6 @@ func main() {
}
},
mode,
func(expected string, actual string, prefix string) { //nolint:thelper
assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
},
includeSkipped,
)
if err != nil {
@ -61,12 +55,6 @@ func main() {
}
}
type MockTestingT struct{}
func (t MockTestingT) Errorf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
func runCmdInTerminal(cmd *exec.Cmd) error {
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin

View File

@ -3,7 +3,6 @@ package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/integration/types"
)
var Suggestions = helpers.NewTest(helpers.NewTestArgs{
@ -11,7 +10,7 @@ var Suggestions = helpers.NewTest(helpers.NewTestArgs{
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell types.Shell) {
SetupRepo: func(shell *helpers.Shell) {
shell.
EmptyCommit("my commit message").
NewBranch("new-branch").
@ -21,7 +20,7 @@ var Suggestions = helpers.NewTest(helpers.NewTestArgs{
NewBranch("other-new-branch-2").
NewBranch("other-new-branch-3")
},
Run: func(shell types.Shell, input types.Input, assert types.Assert, keys config.KeybindingConfig) {
Run: func(shell *helpers.Shell, input *helpers.Impl, assert *helpers.Assert, keys config.KeybindingConfig) {
input.SwitchToBranchesWindow()
input.PressKeys(keys.Branches.CheckoutBranchByName)

View File

@ -3,7 +3,6 @@ package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/integration/types"
)
var Commit = helpers.NewTest(helpers.NewTestArgs{
@ -11,11 +10,11 @@ var Commit = helpers.NewTest(helpers.NewTestArgs{
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell types.Shell) {
SetupRepo: func(shell *helpers.Shell) {
shell.CreateFile("myfile", "myfile content")
shell.CreateFile("myfile2", "myfile2 content")
},
Run: func(shell types.Shell, input types.Input, assert types.Assert, keys config.KeybindingConfig) {
Run: func(shell *helpers.Shell, input *helpers.Impl, assert *helpers.Assert, keys config.KeybindingConfig) {
assert.CommitCount(0)
input.Select()

View File

@ -3,7 +3,6 @@ package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/integration/types"
)
var NewBranch = helpers.NewTest(helpers.NewTestArgs{
@ -11,13 +10,13 @@ var NewBranch = helpers.NewTest(helpers.NewTestArgs{
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell types.Shell) {
SetupRepo: func(shell *helpers.Shell) {
shell.
EmptyCommit("commit 1").
EmptyCommit("commit 2").
EmptyCommit("commit 3")
},
Run: func(shell types.Shell, input types.Input, assert types.Assert, keys config.KeybindingConfig) {
Run: func(shell *helpers.Shell, input *helpers.Impl, assert *helpers.Assert, keys config.KeybindingConfig) {
assert.CommitCount(3)
input.SwitchToCommitsWindow()

View File

@ -3,7 +3,6 @@ package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/integration/types"
)
var One = helpers.NewTest(helpers.NewTestArgs{
@ -11,11 +10,11 @@ var One = helpers.NewTest(helpers.NewTestArgs{
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell types.Shell) {
SetupRepo: func(shell *helpers.Shell) {
shell.
CreateNCommits(5) // these will appears at commit 05, 04, 04, down to 01
},
Run: func(shell types.Shell, input types.Input, assert types.Assert, keys config.KeybindingConfig) {
Run: func(shell *helpers.Shell, input *helpers.Impl, assert *helpers.Assert, keys config.KeybindingConfig) {
input.SwitchToCommitsWindow()
assert.CurrentViewName("commits")

View File

@ -0,0 +1,18 @@
package tests
import (
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/integration/tests/branch"
"github.com/jesseduffield/lazygit/pkg/integration/tests/commit"
"github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase"
)
// Here is where we lists the actual tests that will run. When you create a new test,
// be sure to add it to this list.
var Tests = []*helpers.Test{
commit.Commit,
commit.NewBranch,
branch.Suggestions,
interactive_rebase.One,
}

323
pkg/integration/tui/main.go Normal file
View File

@ -0,0 +1,323 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/integration/helpers"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// this program lets you manage integration tests in a TUI. See docs/Integration_Tests.md for more info.
type App struct {
tests []*helpers.Test
itemIdx int
testDir string
filtering bool
g *gocui.Gui
}
func (app *App) getCurrentTest() *helpers.Test {
if len(app.tests) > 0 {
return app.tests[app.itemIdx]
}
return nil
}
func (app *App) refreshTests() {
app.loadTests()
app.g.Update(func(*gocui.Gui) error {
listView, err := app.g.View("list")
if err != nil {
return err
}
listView.Clear()
for _, test := range app.tests {
fmt.Fprintln(listView, test.Name())
}
return nil
})
}
func (app *App) loadTests() {
app.tests = integration.Tests
if app.itemIdx > len(app.tests)-1 {
app.itemIdx = len(app.tests) - 1
}
}
func main() {
rootDir := integration.GetRootDirectory()
testDir := filepath.Join(rootDir, "test", "integration")
app := &App{testDir: testDir}
app.loadTests()
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL, false, gui.RuneReplacements)
if err != nil {
log.Panicln(err)
}
g.Cursor = false
app.g = g
g.SetManagerFunc(app.layout)
if err := g.SetKeybinding("list", gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
if app.itemIdx > 0 {
app.itemIdx--
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'q', gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 's', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=sandbox go run pkg/integration/runner/main.go %s", currentTest.Name()))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true go run pkg/integration/runner/main.go %s", currentTest.Name()))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 't', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true KEY_PRESS_DELAY=200 go run pkg/integration/runner/main.go %s", currentTest.Name()))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'o', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code -r pkg/integration/tests/%s", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'O', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code test/integration_new/%s", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", '/', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
app.filtering = true
if _, err := g.SetCurrentView("editor"); err != nil {
return err
}
editorView, err := g.View("editor")
if err != nil {
return err
}
editorView.Clear()
return nil
}); err != nil {
log.Panicln(err)
}
// not using the editor yet, but will use it to help filter the list
if err := g.SetKeybinding("editor", gocui.KeyEsc, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
app.filtering = false
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
err = g.MainLoop()
g.Close()
switch err {
case gocui.ErrQuit:
return
default:
log.Panicln(err)
}
}
func (app *App) runSubprocess(cmd *exec.Cmd) {
if err := gocui.Screen.Suspend(); err != nil {
panic(err)
}
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Println(err.Error())
}
cmd.Stdin = nil
cmd.Stderr = nil
cmd.Stdout = nil
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint("press enter to return"))
fmt.Scanln() // wait for enter press
if err := gocui.Screen.Resume(); err != nil {
panic(err)
}
}
func (app *App) layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
descriptionViewHeight := 7
keybindingsViewHeight := 3
editorViewHeight := 3
if !app.filtering {
editorViewHeight = 0
} else {
descriptionViewHeight = 0
keybindingsViewHeight = 0
}
g.Cursor = app.filtering
g.FgColor = gocui.ColorGreen
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
listView.Highlight = true
listView.Clear()
for _, test := range app.tests {
fmt.Fprintln(listView, test.Name())
}
listView.Title = "Tests"
listView.FgColor = gocui.ColorDefault
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
}
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
if err != nil {
if err.Error() != "unknown view" {
return err
}
descriptionView.Title = "Test description"
descriptionView.Wrap = true
descriptionView.FgColor = gocui.ColorDefault
}
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
if err != nil {
if err.Error() != "unknown view" {
return err
}
keybindingsView.Title = "Keybindings"
keybindingsView.Wrap = true
keybindingsView.FgColor = gocui.ColorDefault
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
}
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
if err != nil {
if err.Error() != "unknown view" {
return err
}
editorView.Title = "Filter"
editorView.FgColor = gocui.ColorDefault
editorView.Editable = true
}
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
descriptionView.Clear()
fmt.Fprint(descriptionView, currentTest.Description())
if err := g.SetKeybinding("list", gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
if app.itemIdx < len(app.tests)-1 {
app.itemIdx++
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
return nil
}); err != nil {
log.Panicln(err)
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}

View File

@ -1,99 +1,31 @@
package types
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// TODO: refactor this so that we don't have code spread around so much. We want
// our TestImpl struct to take the dependencies it needs from the gui and then
// create the input, assert, shell structs itself. That way, we can potentially
// ditch these interfaces so that we don't need to keep updating them every time
// we add a method to the concrete struct.
// these interfaces are used by the gui package so that it knows what it needs
// to provide to a test in order for the test to run.
type Test interface {
Name() string
Description() string
// this is called before lazygit is run, for the sake of preparing the repo
SetupRepo(Shell)
// this gives you the default config and lets you set whatever values on it you like,
// so that they appear when lazygit runs
type IntegrationTest interface {
Run(GuiAdapter)
SetupConfig(config *config.AppConfig)
// this is called upon lazygit starting
Run(types.GuiAdapter)
// e.g. '-debug'
ExtraCmdArgs() string
// for tests that are flakey and when we don't have time to fix them
Skip() bool
}
// this is for running shell commands, mostly for the sake of setting up the repo
// but you can also run the commands from within lazygit to emulate things happening
// in the background.
// Implementation is at pkg/integration/shell.go
type Shell interface {
RunCommand(command string) Shell
CreateFile(name string, content string) Shell
NewBranch(branchName string) Shell
GitAdd(path string) Shell
GitAddAll() Shell
Commit(message string) Shell
EmptyCommit(message string) Shell
// convenience method for creating a file and adding it
CreateFileAndAdd(fileName string, fileContents string) Shell
// creates commits 01, 02, 03, ..., n with a new file in each
// The reason for padding with zeroes is so that it's easier to do string
// matches on the commit messages when there are many of them
CreateNCommits(n int) Shell
}
// through this interface our test interacts with the lazygit gui
// Implementation is at pkg/gui/input.go
type Input interface {
// key is something like 'w' or '<space>'. It's best not to pass a direct value,
// but instead to go through the default user config to get a more meaningful key name
PressKeys(keys ...string)
// for typing into a popup prompt
Type(content string)
// for when you want to allow lazygit to process something before continuing
Wait(milliseconds int)
// going straight to a particular side window
SwitchToStatusWindow()
SwitchToFilesWindow()
SwitchToBranchesWindow()
SwitchToCommitsWindow()
SwitchToStashWindow()
// i.e. pressing enter
Confirm()
// i.e. pressing escape
Cancel()
// i.e. pressing space
Select()
// i.e. pressing down arrow
NextItem()
// i.e. pressing up arrow
PreviousItem()
// 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
NavigateToListItemContainingText(text string)
ContinueRebase()
ContinueMerge()
}
// through this interface we assert on the state of the lazygit gui
// implementation is at pkg/gui/assert.go
type Assert interface {
WorkingTreeFileCount(int)
CommitCount(int)
HeadCommitMessage(string)
CurrentViewName(expectedViewName string)
CurrentBranchName(expectedBranchName string)
InListContext()
SelectedLineContains(text string)
// for when you just want to fail the test yourself
Fail(errorMessage string)
// this is the interface through which our integration tests interact with the lazygit gui
type GuiAdapter interface {
PressKey(string)
Keys() config.KeybindingConfig
CurrentContext() types.Context
Model() *types.Model
Fail(message string)
// These two log methods are for the sake of debugging while testing. There's no need to actually
// commit any logging.
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
Log(message string)
// logs in the actual UI (in the commands panel)
LogUI(message string)
CheckedOutRef() *models.Branch
}

View File

@ -1 +1 @@
0000000000000000000000000000000000000000 460150760ff1f381c3f5769b919cb73107c5871a CI <CI@example.com> 1659863059 +1000 commit (initial): my commit message
0000000000000000000000000000000000000000 944b9ea58bef8f6352c3a081a1d0037125bcaabc CI <CI@example.com> 1660133266 +1000 commit (initial): my commit message

View File

@ -1 +1 @@
0000000000000000000000000000000000000000 460150760ff1f381c3f5769b919cb73107c5871a CI <CI@example.com> 1659863059 +1000 commit (initial): my commit message
0000000000000000000000000000000000000000 944b9ea58bef8f6352c3a081a1d0037125bcaabc CI <CI@example.com> 1660133266 +1000 commit (initial): my commit message

View File

@ -1 +1 @@
460150760ff1f381c3f5769b919cb73107c5871a
944b9ea58bef8f6352c3a081a1d0037125bcaabc

View File

@ -1,4 +1,4 @@
0000000000000000000000000000000000000000 470038e1336649b2965305f9f6a82501a836810e CI <CI@example.com> 1659865912 +1000 commit (initial): commit 1
470038e1336649b2965305f9f6a82501a836810e c8bec8f2b323cbb476e708bd10c145ea7cc9f726 CI <CI@example.com> 1659865912 +1000 commit: commit 2
c8bec8f2b323cbb476e708bd10c145ea7cc9f726 62a60693a2e154e745ee353f67a05156d0532c23 CI <CI@example.com> 1659865912 +1000 commit: commit 3
62a60693a2e154e745ee353f67a05156d0532c23 c8bec8f2b323cbb476e708bd10c145ea7cc9f726 CI <CI@example.com> 1659865912 +1000 checkout: moving from master to my-branch-name
0000000000000000000000000000000000000000 4e72cd440eec154569568bff8d4c955052ae246c CI <CI@example.com> 1660125381 +1000 commit (initial): commit 1
4e72cd440eec154569568bff8d4c955052ae246c 563414ba32c967cfbe21a17fe892d6118c1c58e8 CI <CI@example.com> 1660125381 +1000 commit: commit 2
563414ba32c967cfbe21a17fe892d6118c1c58e8 0af36e404e6fec1c3a4d887e30622238e5ea0b2b CI <CI@example.com> 1660125381 +1000 commit: commit 3
0af36e404e6fec1c3a4d887e30622238e5ea0b2b 563414ba32c967cfbe21a17fe892d6118c1c58e8 CI <CI@example.com> 1660125382 +1000 checkout: moving from master to my-branch-name

View File

@ -1,3 +1,3 @@
0000000000000000000000000000000000000000 470038e1336649b2965305f9f6a82501a836810e CI <CI@example.com> 1659865912 +1000 commit (initial): commit 1
470038e1336649b2965305f9f6a82501a836810e c8bec8f2b323cbb476e708bd10c145ea7cc9f726 CI <CI@example.com> 1659865912 +1000 commit: commit 2
c8bec8f2b323cbb476e708bd10c145ea7cc9f726 62a60693a2e154e745ee353f67a05156d0532c23 CI <CI@example.com> 1659865912 +1000 commit: commit 3
0000000000000000000000000000000000000000 4e72cd440eec154569568bff8d4c955052ae246c CI <CI@example.com> 1660125381 +1000 commit (initial): commit 1
4e72cd440eec154569568bff8d4c955052ae246c 563414ba32c967cfbe21a17fe892d6118c1c58e8 CI <CI@example.com> 1660125381 +1000 commit: commit 2
563414ba32c967cfbe21a17fe892d6118c1c58e8 0af36e404e6fec1c3a4d887e30622238e5ea0b2b CI <CI@example.com> 1660125381 +1000 commit: commit 3

View File

@ -1 +1 @@
0000000000000000000000000000000000000000 c8bec8f2b323cbb476e708bd10c145ea7cc9f726 CI <CI@example.com> 1659865912 +1000 branch: Created from c8bec8f2b323cbb476e708bd10c145ea7cc9f726
0000000000000000000000000000000000000000 563414ba32c967cfbe21a17fe892d6118c1c58e8 CI <CI@example.com> 1660125382 +1000 branch: Created from 563414ba32c967cfbe21a17fe892d6118c1c58e8

View File

@ -0,0 +1,2 @@
xŤÎA
1 @Q×=Eö‚¤1‰-�®ćm&�‚u†ˇ‚ÇwŔíç-ľÍ­=:Ä,»ľş×D2š2YUŻą ˘ ×)Ťš)Ą©şsFKYýŐ�ýD62Ł»EaŃ,šę´q¶,‚BʼnŐBy÷űĽÂm€óm¸ú§´ĺé›Ű˘*F’cŠ°Ź�¶şMu˙“˙<Př)R:p

View File

@ -1,2 +0,0 @@
x�ŽK
1]罤ók»AD˜Õ#ÉtP0Î0Dðøfá\ÔæQ<ª¬­=:X‰‡¾«BÈìâR(¸’I³$$Ôrå…Ä1׬ƒÙÒ®¯áŒèY­÷DA²Šc•Ji¡Mì‰-ªIï~_w˜f¸LóM?©mO=•µ]ÁRXG‹ˆf¬#ªëŸúÏg¾…_9>

View File

@ -1 +1 @@
62a60693a2e154e745ee353f67a05156d0532c23
0af36e404e6fec1c3a4d887e30622238e5ea0b2b

View File

@ -1 +1 @@
c8bec8f2b323cbb476e708bd10c145ea7cc9f726
563414ba32c967cfbe21a17fe892d6118c1c58e8

View File

@ -14,14 +14,14 @@ commit 05
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Mon Aug 8 21:32:34 2022 +1000
# Date: Wed Aug 10 19:26:28 2022 +1000
#
# interactive rebase in progress; onto a1a6f7b
# interactive rebase in progress; onto cc9defb
# Last commands done (4 commands done):
# drop 84b1ae9 commit 04
# squash aa2585a commit 05
# drop da71be1 commit 04
# squash 8a38398 commit 05
# No commands remaining.
# You are currently rebasing branch 'master' on 'a1a6f7b'.
# You are currently rebasing branch 'master' on 'cc9defb'.
#
# Changes to be committed:
# new file: file02.txt

View File

@ -1 +1 @@
aa2585aff7d2278341ca816f187e623503d7c4fb
8a3839811a7a9f4c678090c9def892d1e7ad7e54

View File

@ -1 +1 @@
aa2585aff7d2278341ca816f187e623503d7c4fb
8a3839811a7a9f4c678090c9def892d1e7ad7e54

View File

@ -1,10 +1,10 @@
0000000000000000000000000000000000000000 a1a6f7bda6aeaa08ec75f590845780fde90d901c CI <CI@example.com> 1659958354 +1000 commit (initial): commit 01
a1a6f7bda6aeaa08ec75f590845780fde90d901c cb7e56856ecee89fa44c613e094fcf962fe18cf1 CI <CI@example.com> 1659958354 +1000 commit: commit 02
cb7e56856ecee89fa44c613e094fcf962fe18cf1 6741ab4fe22a3d36b6c64397fc4295dbae1ba71d CI <CI@example.com> 1659958354 +1000 commit: commit 03
6741ab4fe22a3d36b6c64397fc4295dbae1ba71d 84b1ae9d83049341897c9388afffdc9049c3317f CI <CI@example.com> 1659958354 +1000 commit: commit 04
84b1ae9d83049341897c9388afffdc9049c3317f aa2585aff7d2278341ca816f187e623503d7c4fb CI <CI@example.com> 1659958354 +1000 commit: commit 05
aa2585aff7d2278341ca816f187e623503d7c4fb a1a6f7bda6aeaa08ec75f590845780fde90d901c CI <CI@example.com> 1659958355 +1000 rebase (start): checkout a1a6f7bda6aeaa08ec75f590845780fde90d901c
a1a6f7bda6aeaa08ec75f590845780fde90d901c cb7e56856ecee89fa44c613e094fcf962fe18cf1 CI <CI@example.com> 1659958355 +1000 rebase: fast-forward
cb7e56856ecee89fa44c613e094fcf962fe18cf1 9c68b57ac7b652fbebc5e93a9a1b72014400c269 CI <CI@example.com> 1659958355 +1000 rebase (continue) (fixup): # This is a combination of 2 commits.
9c68b57ac7b652fbebc5e93a9a1b72014400c269 f4316f7a6df3fe5b7e8da1b2c8767ed1e825dc05 CI <CI@example.com> 1659958355 +1000 rebase (continue) (squash): commit 02
f4316f7a6df3fe5b7e8da1b2c8767ed1e825dc05 f4316f7a6df3fe5b7e8da1b2c8767ed1e825dc05 CI <CI@example.com> 1659958355 +1000 rebase (continue) (finish): returning to refs/heads/master
0000000000000000000000000000000000000000 cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347 CI <CI@example.com> 1660123588 +1000 commit (initial): commit 01
cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347 2e2cd25ffdec58d32b5d549f8402bd054e22cc2a CI <CI@example.com> 1660123588 +1000 commit: commit 02
2e2cd25ffdec58d32b5d549f8402bd054e22cc2a 90fda12ce101e7d0d4594a879e5bbd1be3c857a8 CI <CI@example.com> 1660123588 +1000 commit: commit 03
90fda12ce101e7d0d4594a879e5bbd1be3c857a8 da71be1afbb03f46e91ab5de17d69f148bb009f3 CI <CI@example.com> 1660123588 +1000 commit: commit 04
da71be1afbb03f46e91ab5de17d69f148bb009f3 8a3839811a7a9f4c678090c9def892d1e7ad7e54 CI <CI@example.com> 1660123589 +1000 commit: commit 05
8a3839811a7a9f4c678090c9def892d1e7ad7e54 cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347 CI <CI@example.com> 1660123589 +1000 rebase (start): checkout cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347
cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347 2e2cd25ffdec58d32b5d549f8402bd054e22cc2a CI <CI@example.com> 1660123589 +1000 rebase: fast-forward
2e2cd25ffdec58d32b5d549f8402bd054e22cc2a b85535ebf12659044c33386376121d76756ceb59 CI <CI@example.com> 1660123590 +1000 rebase (continue) (fixup): # This is a combination of 2 commits.
b85535ebf12659044c33386376121d76756ceb59 aba3469fd6fc584a6af9c0073873005ffaaea56c CI <CI@example.com> 1660123590 +1000 rebase (continue) (squash): commit 02
aba3469fd6fc584a6af9c0073873005ffaaea56c aba3469fd6fc584a6af9c0073873005ffaaea56c CI <CI@example.com> 1660123590 +1000 rebase (continue) (finish): returning to refs/heads/master

View File

@ -1,6 +1,6 @@
0000000000000000000000000000000000000000 a1a6f7bda6aeaa08ec75f590845780fde90d901c CI <CI@example.com> 1659958354 +1000 commit (initial): commit 01
a1a6f7bda6aeaa08ec75f590845780fde90d901c cb7e56856ecee89fa44c613e094fcf962fe18cf1 CI <CI@example.com> 1659958354 +1000 commit: commit 02
cb7e56856ecee89fa44c613e094fcf962fe18cf1 6741ab4fe22a3d36b6c64397fc4295dbae1ba71d CI <CI@example.com> 1659958354 +1000 commit: commit 03
6741ab4fe22a3d36b6c64397fc4295dbae1ba71d 84b1ae9d83049341897c9388afffdc9049c3317f CI <CI@example.com> 1659958354 +1000 commit: commit 04
84b1ae9d83049341897c9388afffdc9049c3317f aa2585aff7d2278341ca816f187e623503d7c4fb CI <CI@example.com> 1659958354 +1000 commit: commit 05
aa2585aff7d2278341ca816f187e623503d7c4fb f4316f7a6df3fe5b7e8da1b2c8767ed1e825dc05 CI <CI@example.com> 1659958355 +1000 rebase (continue) (finish): refs/heads/master onto a1a6f7bda6aeaa08ec75f590845780fde90d901c
0000000000000000000000000000000000000000 cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347 CI <CI@example.com> 1660123588 +1000 commit (initial): commit 01
cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347 2e2cd25ffdec58d32b5d549f8402bd054e22cc2a CI <CI@example.com> 1660123588 +1000 commit: commit 02
2e2cd25ffdec58d32b5d549f8402bd054e22cc2a 90fda12ce101e7d0d4594a879e5bbd1be3c857a8 CI <CI@example.com> 1660123588 +1000 commit: commit 03
90fda12ce101e7d0d4594a879e5bbd1be3c857a8 da71be1afbb03f46e91ab5de17d69f148bb009f3 CI <CI@example.com> 1660123588 +1000 commit: commit 04
da71be1afbb03f46e91ab5de17d69f148bb009f3 8a3839811a7a9f4c678090c9def892d1e7ad7e54 CI <CI@example.com> 1660123589 +1000 commit: commit 05
8a3839811a7a9f4c678090c9def892d1e7ad7e54 aba3469fd6fc584a6af9c0073873005ffaaea56c CI <CI@example.com> 1660123590 +1000 rebase (continue) (finish): refs/heads/master onto cc9defb8ae9134f1a9a6c28a0006dc8c8cd78347

View File

@ -0,0 +1,3 @@
x█нA
б0Pв9еЛи$1≥─┬пU▐1≥LQ0╤■ъ,<─ЭщГ}Ь╡╤ЖХ─9З╝
l²└Бc┴T≈d ЯЛ9Y░╧≈T\п≤▀ыxвW▒\u)д я┤║2GqджзX┘Fj"▓Аw©╞;L3\╕Ы╕nшSO╡╤+`▄²?агп▄v°ЙЗ'Ъy╟н|л╠;u

View File

@ -1,3 +0,0 @@
x�ÎM
Â0†a×9Åì™ÉŸºê1’ô
Ö–ÁãÛ…pûò,ÞºÌó£“h<ô Ä©H ©ÚRòä…½æ„æTØEÆÞ¬Àš5oxuªåŒSˆ¨@Ò–½¯QX}«M£m�T›˜üî÷e£a¤Ë0ÞðÉóúÄ©.ó•$Õ�\ðtf6{ݧ:þä?OìÌ�;ß

View File

@ -1,3 +0,0 @@
xŤÎM
Â0@a×9ĹěI2?m@DčĘcL’
Ö–ÁăŰ…pűřŻ,óüč’úfČU…[LąřLŁęčsEő±´&K#dnÝŞ›˝:Č@A35‹Q±˘d)B�†V(&®Y-dBuúî÷e�éçévµŹÎëÓNe™/„Sâ™ŕĽ÷nŻűT·?ůĎ�'÷Ěu;Ť

View File

@ -1,2 +0,0 @@
x…ŽÁJÄ0†=ç)z–IÚ´‰Č˛°§˝ű“fbĂ6mi"úřF© ^„9}óý3˙¸¦ ´Ş{(;3pďť ÚŚĘ9Íä;‰ť%áµŰą2%Y‰Ťv^
�¤> ÎSOL„†ÇAmŃtz0<[ôĺ(č­Lë×<_oţ ´Í|×tŮkkµiuŹEĄµTát}č˘�—)f¨CPł..TâşŔ@}�z,ź~Yeb�ą+Hś3˝ň“8*ńmóďqžÁ1ä{Ü6ö5ÔüXŘŠOz d×

View File

@ -1,4 +0,0 @@
x�ÎA
1 …a×=E÷‚4“¦m@D˜•Çh3)
Ö†
ß.<€Û�ÿÁ“µµG·ÀáÐwU«pLJ9׊‘¦²¨–€$BT!*ÃÀly×W·ÉÈÊKBç=$ŽÂ˜Ò˜×Ex  B¬&¿û}Ýí|³çùvÕOnÛSO²¶‹…@Ì”�¼=‚sÎ §ºþ™ÿzëÈ|²);=

View File

@ -0,0 +1,3 @@
x}ŽÍ
Â0„=ç)ö.Èæwzêc¬Û-
Æ–ÁÇ7‡âQfÃð Œ,µ>8¤CÛTA‹ÏÈ^(: 7RŠa–€N4:�ŽJän³ò¦¯"eÒù–Y‹õa¶\8‰ËŒˆi’Ü5Qö� ¿Û}Ù`á<ŒWýp]Ÿz’¥^À¦„Öù˜3mšÞöSMÿãw|ç�ùÅh¾ý-=ä

View File

@ -0,0 +1,4 @@
x�ΞA
1 @QΧ=Eφ‚$mΣiAD�•ΗHΫλ C�ο,<€ΫΟ[ό²τώ@)Ζ¦
�«n6ε‚ΩG‘�Ή:A[Z Φ•ζs«Ξ¬²ιk@ΒV…lQB�©bυ�ΌΔ))η\)«+‘'‰FήγΎl0ίΰ<ί®ϊ‘Ύ>υT–~
Ι:��„�f―ϋΤΠ?ωΟzσΟ£;v

View File

@ -1 +1 @@
f4316f7a6df3fe5b7e8da1b2c8767ed1e825dc05
aba3469fd6fc584a6af9c0073873005ffaaea56c