mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-29 23:17:32 +02:00
Merge branch 'master' into feat/detect-bare-repo
This commit is contained in:
commit
21a4522a51
8
.github/pull_request_template.md
vendored
Normal file
8
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
- **PR Description**
|
||||
|
||||
- **Please check if the PR fulfills these requirements**
|
||||
|
||||
* [ ] Cheatsheets are up-to-date (run `go run scripts/cheatsheet/main.go generate`)
|
||||
* [ ] Code has been formatted (run `go install mvdan.cc/gofumpt@latest && gofumpt -l -w .`)
|
||||
* [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide)
|
||||
* [ ] Docs (specifically `docs/Config.md`) have been updated if necessary
|
28
.github/workflows/automerge.yml
vendored
28
.github/workflows/automerge.yml
vendored
@ -1,28 +0,0 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: rebase
|
48
.github/workflows/ci.yml
vendored
48
.github/workflows/ci.yml
vendored
@ -46,14 +46,14 @@ jobs:
|
||||
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
||||
run: |
|
||||
go test ./... -short
|
||||
integration-tests:
|
||||
integration-tests-old:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
parallelism: [5]
|
||||
index: [0,1,2,3,4]
|
||||
name: "Integration Tests (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
@ -74,7 +74,31 @@ jobs:
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
run: |
|
||||
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/gui/gui_test.go
|
||||
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Integration Tests"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
run: |
|
||||
go test pkg/integration/clients/*.go
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@ -105,7 +129,13 @@ jobs:
|
||||
- name: Build darwin binary
|
||||
run: |
|
||||
GOOS=darwin go build
|
||||
check-cheatsheet:
|
||||
- name: Build integration test binary
|
||||
run: |
|
||||
GOOS=linux go build cmd/integration_test/main.go
|
||||
- name: Build integration test injector
|
||||
run: |
|
||||
GOOS=linux go build pkg/integration/clients/injector/main.go
|
||||
check-codebase:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
@ -129,6 +159,10 @@ jobs:
|
||||
- name: Check Cheatsheet
|
||||
run: |
|
||||
go run scripts/cheatsheet/main.go check
|
||||
- name: Check Vendor Directory
|
||||
# ensure our vendor directory matches up with our go modules
|
||||
run: |
|
||||
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@ -153,12 +187,6 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v3.1.0
|
||||
with:
|
||||
version: latest
|
||||
- name: Format code
|
||||
run: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1
|
||||
fi
|
||||
- name: errors
|
||||
run: golangci-lint run
|
||||
if: ${{ failure() }}
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -33,11 +33,19 @@ lazygit.exe
|
||||
!.gitmodules_keep
|
||||
|
||||
test/git_server/data
|
||||
|
||||
# we'll scrap these lines once we've fully moved over to the new integration test approach
|
||||
test/integration/*/actual/
|
||||
test/integration/*/used_config/
|
||||
# these sample hooks waste too much space
|
||||
test/integration/*/expected/**/hooks/
|
||||
test/integration/*/expected_remote/**/hooks/
|
||||
|
||||
test/integration_new/**/actual/
|
||||
test/integration_new/**/used_config/
|
||||
# these sample hooks waste too much space
|
||||
test/integration_new/**/expected/**/hooks/
|
||||
test/integration_new/**/expected_remote/**/hooks/
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
__debug_bin
|
||||
|
@ -63,9 +63,9 @@ by setting [`formatting.gofumpt`](https://github.com/golang/tools/blob/master/go
|
||||
```jsonc
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -82,6 +82,7 @@ From most places in the codebase you have access to a logger e.g. `gui.Log.Warn(
|
||||
If you find that the existing logs are too noisy, you can set the log level with e.g. `LOG_LEVEL=warn go run main.go -debug` and then only use `Warn` logs yourself.
|
||||
|
||||
If you need to log from code in the vendor directory (e.g. the `gocui` package), you won't have access to the logger, but you can easily add logging support by adding the following:
|
||||
|
||||
```go
|
||||
func newLogger() *logrus.Entry {
|
||||
// REPLACE THE BELOW PATH WITH YOUR ACTUAL LOG PATH (YOU'LL SEE THIS PRINTED WHEN YOU RUN `lazygit --logs`
|
||||
@ -118,9 +119,7 @@ If you want to trigger a debug session from VSCode, you can use the following sn
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": [
|
||||
"--debug"
|
||||
],
|
||||
"args": ["--debug"],
|
||||
"console": "externalTerminal" // <-- you need this to actually see the lazygit UI in a window while debugging
|
||||
}
|
||||
]
|
||||
@ -129,7 +128,7 @@ If you want to trigger a debug session from VSCode, you can use the following sn
|
||||
|
||||
## Testing
|
||||
|
||||
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. Lazygit has its own integration test system where you can build a sandbox repo with a shell script, record yourself doing something, and commit the resulting repo snapshot. It's pretty damn cool! To learn more see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md)
|
||||
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. For integration tests, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
||||
## Updating Gocui
|
||||
|
||||
|
56
cmd/integration_test/main.go
Normal file
56
cmd/integration_test/main.go
Normal file
@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/clients"
|
||||
)
|
||||
|
||||
var usage = `
|
||||
Usage:
|
||||
See https://github.com/jesseduffield/lazygit/tree/master/pkg/integration/README.md
|
||||
|
||||
CLI mode:
|
||||
> go run cmd/integration_test/main.go cli [--slow] <test1> <test2> ...
|
||||
If you pass no test names, it runs all tests
|
||||
Accepted environment variables:
|
||||
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
|
||||
MODE:
|
||||
* ask (default): if a snapshot test fails, asks if you want to update the snapshot
|
||||
* check: if a snapshot test fails, exits with an error
|
||||
* update: if a snapshot test fails, updates the snapshot
|
||||
* sandbox: uses the test's setup step to run the test in a sandbox where you can do whatever you want
|
||||
|
||||
TUI mode:
|
||||
> go run cmd/integration_test/main.go tui
|
||||
This will open up a terminal UI where you can run tests
|
||||
|
||||
Help:
|
||||
> go run cmd/integration_test/main.go help
|
||||
`
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "help":
|
||||
fmt.Println(usage)
|
||||
case "cli":
|
||||
testNames := os.Args[2:]
|
||||
slow := false
|
||||
// get the next arg if it's --slow
|
||||
if len(os.Args) > 2 && (os.Args[2] == "--slow" || os.Args[2] == "-slow") {
|
||||
testNames = os.Args[3:]
|
||||
slow = true
|
||||
}
|
||||
clients.RunCLI(testNames, slow)
|
||||
case "tui":
|
||||
clients.RunTUI()
|
||||
default:
|
||||
log.Fatal(usage)
|
||||
}
|
||||
}
|
@ -101,7 +101,7 @@ confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip' | 'quit'
|
||||
promptToReturnFromSubprocess: true # display confirmation when subprocess terminates
|
||||
keybinding:
|
||||
universal:
|
||||
@ -531,3 +531,8 @@ notARepository: 'create'
|
||||
# to skip without creating a new repo
|
||||
notARepository: 'skip'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# to exit immediately if run outside of the Git repository
|
||||
notARepository: 'quit'
|
||||
```
|
||||
|
@ -136,7 +136,7 @@ If an option has no name the value will be displayed to the user in place of the
|
||||
|
||||
### Placeholder values
|
||||
|
||||
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/go/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
|
||||
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/golang/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
|
||||
|
||||
```
|
||||
SelectedLocalCommit
|
||||
|
@ -1,122 +1 @@
|
||||
# How To Make And Run Integration Tests For lazygit
|
||||
|
||||
Integration tests are located in `test/integration`. Each test will run a bash script to prepare a test repo, then replay a recorded lazygit session from within that repo, and then the resultant repo will be compared to an expected repo that was created upon the initial recording. Each integration test lives in its own directory, and the name of the directory becomes the name of the test. Within the directory must be the following files:
|
||||
|
||||
### `test.json`
|
||||
|
||||
An example of a `test.json` is:
|
||||
|
||||
```
|
||||
{ "description": "Open a confirmation, then open a menu over that, then close the menu. Verify that the confirmation popup also closes automatically", "speed": 20 }
|
||||
```
|
||||
|
||||
The `speed` key refers to the playback speed as a multiple of the original recording speed. So 20 means the test will run 20 times faster than the original recording speed. If a test fails for a given speed, it will drop the speed and re-test, until finally attempting the test at the original speed. If you omit the speed, it will default to 10.
|
||||
|
||||
### `setup.sh`
|
||||
|
||||
This is a bash script containing the instructions for creating the test repo from scratch. For example:
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
|
||||
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"
|
||||
```
|
||||
|
||||
Be sure to:
|
||||
|
||||
- ensure that by the end of the test you've got at least one commit in the repo, as we've had issues in the past when that wasn't the case.
|
||||
- set the git user email and name as above so that your own user details aren't included in the snapshot.
|
||||
|
||||
## Running tests
|
||||
|
||||
### From a TUI
|
||||
|
||||
You can run/record/sandbox tests via a TUI with the following command:
|
||||
|
||||
```
|
||||
go run test/lazyintegration/main.go
|
||||
```
|
||||
|
||||
This TUI makes much of the following documentation redundant, but feel free to read through anyway!
|
||||
|
||||
### From command line
|
||||
|
||||
To run all tests - assuming you're at the project root:
|
||||
|
||||
```
|
||||
go test ./pkg/gui/
|
||||
```
|
||||
|
||||
To run them in parallel
|
||||
|
||||
```
|
||||
PARALLEL=true go test ./pkg/gui
|
||||
```
|
||||
|
||||
To run a single test
|
||||
|
||||
```
|
||||
go test ./pkg/gui -run /<test name>
|
||||
# For example, to run the `tags` test:
|
||||
go test ./pkg/gui -run /tags
|
||||
```
|
||||
|
||||
To run a test at a certain speed
|
||||
|
||||
```
|
||||
SPEED=2 go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
To update a snapshot
|
||||
|
||||
```
|
||||
MODE=updateSnapshot go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
## Creating a new test
|
||||
|
||||
To create a new test:
|
||||
|
||||
1. Copy and paste an existing test directory and rename the new directory to whatever you want the test name to be. Update the test.json file's description to describe your test.
|
||||
2. Update the `setup.sh` any way you like
|
||||
3. If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used.
|
||||
4. From the lazygit root directory, run:
|
||||
|
||||
```
|
||||
MODE=record go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
5. Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better!
|
||||
6. Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created.
|
||||
|
||||
The resulting directory will look like:
|
||||
|
||||
```
|
||||
actual/ (the resulting repo(s) after running the test, ignored by git)
|
||||
expected/ (the 'snapshot' repo(s))
|
||||
config/ (need not be present)
|
||||
test.json
|
||||
setup.sh
|
||||
recording.json
|
||||
```
|
||||
|
||||
## Sandboxing
|
||||
|
||||
The integration tests serve a secondary purpose of providing a setup for easy sandboxing. If you want to run a test in sandbox mode (meaning the session won't be recorded and we won't create/update snapshots), go:
|
||||
|
||||
```
|
||||
MODE=sandbox go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
## Feedback
|
||||
|
||||
If you think this process can be improved, let me know! It shouldn't be too hard to change things.
|
||||
see new docs [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
@ -102,7 +102,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: Ignore or Exclude file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>s</kbd>: stash all changes
|
||||
<kbd>S</kbd>: view stash options
|
||||
|
@ -277,7 +277,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>i</kbd>: Ignore file
|
||||
<kbd>i</kbd>: ignore file
|
||||
<kbd>r</kbd>: 파일 새로고침
|
||||
<kbd>s</kbd>: 변경사항을 Stash
|
||||
<kbd>S</kbd>: Stash 옵션 보기
|
||||
|
@ -55,7 +55,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: Ignore or Exclude file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
|
@ -125,7 +125,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: Ignore or Exclude file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj zmiany
|
||||
<kbd>S</kbd>: wyświetl opcje schowka
|
||||
|
8
go.mod
8
go.mod
@ -11,13 +11,14 @@ require (
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.5.2
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220806032055-dfd3eb22e18a
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
@ -36,6 +37,7 @@ require (
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
@ -43,7 +45,6 @@ require (
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.5.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.0.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
@ -66,9 +67,8 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
8
go.sum
8
go.sum
@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220806032055-dfd3eb22e18a h1:k+lnojvZ6FoSzOIsSVVBlB9v3EZ+L5Qn/GS5PEzyTAA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220806032055-dfd3eb22e18a/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419 h1:p3Ix7RUcy4X16Lk5jTSfTxecJT7ryqYHclfRbo/Svzs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
|
||||
@ -194,8 +194,8 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
214
main.go
214
main.go
@ -1,222 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const DEFAULT_VERSION = "unversioned"
|
||||
|
||||
// These values may be set by the build script via the LDFLAGS argument
|
||||
var (
|
||||
commit string
|
||||
version = DEFAULT_VERSION
|
||||
date string
|
||||
version string
|
||||
buildSource = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
updateBuildInfo()
|
||||
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := ""
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
gitArg := ""
|
||||
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
||||
|
||||
versionFlag := false
|
||||
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
|
||||
|
||||
debuggingFlag := false
|
||||
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
|
||||
|
||||
logFlag := false
|
||||
flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
configFlag := false
|
||||
flaggy.Bool(&configFlag, "c", "config", "Print the default config")
|
||||
|
||||
configDirFlag := false
|
||||
flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory")
|
||||
|
||||
useConfigDir := ""
|
||||
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
|
||||
|
||||
workTree := ""
|
||||
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
|
||||
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
customConfig := ""
|
||||
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debuggingFlag = true
|
||||
ldFlagsBuildInfo := &app.BuildInfo{
|
||||
Commit: commit,
|
||||
Date: date,
|
||||
Version: version,
|
||||
BuildSource: buildSource,
|
||||
}
|
||||
|
||||
if repoPath != "" {
|
||||
if workTree != "" || gitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
absRepoPath, err := filepath.Abs(repoPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
workTree = absRepoPath
|
||||
gitDir = filepath.Join(absRepoPath, ".git")
|
||||
}
|
||||
|
||||
if customConfig != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", customConfig)
|
||||
}
|
||||
|
||||
if useConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", useConfigDir)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
env.SetGitWorkTreeEnv(workTree)
|
||||
}
|
||||
|
||||
if gitDir != "" {
|
||||
env.SetGitDirEnv(gitDir)
|
||||
}
|
||||
|
||||
if versionFlag {
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if configFlag {
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
err := encoder.Encode(config.GetDefaultConfig())
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
fmt.Printf("%s\n", buf.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if configDirFlag {
|
||||
fmt.Printf("%s\n", config.ConfigDir())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if logFlag {
|
||||
logs.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
if err := os.Chdir(workTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "lazygit-*")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag, tempDir)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
common, err := app.NewCommon(appConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(gitArg)
|
||||
|
||||
app.Run(appConfig, common, types.NewStartArgs(filterPath, parsedGitArg))
|
||||
}
|
||||
|
||||
func parseGitArg(gitArg string) types.GitArg {
|
||||
typedArg := types.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:
|
||||
return typedArg
|
||||
}
|
||||
|
||||
permittedValues := []string{
|
||||
string(types.GitArgStatus),
|
||||
string(types.GitArgBranch),
|
||||
string(types.GitArgLog),
|
||||
string(types.GitArgStash),
|
||||
}
|
||||
|
||||
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
|
||||
gitArg,
|
||||
strings.Join(permittedValues, ", "),
|
||||
)
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func updateBuildInfo() {
|
||||
// if the version has already been set by build flags then we'll honour that.
|
||||
// chances are it's something like v0.31.0 which is more informative than a
|
||||
// commit hash.
|
||||
if version != DEFAULT_VERSION {
|
||||
return
|
||||
}
|
||||
|
||||
buildInfo, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
revision, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.revision"
|
||||
})
|
||||
if ok {
|
||||
commit = revision.Value
|
||||
// if lazygit was built from source we'll show the version as the
|
||||
// abbreviated commit hash
|
||||
version = utils.ShortSha(revision.Value)
|
||||
}
|
||||
|
||||
// if version hasn't been set we assume that neither has the date
|
||||
time, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.time"
|
||||
})
|
||||
if ok {
|
||||
date = time.Value
|
||||
}
|
||||
app.Start(ldFlagsBuildInfo, nil)
|
||||
}
|
||||
|
@ -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_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
@ -23,7 +24,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"
|
||||
)
|
||||
@ -39,7 +39,11 @@ type App struct {
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
}
|
||||
|
||||
func Run(config config.AppConfigurer, common *common.Common, startArgs types.StartArgs) {
|
||||
func Run(
|
||||
config config.AppConfigurer,
|
||||
common *common.Common,
|
||||
startArgs appTypes.StartArgs,
|
||||
) {
|
||||
app, err := NewApp(config, common)
|
||||
|
||||
if err == nil {
|
||||
@ -186,39 +190,53 @@ func (app *App) setupRepo() (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
shouldInitRepo := true
|
||||
notARepository := app.UserConfig.NotARepository
|
||||
initialBranch := ""
|
||||
if notARepository == "prompt" {
|
||||
var shouldInitRepo bool
|
||||
initialBranchArg := ""
|
||||
switch app.UserConfig.NotARepository {
|
||||
case "prompt":
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.CreateRepo)
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \r\n") != "y" {
|
||||
shouldInitRepo = false
|
||||
} else {
|
||||
shouldInitRepo = (strings.Trim(response, " \r\n") == "y")
|
||||
if shouldInitRepo {
|
||||
// Ask for the initial branch name
|
||||
fmt.Print(app.Tr.InitialBranch)
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if trimmedResponse := strings.Trim(response, " \r\n"); len(trimmedResponse) > 0 {
|
||||
initialBranch += "--initial-branch=" + trimmedResponse
|
||||
initialBranchArg += "--initial-branch=" + app.OSCommand.Quote(trimmedResponse)
|
||||
}
|
||||
}
|
||||
} else if notARepository == "skip" {
|
||||
case "create":
|
||||
shouldInitRepo = true
|
||||
case "skip":
|
||||
shouldInitRepo = false
|
||||
case "quit":
|
||||
fmt.Fprintln(os.Stderr, app.Tr.NotARepository)
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, app.Tr.IncorrectNotARepository)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !shouldInitRepo {
|
||||
// Attempt to open a recent repo, exit if no repo could be opened
|
||||
if didOpenRepo := openRecentRepo(app); !didOpenRepo {
|
||||
fmt.Println(app.Tr.NoRecentRepositories)
|
||||
os.Exit(1)
|
||||
|
||||
if shouldInitRepo {
|
||||
if err := app.OSCommand.Cmd.New("git init " + initialBranchArg).Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
if err := app.OSCommand.Cmd.New("git init " + initialBranch).Run(); err != nil {
|
||||
return false, err
|
||||
// check if we have a recent repo we can open
|
||||
for _, repoDir := range app.Config.GetAppState().RecentRepos {
|
||||
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
|
||||
if err := os.Chdir(repoDir); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, app.Tr.NoRecentRepositories)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run this afterward so that the previous repo creation steps can run without this interfering
|
||||
@ -246,7 +264,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
|
||||
}
|
||||
|
265
pkg/app/entry_point.go
Normal file
265
pkg/app/entry_point.go
Normal file
@ -0,0 +1,265 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
"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"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type cliArgs struct {
|
||||
RepoPath string
|
||||
FilterPath string
|
||||
GitArg string
|
||||
PrintVersionInfo bool
|
||||
Debug bool
|
||||
TailLogs bool
|
||||
PrintDefaultConfig bool
|
||||
PrintConfigDir bool
|
||||
UseConfigDir string
|
||||
WorkTree string
|
||||
GitDir string
|
||||
CustomConfigFile string
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
Commit string
|
||||
Date string
|
||||
Version string
|
||||
BuildSource string
|
||||
}
|
||||
|
||||
func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTest) {
|
||||
cliArgs := parseCliArgsAndEnvVars()
|
||||
mergeBuildInfo(buildInfo)
|
||||
|
||||
if cliArgs.RepoPath != "" {
|
||||
if cliArgs.WorkTree != "" || cliArgs.GitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
absRepoPath, err := filepath.Abs(cliArgs.RepoPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cliArgs.WorkTree = absRepoPath
|
||||
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
|
||||
}
|
||||
|
||||
if cliArgs.CustomConfigFile != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", cliArgs.CustomConfigFile)
|
||||
}
|
||||
|
||||
if cliArgs.UseConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", cliArgs.UseConfigDir)
|
||||
}
|
||||
|
||||
if cliArgs.WorkTree != "" {
|
||||
env.SetGitWorkTreeEnv(cliArgs.WorkTree)
|
||||
}
|
||||
|
||||
if cliArgs.GitDir != "" {
|
||||
env.SetGitDirEnv(cliArgs.GitDir)
|
||||
}
|
||||
|
||||
if cliArgs.PrintVersionInfo {
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.PrintDefaultConfig {
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
err := encoder.Encode(config.GetDefaultConfig())
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
fmt.Printf("%s\n", buf.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.PrintConfigDir {
|
||||
fmt.Printf("%s\n", config.ConfigDir())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.TailLogs {
|
||||
logs.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.WorkTree != "" {
|
||||
if err := os.Chdir(cliArgs.WorkTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "lazygit-*")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", buildInfo.Version, buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, cliArgs.Debug, tempDir)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if integrationTest != nil {
|
||||
integrationTest.SetupConfig(appConfig)
|
||||
}
|
||||
|
||||
common, err := NewCommon(appConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(cliArgs.GitArg)
|
||||
|
||||
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
|
||||
}
|
||||
|
||||
func parseCliArgsAndEnvVars() *cliArgs {
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := ""
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
gitArg := ""
|
||||
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
||||
|
||||
printVersionInfo := false
|
||||
flaggy.Bool(&printVersionInfo, "v", "version", "Print the current version")
|
||||
|
||||
debug := false
|
||||
flaggy.Bool(&debug, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
|
||||
|
||||
tailLogs := false
|
||||
flaggy.Bool(&tailLogs, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
printDefaultConfig := false
|
||||
flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config")
|
||||
|
||||
printConfigDir := false
|
||||
flaggy.Bool(&printConfigDir, "cd", "print-config-dir", "Print the config directory")
|
||||
|
||||
useConfigDir := ""
|
||||
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
|
||||
|
||||
workTree := ""
|
||||
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
|
||||
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
customConfigFile := ""
|
||||
flaggy.String(&customConfigFile, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debug = true
|
||||
}
|
||||
|
||||
return &cliArgs{
|
||||
RepoPath: repoPath,
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
PrintVersionInfo: printVersionInfo,
|
||||
Debug: debug,
|
||||
TailLogs: tailLogs,
|
||||
PrintDefaultConfig: printDefaultConfig,
|
||||
PrintConfigDir: printConfigDir,
|
||||
UseConfigDir: useConfigDir,
|
||||
WorkTree: workTree,
|
||||
GitDir: gitDir,
|
||||
CustomConfigFile: customConfigFile,
|
||||
}
|
||||
}
|
||||
|
||||
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 appTypes.GitArgNone, appTypes.GitArgStatus, appTypes.GitArgBranch, appTypes.GitArgLog, appTypes.GitArgStash:
|
||||
return typedArg
|
||||
}
|
||||
|
||||
permittedValues := []string{
|
||||
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'.",
|
||||
gitArg,
|
||||
strings.Join(permittedValues, ", "),
|
||||
)
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// the buildInfo struct we get passed in is based on what's baked into the lazygit
|
||||
// binary via the LDFLAGS argument. Some lazygit distributions will make use of these
|
||||
// arguments and some will not. Go recently started baking in build info
|
||||
// into the binary by default e.g. the git commit hash. So in this function
|
||||
// we merge the two together, giving priority to the stuff set by LDFLAGS.
|
||||
// Note: this mutates the argument passed in
|
||||
func mergeBuildInfo(buildInfo *BuildInfo) {
|
||||
// if the version has already been set by build flags then we'll honour that.
|
||||
// chances are it's something like v0.31.0 which is more informative than a
|
||||
// commit hash.
|
||||
if buildInfo.Version != "" {
|
||||
return
|
||||
}
|
||||
|
||||
buildInfo.Version = "unversioned"
|
||||
|
||||
goBuildInfo, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
revision, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.revision"
|
||||
})
|
||||
if ok {
|
||||
buildInfo.Commit = revision.Value
|
||||
// if lazygit was built from source we'll show the version as the
|
||||
// abbreviated commit hash
|
||||
buildInfo.Version = utils.ShortSha(revision.Value)
|
||||
}
|
||||
|
||||
// if version hasn't been set we assume that neither has the date
|
||||
time, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.time"
|
||||
})
|
||||
if ok {
|
||||
buildInfo.Date = time.Value
|
||||
}
|
||||
}
|
@ -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 {
|
||||
@ -6,6 +10,8 @@ type StartArgs struct {
|
||||
FilterPath string
|
||||
// GitArg determines what context we open in
|
||||
GitArg GitArg
|
||||
// integration test (only relevant when invoking lazygit in the context of an integration test)
|
||||
IntegrationTest integrationTypes.IntegrationTest
|
||||
}
|
||||
|
||||
type GitArg string
|
||||
@ -18,9 +24,10 @@ const (
|
||||
GitArgStash GitArg = "stash"
|
||||
)
|
||||
|
||||
func NewStartArgs(filterPath string, gitArg GitArg) StartArgs {
|
||||
func NewStartArgs(filterPath string, gitArg GitArg, test integrationTypes.IntegrationTest) StartArgs {
|
||||
return StartArgs{
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
IntegrationTest: test,
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@ -45,7 +45,7 @@ func CommandToRun() string {
|
||||
}
|
||||
|
||||
func GetDir() string {
|
||||
return integration.GetRootDirectory() + "/docs/keybindings"
|
||||
return utils.GetLazygitRootDirectory() + "/docs/keybindings"
|
||||
}
|
||||
|
||||
func generateAtDir(cheatsheetDir string) {
|
||||
|
@ -27,8 +27,6 @@ type AppConfig struct {
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
// from AppConfig and still be used by lazygit.
|
||||
type AppConfigurer interface {
|
||||
GetDebug() bool
|
||||
|
||||
|
@ -314,7 +314,9 @@ type CustomCommand struct {
|
||||
}
|
||||
|
||||
type CustomCommandPrompt struct {
|
||||
Type string `yaml:"type"` // one of 'input', 'menu', or 'confirm'
|
||||
// one of 'input', 'menu', 'confirm', or 'menuFromCommand'
|
||||
Type string `yaml:"type"`
|
||||
|
||||
Title string `yaml:"title"`
|
||||
|
||||
// this only apply to input prompts
|
||||
|
@ -85,7 +85,8 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Files.IgnoreOrExcludeFile),
|
||||
Handler: self.checkSelectedFileNode(self.ignoreOrExcludeMenu),
|
||||
Description: self.c.Tr.Actions.IgnoreExcludeFile,
|
||||
Description: self.c.Tr.Actions.LcIgnoreExcludeFile,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Files.RefreshFiles),
|
||||
@ -501,7 +502,7 @@ func (self *FilesController) ignore(node *filetree.FileNode) error {
|
||||
if node.GetPath() == ".gitignore" {
|
||||
return self.c.ErrorMsg(self.c.Tr.Actions.IgnoreFileErr)
|
||||
}
|
||||
err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.IgnoreExcludeFile, self.git.WorkingTree.Ignore)
|
||||
err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.LcIgnoreExcludeFile, self.git.WorkingTree.Ignore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -527,7 +528,7 @@ func (self *FilesController) exclude(node *filetree.FileNode) error {
|
||||
|
||||
func (self *FilesController) ignoreOrExcludeMenu(node *filetree.FileNode) error {
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.Actions.IgnoreExcludeFile,
|
||||
Title: self.c.Tr.Actions.LcIgnoreExcludeFile,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
LabelColumns: []string{self.c.Tr.LcIgnoreFile},
|
||||
|
@ -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,13 +419,15 @@ var RuneReplacements = map[rune]string{
|
||||
graph.CommitSymbol: "o",
|
||||
}
|
||||
|
||||
func (gui *Gui) initGocui(headless bool) (*gocui.Gui, error) {
|
||||
recordEvents := recordingEvents()
|
||||
func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest) (*gocui.Gui, error) {
|
||||
recordEvents := RecordingEvents()
|
||||
playMode := gocui.NORMAL
|
||||
if recordEvents {
|
||||
playMode = gocui.RECORDING
|
||||
} else if replaying() {
|
||||
} else if Replaying() {
|
||||
playMode = gocui.REPLAYING
|
||||
} else if test != nil {
|
||||
playMode = gocui.REPLAYING_NEW
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless, RuneReplacements)
|
||||
@ -474,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())
|
||||
func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
g, err := gui.initGocui(Headless(), startArgs.IntegrationTest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -490,23 +494,7 @@ func (gui *Gui) Run(startArgs types.StartArgs) error {
|
||||
})
|
||||
deadlock.Opts.Disable = !gui.Debug
|
||||
|
||||
if replaying() {
|
||||
gui.g.RecordingConfig = gocui.RecordingConfig{
|
||||
Speed: getRecordingSpeed(),
|
||||
Leeway: 100,
|
||||
}
|
||||
|
||||
var err error
|
||||
gui.g.Recording, err = gui.loadRecording()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
gui.handleTestMode(startArgs.IntegrationTest)
|
||||
|
||||
gui.g.OnSearchEscape = gui.onSearchEscape
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
@ -567,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 {
|
||||
@ -593,7 +581,7 @@ func (gui *Gui) RunAndHandleError(startArgs types.StartArgs) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.saveRecording(gui.g.Recording); err != nil {
|
||||
if err := SaveRecording(gui.g.Recording); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -627,7 +615,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
|
||||
gui.Mutexes.SubprocessMutex.Lock()
|
||||
defer gui.Mutexes.SubprocessMutex.Unlock()
|
||||
|
||||
if replaying() {
|
||||
if Replaying() {
|
||||
// we do not yet support running subprocesses within integration tests. So if
|
||||
// we're replaying an integration test and we're inside this method, something
|
||||
// has gone wrong, so we should fail
|
||||
|
81
pkg/gui/gui_driver.go
Normal file
81
pkg/gui/gui_driver.go
Normal file
@ -0,0 +1,81 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"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 GuiDriver struct {
|
||||
gui *Gui
|
||||
}
|
||||
|
||||
var _ integrationTypes.GuiDriver = &GuiDriver{}
|
||||
|
||||
func (self *GuiDriver) PressKey(keyStr string) {
|
||||
key := keybindings.GetKey(keyStr)
|
||||
|
||||
var r rune
|
||||
var tcellKey tcell.Key
|
||||
switch v := key.(type) {
|
||||
case rune:
|
||||
r = v
|
||||
tcellKey = tcell.KeyRune
|
||||
case gocui.Key:
|
||||
tcellKey = tcell.Key(v)
|
||||
}
|
||||
|
||||
self.gui.g.ReplayedEvents.Keys <- gocui.NewTcellKeyEventWrapper(
|
||||
tcell.NewEventKey(tcellKey, r, tcell.ModNone),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Keys() config.KeybindingConfig {
|
||||
return self.gui.Config.GetUserConfig().Keybinding
|
||||
}
|
||||
|
||||
func (self *GuiDriver) CurrentContext() types.Context {
|
||||
return self.gui.c.CurrentContext()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Model() *types.Model {
|
||||
return self.gui.State.Model
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Fail(message string) {
|
||||
self.gui.g.Close()
|
||||
// need to give the gui time to close
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
panic(message)
|
||||
}
|
||||
|
||||
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
|
||||
func (self *GuiDriver) Log(message string) {
|
||||
self.gui.c.Log.Warn(message)
|
||||
}
|
||||
|
||||
// logs in the actual UI (in the commands panel)
|
||||
func (self *GuiDriver) LogUI(message string) {
|
||||
self.gui.c.LogAction(message)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) CheckedOutRef() *models.Branch {
|
||||
return self.gui.helpers.Refs.GetCheckedOutRef()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) MainView() *gocui.View {
|
||||
return self.gui.mainView()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) SecondaryView() *gocui.View {
|
||||
return self.gui.secondaryView()
|
||||
}
|
@ -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,
|
||||
|
@ -1,74 +0,0 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func recordingEvents() bool {
|
||||
return recordEventsTo() != ""
|
||||
}
|
||||
|
||||
func recordEventsTo() string {
|
||||
return os.Getenv("RECORD_EVENTS_TO")
|
||||
}
|
||||
|
||||
func replaying() bool {
|
||||
return os.Getenv("REPLAY_EVENTS_FROM") != ""
|
||||
}
|
||||
|
||||
func headless() bool {
|
||||
return os.Getenv("HEADLESS") != ""
|
||||
}
|
||||
|
||||
func getRecordingSpeed() float64 {
|
||||
// humans are slow so this speeds things up.
|
||||
speed := 1.0
|
||||
envReplaySpeed := os.Getenv("SPEED")
|
||||
if envReplaySpeed != "" {
|
||||
var err error
|
||||
speed, err = strconv.ParseFloat(envReplaySpeed, 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return speed
|
||||
}
|
||||
|
||||
func (gui *Gui) loadRecording() (*gocui.Recording, error) {
|
||||
path := os.Getenv("REPLAY_EVENTS_FROM")
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recording := &gocui.Recording{}
|
||||
|
||||
err = json.Unmarshal(data, &recording)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recording, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) saveRecording(recording *gocui.Recording) error {
|
||||
if !recordingEvents() {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonEvents, err := json.Marshal(recording)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := recordEventsTo()
|
||||
|
||||
return ioutil.WriteFile(path, jsonEvents, 0o600)
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
@ -101,3 +104,23 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CustomCommandObject struct {
|
||||
// deprecated. Use Responses instead
|
||||
PromptResponses []string
|
||||
Form map[string]string
|
||||
}
|
||||
|
||||
func ResolveTemplate(templateStr string, object interface{}) (string, error) {
|
||||
tmpl, err := template.New("template").Parse(templateStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, object); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
124
pkg/gui/test_mode.go
Normal file
124
pkg/gui/test_mode.go
Normal file
@ -0,0 +1,124 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type IntegrationTest interface {
|
||||
Run(guiAdapter *GuiDriver)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) {
|
||||
if test != nil {
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
test.Run(&GuiDriver{gui: gui})
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
})
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
log.Fatal("gocui should have already exited")
|
||||
}()
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
|
||||
if Replaying() {
|
||||
gui.g.RecordingConfig = gocui.RecordingConfig{
|
||||
Speed: GetRecordingSpeed(),
|
||||
Leeway: 100,
|
||||
}
|
||||
|
||||
var err error
|
||||
gui.g.Recording, err = LoadRecording()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Headless() bool {
|
||||
return os.Getenv("HEADLESS") != ""
|
||||
}
|
||||
|
||||
// OLD integration test format stuff
|
||||
|
||||
func Replaying() bool {
|
||||
return os.Getenv("REPLAY_EVENTS_FROM") != ""
|
||||
}
|
||||
|
||||
func RecordingEvents() bool {
|
||||
return recordEventsTo() != ""
|
||||
}
|
||||
|
||||
func recordEventsTo() string {
|
||||
return os.Getenv("RECORD_EVENTS_TO")
|
||||
}
|
||||
|
||||
func GetRecordingSpeed() float64 {
|
||||
// humans are slow so this speeds things up.
|
||||
speed := 1.0
|
||||
envReplaySpeed := os.Getenv("SPEED")
|
||||
if envReplaySpeed != "" {
|
||||
var err error
|
||||
speed, err = strconv.ParseFloat(envReplaySpeed, 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return speed
|
||||
}
|
||||
|
||||
func LoadRecording() (*gocui.Recording, error) {
|
||||
path := os.Getenv("REPLAY_EVENTS_FROM")
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recording := &gocui.Recording{}
|
||||
|
||||
err = json.Unmarshal(data, &recording)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recording, nil
|
||||
}
|
||||
|
||||
func SaveRecording(recording *gocui.Recording) error {
|
||||
if !RecordingEvents() {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonEvents, err := json.Marshal(recording)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := recordEventsTo()
|
||||
|
||||
return ioutil.WriteFile(path, jsonEvents, 0o600)
|
||||
}
|
@ -507,7 +507,7 @@ func chineseTranslationSet() TranslationSet {
|
||||
UnstageFile: "取消暂存文件",
|
||||
UnstageAllFiles: "取消暂存所有文件",
|
||||
StageAllFiles: "暂存所有文件",
|
||||
IgnoreExcludeFile: "忽略文件",
|
||||
LcIgnoreExcludeFile: "忽略文件",
|
||||
Commit: "提交 (Commit)",
|
||||
EditFile: "编辑文件",
|
||||
Push: "推送 (Push)",
|
||||
|
@ -262,6 +262,7 @@ type TranslationSet struct {
|
||||
BareRepo string
|
||||
InitialBranch string
|
||||
NoRecentRepositories string
|
||||
IncorrectNotARepository string
|
||||
AutoStashTitle string
|
||||
AutoStashPrompt string
|
||||
StashPrefix string
|
||||
@ -569,8 +570,7 @@ type Actions struct {
|
||||
UnstageFile string
|
||||
UnstageAllFiles string
|
||||
StageAllFiles string
|
||||
IgnoreExcludeFile string
|
||||
IgnoreFile string
|
||||
LcIgnoreExcludeFile string
|
||||
IgnoreFileErr string
|
||||
ExcludeFile string
|
||||
ExcludeFileErr string
|
||||
@ -902,6 +902,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not yet support bare repos. Open most recent repo? (y/n) ",
|
||||
InitialBranch: "Branch name? (leave empty for git's default): ",
|
||||
NoRecentRepositories: "Must open lazygit in a git repository. No valid recent repositories. Exiting.",
|
||||
IncorrectNotARepository: "The value of 'notARepository' is incorrect. It should be one of 'prompt', 'create', 'skip', or 'quit'.",
|
||||
AutoStashTitle: "Autostash?",
|
||||
AutoStashPrompt: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)",
|
||||
StashPrefix: "Auto-stashing changes for ",
|
||||
@ -1192,8 +1193,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
UnstageFile: "Unstage file",
|
||||
UnstageAllFiles: "Unstage all files",
|
||||
StageAllFiles: "Stage all files",
|
||||
IgnoreExcludeFile: "Ignore or Exclude file",
|
||||
IgnoreFile: "Ignore or Exclude file",
|
||||
LcIgnoreExcludeFile: "ignore or exclude file",
|
||||
IgnoreFileErr: "Cannot ignore .gitignore",
|
||||
ExcludeFile: "Exclude file",
|
||||
ExcludeFileErr: "Cannot exclude .git/info/exclude",
|
||||
|
@ -533,7 +533,7 @@ func japaneseTranslationSet() TranslationSet {
|
||||
UnstageFile: "ファイルをアンステージ",
|
||||
UnstageAllFiles: "すべてのファイルをアンステージ",
|
||||
StageAllFiles: "すべてのファイルをステージ",
|
||||
IgnoreExcludeFile: "ファイルをignore",
|
||||
LcIgnoreExcludeFile: "ファイルをignore",
|
||||
Commit: "コミット",
|
||||
EditFile: "ファイルを編集",
|
||||
Push: "Push",
|
||||
|
@ -536,7 +536,7 @@ func koreanTranslationSet() TranslationSet {
|
||||
UnstageFile: "Unstage file",
|
||||
UnstageAllFiles: "Unstage all files",
|
||||
StageAllFiles: "Stage all files",
|
||||
IgnoreExcludeFile: "Ignore file",
|
||||
LcIgnoreExcludeFile: "ignore file",
|
||||
Commit: "커밋",
|
||||
EditFile: "파일 수정",
|
||||
Push: "푸시",
|
||||
|
86
pkg/integration/README.md
Normal file
86
pkg/integration/README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Integration Tests
|
||||
|
||||
The pkg/integration 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.
|
||||
|
||||
TL;DR: integration tests live in pkg/integration/tests. Run integration tests with:
|
||||
|
||||
```sh
|
||||
go run cmd/integration_test/main.go tui
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
go run cmd/integration_test/main.go cli [--slow] [testname or testpath...]
|
||||
```
|
||||
|
||||
## Writing tests
|
||||
|
||||
The tests live in pkg/integration/tests. Each test has two important steps: the setup step and the run step.
|
||||
|
||||
### Setup step
|
||||
|
||||
In the setup step, we prepare a repo with shell commands, for example, creating a merge conflict that will need to be resolved upon opening lazygit. This is all done via the `shell` argument.
|
||||
|
||||
### Run step
|
||||
|
||||
The run step has four arguments passed in:
|
||||
|
||||
1. `shell`
|
||||
2. `input`
|
||||
3. `assert`
|
||||
4. `keys`
|
||||
|
||||
`shell` we've already seen in the setup step. The reason it's passed into the run step is that we may want to emulate background events. For example, the user modifying a file outside of lazygit.
|
||||
|
||||
`input` is for driving the gui by pressing certain keys, selecting list items, etc.
|
||||
|
||||
`assert` is for asserting on the state of the lazygit session. When you call a method on `assert`, the assert struct will wait for the assertion to hold true and then continue (failing the test after a timeout). For this reason, assertions have two purposes: one is to ensure the test fails as soon as something unexpected happens, but another is to allow lazygit to process a keypress before you follow up with more keypresses. If you input a bunch of keypresses too quickly lazygit might get confused.
|
||||
|
||||
### Tips
|
||||
|
||||
Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer.
|
||||
|
||||
Use assertions to ensure that lazygit has processed all your keybindings so far. For example, if you press 'n' on a branch to create a new branch, assert that the confirmation view is now focused.
|
||||
|
||||
If you find yourself doing something frequently in a test, consider making it a method in one of the helper arguments. For example, instead of calling `input.PressKey(keys.Universal.Confirm)` in 100 places, it's better to have a method `input.Confirm()`. This is not to say that everything should be made into a method on the input struct: just things that are particularly common in tests.
|
||||
|
||||
## Running tests
|
||||
|
||||
There are three ways to invoke a test:
|
||||
|
||||
1. go run cmd/integration_test/main.go cli [--slow] [testname or testpath...]
|
||||
2. go run cmd/integration_test/main.go tui
|
||||
3. go test pkg/integration/clients/go_test.go
|
||||
|
||||
The first, the test runner, is for directly running a test from the command line. If you pass no arguments, it runs all tests.
|
||||
The second, the TUI, is for running tests from a terminal UI where it's easier to find a test and run it without having to copy it's name and paste it into the terminal. This is the easiest approach by far.
|
||||
The third, the go-test command, intended only for use in CI, to be run along with the other `go test` tests. This runs the tests in headless mode so there's no visual output.
|
||||
|
||||
The name of a test is based on its path, so the name of the test at `pkg/integration/tests/commit/new_branch.go` is commit/new_branch. So to run it with our test runner you would run `go run cmd/integration_test/main.go cli commit/new_branch`.
|
||||
|
||||
You can pass the KEY_PRESS_DELAY env var to the test runner in order to set a delay in milliseconds between keypresses, which helps for watching a test at a realistic speed to understand what it's doing. Or you can pass the '--slow' flag which sets a pre-set 'slow' key delay. In the tui you can press 't' to run the test in slow mode.
|
||||
|
||||
### Snapshots
|
||||
|
||||
At the moment (this is subject to change) each test has a snapshot repo created after running for the first time. These snapshots live in `test/integration_new`, in folders named 'expected' (alongside the 'actual' folders which contain the resulting repo from the last test run). Whenever you run a test, the resultant repo will be compared against the snapshot repo and if they're different, you'll be asked whether you want to update the snapshot. If you want to update a snapshot without being prompted you can pass MODE=update to the test runner.
|
||||
|
||||
### Sandbox mode
|
||||
|
||||
Say you want to do a manual test of how lazygit handles merge-conflicts, but you can't be bothered actually finding a way to create merge conflicts in a repo. To make your life easier, you can simply run a merge-conflicts test in sandbox mode, meaning the setup step is run for you, and then instead of the test driving the lazygit session, you're allowed to drive it yourself.
|
||||
|
||||
To run a test in sandbox mode you can press 's' on a test in the test TUI or pass the env var MODE=sandbox to the test runner.
|
||||
|
||||
## Migration process
|
||||
|
||||
At the time of writing, most tests are created under an old approach, where you would record yourself in a lazygit session and then the test would replay the keybindings with the same timestamps. This old approach is great for writing tests quickly, but is much harder to maintain. It has to rely entirely on snapshots to determining if a test passes or fails, and can't do assertions along the way. It's also harder to grok what's the intention behind certain actions that take place within the test (e.g. was the recorder intentionally switching to another panel or was that just a misclick?).
|
||||
|
||||
At the moment, all the deprecated test code lives in pkg/integration/deprecated. Hopefully in the very near future we migrate everything across so that we don't need to maintain two systems.
|
||||
|
||||
We should never write any new tests under the old method, and if a given test breaks because of new functionality, it's best to simply rewrite it under the new approach. If you want to run a test for the sake of watching what it does so that you can transcribe it into the new approach, you can run:
|
||||
|
||||
```
|
||||
go run pkg/integration/deprecated/cmd/tui/main.go
|
||||
```
|
||||
|
||||
The tests in the old format live in test/integration. In the old format, test definitions are co-located with the snapshots. The setup step is done in a `setup.sh` shell script and the `recording.json` file contains the recorded keypresses to be replayed during the test.
|
113
pkg/integration/clients/cli.go
Normal file
113
pkg/integration/clients/cli.go
Normal file
@ -0,0 +1,113 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
||||
)
|
||||
|
||||
// see pkg/integration/README.md
|
||||
|
||||
// The purpose of this program is to run integration tests. It does this by
|
||||
// building our injector program (in the sibling injector directory) and then for
|
||||
// each test we're running, invoke the injector program with the test's name as
|
||||
// an environment variable. Then the injector finds the test and passes it to
|
||||
// the lazygit startup code.
|
||||
|
||||
// If invoked directly, you can specify tests to run by passing their names as positional arguments
|
||||
|
||||
func RunCLI(testNames []string, slow bool) {
|
||||
keyPressDelay := tryConvert(os.Getenv("KEY_PRESS_DELAY"), 0)
|
||||
if slow {
|
||||
keyPressDelay = SLOW_KEY_PRESS_DELAY
|
||||
}
|
||||
|
||||
err := components.RunTests(
|
||||
getTestsToRun(testNames),
|
||||
log.Printf,
|
||||
runCmdInTerminal,
|
||||
runAndPrintError,
|
||||
getModeFromEnv(),
|
||||
keyPressDelay,
|
||||
)
|
||||
if err != nil {
|
||||
log.Print(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func runAndPrintError(test *components.IntegrationTest, f func() error) {
|
||||
if err := f(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func getTestsToRun(testNames []string) []*components.IntegrationTest {
|
||||
allIntegrationTests := tests.GetTests()
|
||||
var testsToRun []*components.IntegrationTest
|
||||
|
||||
if len(testNames) == 0 {
|
||||
return allIntegrationTests
|
||||
}
|
||||
|
||||
testNames = slices.Map(testNames, func(name string) string {
|
||||
// allowing full test paths to be passed for convenience
|
||||
return strings.TrimSuffix(
|
||||
regexp.MustCompile(`.*pkg/integration/tests/`).ReplaceAllString(name, ""),
|
||||
".go",
|
||||
)
|
||||
})
|
||||
|
||||
outer:
|
||||
for _, testName := range testNames {
|
||||
// check if our given test name actually exists
|
||||
for _, test := range allIntegrationTests {
|
||||
if test.Name() == testName {
|
||||
testsToRun = append(testsToRun, test)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
log.Fatalf("test %s not found. Perhaps you forgot to add it to `pkg/integration/integration_tests/tests.go`?", testName)
|
||||
}
|
||||
|
||||
return testsToRun
|
||||
}
|
||||
|
||||
func runCmdInTerminal(cmd *exec.Cmd) error {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func getModeFromEnv() components.Mode {
|
||||
switch os.Getenv("MODE") {
|
||||
case "", "ask":
|
||||
return components.ASK_TO_UPDATE_SNAPSHOT
|
||||
case "check":
|
||||
return components.CHECK_SNAPSHOT
|
||||
case "update":
|
||||
return components.UPDATE_SNAPSHOT
|
||||
case "sandbox":
|
||||
return components.SANDBOX
|
||||
default:
|
||||
log.Fatalf("unknown test mode: %s, must be one of [ask, check, update, sandbox]", os.Getenv("MODE"))
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func tryConvert(numStr string, defaultVal int) int {
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
return num
|
||||
}
|
68
pkg/integration/clients/go_test.go
Normal file
68
pkg/integration/clients/go_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package clients
|
||||
|
||||
// this is the new way of running tests. See pkg/integration/integration_tests/commit.go
|
||||
// for an example
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
|
||||
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
|
||||
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
|
||||
testNumber := 0
|
||||
|
||||
err := components.RunTests(
|
||||
tests.GetTests(),
|
||||
t.Logf,
|
||||
runCmdHeadless,
|
||||
func(test *components.IntegrationTest, f func() error) {
|
||||
defer func() { testNumber += 1 }()
|
||||
if testNumber%parallelTotal != parallelIndex {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run(test.Name(), func(t *testing.T) {
|
||||
err := f()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
},
|
||||
components.CHECK_SNAPSHOT,
|
||||
0,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
63
pkg/integration/clients/injector/main.go
Normal file
63
pkg/integration/clients/injector/main.go
Normal file
@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// The purpose of this program is to run lazygit with an integration test passed in.
|
||||
// We could have done the check on TEST_NAME in the root main.go but
|
||||
// that would mean lazygit would be depending on integration test code which
|
||||
// would bloat the binary.
|
||||
|
||||
// You should not invoke this program directly. Instead you should go through
|
||||
// go run cmd/integration_test/main.go
|
||||
|
||||
func main() {
|
||||
dummyBuildInfo := &app.BuildInfo{
|
||||
Commit: "",
|
||||
Date: "",
|
||||
Version: "",
|
||||
BuildSource: "integration test",
|
||||
}
|
||||
|
||||
integrationTest := getIntegrationTest()
|
||||
|
||||
app.Start(dummyBuildInfo, integrationTest)
|
||||
}
|
||||
|
||||
func getIntegrationTest() integrationTypes.IntegrationTest {
|
||||
if daemon.InDaemonMode() {
|
||||
// if we've invoked lazygit as a daemon from within lazygit,
|
||||
// we don't want to pass a test to the rest of the code.
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.Getenv(components.SANDBOX_ENV_VAR) == "true" {
|
||||
// when in sandbox mode we don't want the test controlling the gui
|
||||
return nil
|
||||
}
|
||||
|
||||
integrationTestName := os.Getenv(components.TEST_NAME_ENV_VAR)
|
||||
if integrationTestName == "" {
|
||||
panic(fmt.Sprintf(
|
||||
"expected %s environment variable to be set, given that we're running an integration test",
|
||||
components.TEST_NAME_ENV_VAR,
|
||||
))
|
||||
}
|
||||
|
||||
allTests := tests.GetTests()
|
||||
for _, candidateTest := range allTests {
|
||||
if candidateTest.Name() == integrationTestName {
|
||||
return candidateTest
|
||||
}
|
||||
}
|
||||
|
||||
panic("Could not find integration test with name: " + integrationTestName)
|
||||
}
|
382
pkg/integration/clients/tui.go
Normal file
382
pkg/integration/clients/tui.go
Normal file
@ -0,0 +1,382 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// This program lets you run integration tests from a TUI. See pkg/integration/README.md for more info.
|
||||
|
||||
var SLOW_KEY_PRESS_DELAY = 300
|
||||
|
||||
func RunTUI() {
|
||||
rootDir := utils.GetLazygitRootDirectory()
|
||||
testDir := filepath.Join(rootDir, "test", "integration")
|
||||
|
||||
app := newApp(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.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
||||
if app.itemIdx < len(app.filteredTests)-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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
suspendAndRunTest(currentTest, components.SANDBOX, 0)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
suspendAndRunTest(currentTest, components.ASK_TO_UPDATE_SNAPSHOT, 0)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
suspendAndRunTest(currentTest, components.ASK_TO_UPDATE_SNAPSHOT, SLOW_KEY_PRESS_DELAY)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
app.filteredTests = app.allTests
|
||||
app.renderTests()
|
||||
app.editorView.TextArea.Clear()
|
||||
app.editorView.Clear()
|
||||
app.editorView.Reset()
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("editor", gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
||||
app.filtering = false
|
||||
|
||||
if _, err := g.SetCurrentView("list"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.renderTests()
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
err = g.MainLoop()
|
||||
g.Close()
|
||||
switch err {
|
||||
case gocui.ErrQuit:
|
||||
return
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
type app struct {
|
||||
allTests []*components.IntegrationTest
|
||||
filteredTests []*components.IntegrationTest
|
||||
itemIdx int
|
||||
testDir string
|
||||
filtering bool
|
||||
g *gocui.Gui
|
||||
listView *gocui.View
|
||||
editorView *gocui.View
|
||||
}
|
||||
|
||||
func newApp(testDir string) *app {
|
||||
return &app{testDir: testDir, allTests: tests.GetTests()}
|
||||
}
|
||||
|
||||
func (self *app) getCurrentTest() *components.IntegrationTest {
|
||||
self.adjustCursor()
|
||||
if len(self.filteredTests) > 0 {
|
||||
return self.filteredTests[self.itemIdx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *app) loadTests() {
|
||||
self.filteredTests = self.allTests
|
||||
|
||||
self.adjustCursor()
|
||||
}
|
||||
|
||||
func (self *app) adjustCursor() {
|
||||
self.itemIdx = utils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1)
|
||||
}
|
||||
|
||||
func (self *app) filterWithString(needle string) {
|
||||
if needle == "" {
|
||||
self.filteredTests = self.allTests
|
||||
} else {
|
||||
self.filteredTests = slices.Filter(self.allTests, func(test *components.IntegrationTest) bool {
|
||||
return strings.Contains(test.Name(), needle)
|
||||
})
|
||||
}
|
||||
|
||||
self.renderTests()
|
||||
self.g.Update(func(g *gocui.Gui) error { return nil })
|
||||
}
|
||||
|
||||
func (self *app) renderTests() {
|
||||
self.listView.Clear()
|
||||
for _, test := range self.filteredTests {
|
||||
fmt.Fprintln(self.listView, test.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := f(v, key, ch, mod)
|
||||
if matched {
|
||||
self.filterWithString(v.TextArea.GetContent())
|
||||
}
|
||||
return matched
|
||||
}
|
||||
}
|
||||
|
||||
func suspendAndRunTest(test *components.IntegrationTest, mode components.Mode, keyPressDelay int) {
|
||||
if err := gocui.Screen.Suspend(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
runTuiTest(test, mode, keyPressDelay)
|
||||
|
||||
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 (self *app) layout(g *gocui.Gui) error {
|
||||
maxX, maxY := g.Size()
|
||||
descriptionViewHeight := 7
|
||||
keybindingsViewHeight := 3
|
||||
editorViewHeight := 3
|
||||
if !self.filtering {
|
||||
editorViewHeight = 0
|
||||
} else {
|
||||
descriptionViewHeight = 0
|
||||
keybindingsViewHeight = 0
|
||||
}
|
||||
g.Cursor = self.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
|
||||
}
|
||||
|
||||
if self.listView == nil {
|
||||
self.listView = listView
|
||||
}
|
||||
|
||||
listView.Highlight = true
|
||||
self.renderTests()
|
||||
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
|
||||
}
|
||||
|
||||
if self.editorView == nil {
|
||||
self.editorView = editorView
|
||||
}
|
||||
|
||||
editorView.Title = "Filter"
|
||||
editorView.FgColor = gocui.ColorDefault
|
||||
editorView.Editable = true
|
||||
editorView.Editor = gocui.EditorFunc(self.wrapEditor(gocui.SimpleEditor))
|
||||
}
|
||||
|
||||
currentTest := self.getCurrentTest()
|
||||
if currentTest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
descriptionView.Clear()
|
||||
fmt.Fprint(descriptionView, currentTest.Description())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
func runTuiTest(test *components.IntegrationTest, mode components.Mode, keyPressDelay int) {
|
||||
err := components.RunTests(
|
||||
[]*components.IntegrationTest{test},
|
||||
log.Printf,
|
||||
runCmdInTerminal,
|
||||
runAndPrintError,
|
||||
mode,
|
||||
keyPressDelay,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
}
|
202
pkg/integration/components/assert.go
Normal file
202
pkg/integration/components/assert.go
Normal file
@ -0,0 +1,202 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// through this struct we assert on the state of the lazygit gui
|
||||
|
||||
type Assert struct {
|
||||
gui integrationTypes.GuiDriver
|
||||
}
|
||||
|
||||
func NewAssert(gui integrationTypes.GuiDriver) *Assert {
|
||||
return &Assert{gui: gui}
|
||||
}
|
||||
|
||||
// for making assertions on string values
|
||||
type matcher struct {
|
||||
testFn func(string) (bool, string)
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (self *matcher) test(value string) (bool, string) {
|
||||
ok, message := self.testFn(value)
|
||||
if ok {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
if self.prefix != "" {
|
||||
return false, self.prefix + " " + message
|
||||
}
|
||||
|
||||
return false, message
|
||||
}
|
||||
|
||||
func (self *matcher) context(prefix string) *matcher {
|
||||
self.prefix = prefix
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func Contains(target string) *matcher {
|
||||
return &matcher{testFn: func(value string) (bool, string) {
|
||||
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to contain '%s'", value, target)
|
||||
}}
|
||||
}
|
||||
|
||||
func Equals(target string) *matcher {
|
||||
return &matcher{testFn: func(value string) (bool, string) {
|
||||
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
|
||||
}}
|
||||
}
|
||||
|
||||
func (self *Assert) WorkingTreeFileCount(expectedCount int) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Files)
|
||||
|
||||
return actualCount == expectedCount, fmt.Sprintf(
|
||||
"Expected %d changed working tree files, but got %d",
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) CommitCount(expectedCount int) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Commits)
|
||||
|
||||
return actualCount == expectedCount, fmt.Sprintf(
|
||||
"Expected %d commits present, but got %d",
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) MatchHeadCommitMessage(matcher *matcher) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
return len(self.gui.Model().Commits) > 0, "Expected at least one commit to be present"
|
||||
})
|
||||
|
||||
self.matchString(matcher, "Unexpected commit message.",
|
||||
func() string {
|
||||
return self.gui.Model().Commits[0].Name
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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 *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 *Assert) InListContext() {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
currentContext := self.gui.CurrentContext()
|
||||
_, ok := currentContext.(types.IListContext)
|
||||
return ok, fmt.Sprintf("Expected current context to be a list context, but got %s", currentContext.GetKey())
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) MatchSelectedLine(matcher *matcher) {
|
||||
self.matchString(matcher, "Unexpected selected line.",
|
||||
func() string {
|
||||
return self.gui.CurrentContext().GetView().SelectedLine()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *Assert) InPrompt() {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
currentView := self.gui.CurrentContext().GetView()
|
||||
return currentView.Name() == "confirmation" && currentView.Editable, "Expected prompt popup to be focused"
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) InConfirm() {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
currentView := self.gui.CurrentContext().GetView()
|
||||
return currentView.Name() == "confirmation" && !currentView.Editable, "Expected confirmation popup to be focused"
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) InAlert() {
|
||||
// basically the same thing as a confirmation popup with the current implementation
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
currentView := self.gui.CurrentContext().GetView()
|
||||
return currentView.Name() == "confirmation" && !currentView.Editable, "Expected alert popup to be focused"
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) InMenu() {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
return self.gui.CurrentContext().GetView().Name() == "menu", "Expected popup menu to be focused"
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) MatchCurrentViewTitle(matcher *matcher) {
|
||||
self.matchString(matcher, "Unexpected current view title.",
|
||||
func() string {
|
||||
return self.gui.CurrentContext().GetView().Title
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *Assert) MatchMainViewContent(matcher *matcher) {
|
||||
self.matchString(matcher, "Unexpected main view content.",
|
||||
func() string {
|
||||
return self.gui.MainView().Buffer()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *Assert) MatchSecondaryViewContent(matcher *matcher) {
|
||||
self.matchString(matcher, "Unexpected secondary view title.",
|
||||
func() string {
|
||||
return self.gui.SecondaryView().Buffer()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *Assert) matchString(matcher *matcher, context string, getValue func() string) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
value := getValue()
|
||||
return matcher.context(context).test(value)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) assertWithRetries(test func() (bool, string)) {
|
||||
waitTimes := []int{0, 1, 5, 10, 200, 500, 1000}
|
||||
|
||||
var message string
|
||||
for _, waitTime := range waitTimes {
|
||||
time.Sleep(time.Duration(waitTime) * time.Millisecond)
|
||||
|
||||
var ok bool
|
||||
ok, message = test()
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.Fail(message)
|
||||
}
|
||||
|
||||
// for when you just want to fail the test yourself
|
||||
func (self *Assert) Fail(message string) {
|
||||
self.gui.Fail(message)
|
||||
}
|
166
pkg/integration/components/input.go
Normal file
166
pkg/integration/components/input.go
Normal file
@ -0,0 +1,166 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
type Input struct {
|
||||
gui integrationTypes.GuiDriver
|
||||
keys config.KeybindingConfig
|
||||
assert *Assert
|
||||
pushKeyDelay int
|
||||
}
|
||||
|
||||
func NewInput(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, assert *Assert, pushKeyDelay int) *Input {
|
||||
return &Input{
|
||||
gui: gui,
|
||||
keys: keys,
|
||||
assert: assert,
|
||||
pushKeyDelay: pushKeyDelay,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *Input) PressKeys(keyStrs ...string) {
|
||||
for _, keyStr := range keyStrs {
|
||||
self.pressKey(keyStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Input) pressKey(keyStr string) {
|
||||
self.Wait(self.pushKeyDelay)
|
||||
|
||||
self.gui.PressKey(keyStr)
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToStatusWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[0])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToFilesWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[1])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToBranchesWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[2])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToCommitsWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[3])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToStashWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[4])
|
||||
}
|
||||
|
||||
func (self *Input) Type(content string) {
|
||||
for _, char := range content {
|
||||
self.pressKey(string(char))
|
||||
}
|
||||
}
|
||||
|
||||
// i.e. pressing enter
|
||||
func (self *Input) Confirm() {
|
||||
self.pressKey(self.keys.Universal.Confirm)
|
||||
}
|
||||
|
||||
// i.e. pressing escape
|
||||
func (self *Input) Cancel() {
|
||||
self.pressKey(self.keys.Universal.Return)
|
||||
}
|
||||
|
||||
// i.e. pressing space
|
||||
func (self *Input) PrimaryAction() {
|
||||
self.pressKey(self.keys.Universal.Select)
|
||||
}
|
||||
|
||||
// i.e. pressing down arrow
|
||||
func (self *Input) NextItem() {
|
||||
self.pressKey(self.keys.Universal.NextItem)
|
||||
}
|
||||
|
||||
// i.e. pressing up arrow
|
||||
func (self *Input) PreviousItem() {
|
||||
self.pressKey(self.keys.Universal.PrevItem)
|
||||
}
|
||||
|
||||
func (self *Input) ContinueMerge() {
|
||||
self.PressKeys(self.keys.Universal.CreateRebaseOptionsMenu)
|
||||
self.assert.MatchSelectedLine(Contains("continue"))
|
||||
self.Confirm()
|
||||
}
|
||||
|
||||
func (self *Input) ContinueRebase() {
|
||||
self.ContinueMerge()
|
||||
}
|
||||
|
||||
// for when you want to allow lazygit to process something before continuing
|
||||
func (self *Input) Wait(milliseconds int) {
|
||||
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
|
||||
}
|
||||
|
||||
func (self *Input) LogUI(message string) {
|
||||
self.gui.LogUI(message)
|
||||
}
|
||||
|
||||
func (self *Input) 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 *Input) NavigateToListItemContainingText(text string) {
|
||||
self.assert.InListContext()
|
||||
|
||||
currentContext := self.gui.CurrentContext().(types.IListContext)
|
||||
|
||||
view := currentContext.GetView()
|
||||
|
||||
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
|
||||
matchCount := 0
|
||||
matchIndex := -1
|
||||
for i, line := range view.ViewBufferLines() {
|
||||
if strings.Contains(line, text) {
|
||||
matchCount++
|
||||
matchIndex = i
|
||||
}
|
||||
}
|
||||
if matchCount > 1 {
|
||||
self.assert.Fail(fmt.Sprintf("Found %d matches for %s, expected only a single match", matchCount, text))
|
||||
}
|
||||
if matchCount == 1 {
|
||||
selectedLineIdx := view.SelectedLineIdx()
|
||||
if selectedLineIdx == matchIndex {
|
||||
return
|
||||
}
|
||||
if selectedLineIdx < matchIndex {
|
||||
for i := selectedLineIdx; i < matchIndex; i++ {
|
||||
self.NextItem()
|
||||
}
|
||||
return
|
||||
} else {
|
||||
for i := selectedLineIdx; i > matchIndex; i-- {
|
||||
self.PreviousItem()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.assert.Fail(fmt.Sprintf("Could not find item containing text: %s", text))
|
||||
}
|
43
pkg/integration/components/paths.go
Normal file
43
pkg/integration/components/paths.go
Normal file
@ -0,0 +1,43 @@
|
||||
package components
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// convenience struct for easily getting directories within our test directory.
|
||||
// We have one test directory for each test, found in test/integration_new.
|
||||
type Paths struct {
|
||||
// e.g. test/integration/test_name
|
||||
root string
|
||||
}
|
||||
|
||||
func NewPaths(root string) Paths {
|
||||
return Paths{root: root}
|
||||
}
|
||||
|
||||
// when a test first runs, it's situated in a repo called 'repo' within this
|
||||
// directory. In its setup step, the test is allowed to create other repos
|
||||
// alongside the 'repo' repo in this directory, for example, creating remotes
|
||||
// or repos to add as submodules.
|
||||
func (self Paths) Actual() string {
|
||||
return filepath.Join(self.root, "actual")
|
||||
}
|
||||
|
||||
// this is the 'repo' directory within the 'actual' directory,
|
||||
// where a lazygit test will start within.
|
||||
func (self Paths) ActualRepo() string {
|
||||
return filepath.Join(self.Actual(), "repo")
|
||||
}
|
||||
|
||||
// When an integration test first runs, we copy everything in the 'actual' directory,
|
||||
// and copy it into the 'expected' directory so that future runs can be compared
|
||||
// against what we expect.
|
||||
func (self Paths) Expected() string {
|
||||
return filepath.Join(self.root, "expected")
|
||||
}
|
||||
|
||||
func (self Paths) Config() string {
|
||||
return filepath.Join(self.root, "used_config")
|
||||
}
|
||||
|
||||
func (self Paths) Root() string {
|
||||
return self.root
|
||||
}
|
216
pkg/integration/components/runner.go
Normal file
216
pkg/integration/components/runner.go
Normal file
@ -0,0 +1,216 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// this is the integration runner for the new and improved integration interface
|
||||
|
||||
const (
|
||||
TEST_NAME_ENV_VAR = "TEST_NAME"
|
||||
SANDBOX_ENV_VAR = "SANDBOX"
|
||||
)
|
||||
|
||||
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 Mode = 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
|
||||
)
|
||||
|
||||
func RunTests(
|
||||
tests []*IntegrationTest,
|
||||
logf func(format string, formatArgs ...interface{}),
|
||||
runCmd func(cmd *exec.Cmd) error,
|
||||
testWrapper func(test *IntegrationTest, f func() error),
|
||||
mode Mode,
|
||||
keyPressDelay int,
|
||||
) error {
|
||||
projectRootDir := utils.GetLazygitRootDirectory()
|
||||
err := os.Chdir(projectRootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testDir := filepath.Join(projectRootDir, "test", "integration_new")
|
||||
|
||||
if err := buildLazygit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
paths := NewPaths(
|
||||
filepath.Join(testDir, test.Name()),
|
||||
)
|
||||
|
||||
testWrapper(test, func() error { //nolint: thelper
|
||||
return runTest(test, paths, projectRootDir, logf, runCmd, mode, keyPressDelay)
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runTest(
|
||||
test *IntegrationTest,
|
||||
paths Paths,
|
||||
projectRootDir string,
|
||||
logf func(format string, formatArgs ...interface{}),
|
||||
runCmd func(cmd *exec.Cmd) error,
|
||||
mode Mode,
|
||||
keyPressDelay int,
|
||||
) error {
|
||||
if test.Skip() {
|
||||
logf("Skipping test %s", test.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
logf("path: %s", paths.Root())
|
||||
|
||||
if err := prepareTestDir(test, paths); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := getLazygitCommand(test, paths, projectRootDir, mode, keyPressDelay)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = runCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return HandleSnapshots(paths, logf, test, mode)
|
||||
}
|
||||
|
||||
func prepareTestDir(
|
||||
test *IntegrationTest,
|
||||
paths Paths,
|
||||
) error {
|
||||
findOrCreateDir(paths.Root())
|
||||
deleteAndRecreateEmptyDir(paths.Actual())
|
||||
|
||||
err := os.Mkdir(paths.ActualRepo(), 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return createFixture(test, paths)
|
||||
}
|
||||
|
||||
func buildLazygit() error {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
return osCommand.Cmd.New(fmt.Sprintf(
|
||||
"go build -o %s pkg/integration/clients/injector/main.go", tempLazygitPath(),
|
||||
)).Run()
|
||||
}
|
||||
|
||||
func createFixture(test *IntegrationTest, paths Paths) error {
|
||||
originalDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chdir(paths.ActualRepo()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
shell := NewShell()
|
||||
shell.RunCommand("git init")
|
||||
shell.RunCommand(`git config user.email "CI@example.com"`)
|
||||
shell.RunCommand(`git config user.name "CI"`)
|
||||
|
||||
test.SetupRepo(shell)
|
||||
|
||||
if err := os.Chdir(originalDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, mode Mode, keyPressDelay int) (*exec.Cmd, error) {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
|
||||
|
||||
err := os.RemoveAll(paths.Config())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = oscommands.CopyDir(templateConfigDir, paths.Config())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("%s -debug --use-config-dir=%s --path=%s %s", tempLazygitPath(), paths.Config(), paths.ActualRepo(), test.ExtraCmdArgs())
|
||||
|
||||
cmdObj := osCommand.Cmd.New(cmdStr)
|
||||
|
||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", TEST_NAME_ENV_VAR, test.Name()))
|
||||
if mode == SANDBOX {
|
||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", "SANDBOX", "true"))
|
||||
}
|
||||
|
||||
if keyPressDelay > 0 {
|
||||
cmdObj.AddEnvVars(fmt.Sprintf("KEY_PRESS_DELAY=%d", keyPressDelay))
|
||||
}
|
||||
|
||||
return cmdObj.GetCmd(), nil
|
||||
}
|
||||
|
||||
func tempLazygitPath() string {
|
||||
return filepath.Join("/tmp", "lazygit", "test_lazygit")
|
||||
}
|
||||
|
||||
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 deleteAndRecreateEmptyDir(path string) {
|
||||
// remove contents of integration test directory
|
||||
dir, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Mkdir(path, 0o777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for _, d := range dir {
|
||||
os.RemoveAll(filepath.Join(path, d.Name()))
|
||||
}
|
||||
}
|
83
pkg/integration/components/shell.go
Normal file
83
pkg/integration/components/shell.go
Normal file
@ -0,0 +1,83 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// 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{}
|
||||
|
||||
func NewShell() *Shell {
|
||||
return &Shell{}
|
||||
}
|
||||
|
||||
func (s *Shell) RunCommand(cmdStr string) *Shell {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := secureexec.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error running command: %s\n%s", cmdStr, string(output)))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Shell) NewBranch(name string) *Shell {
|
||||
return s.RunCommand("git checkout -b " + name)
|
||||
}
|
||||
|
||||
func (s *Shell) GitAdd(path string) *Shell {
|
||||
return s.RunCommand(fmt.Sprintf("git add \"%s\"", path))
|
||||
}
|
||||
|
||||
func (s *Shell) GitAddAll() *Shell {
|
||||
return s.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
func (s *Shell) Commit(message string) *Shell {
|
||||
return s.RunCommand(fmt.Sprintf("git commit -m \"%s\"", message))
|
||||
}
|
||||
|
||||
func (s *Shell) EmptyCommit(message string) *Shell {
|
||||
return s.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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),
|
||||
fmt.Sprintf("file%02d content", i),
|
||||
).
|
||||
Commit(fmt.Sprintf("commit %02d", i))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
372
pkg/integration/components/snapshot.go
Normal file
372
pkg/integration/components/snapshot.go
Normal file
@ -0,0 +1,372 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// This creates and compares integration test snapshots.
|
||||
|
||||
type (
|
||||
logf func(format string, formatArgs ...interface{})
|
||||
)
|
||||
|
||||
func HandleSnapshots(paths Paths, logf logf, test *IntegrationTest, mode Mode) error {
|
||||
return NewSnapshotter(paths, logf, test, mode).
|
||||
handleSnapshots()
|
||||
}
|
||||
|
||||
type Snapshotter struct {
|
||||
paths Paths
|
||||
logf logf
|
||||
test *IntegrationTest
|
||||
mode Mode
|
||||
}
|
||||
|
||||
func NewSnapshotter(
|
||||
paths Paths,
|
||||
logf logf,
|
||||
test *IntegrationTest,
|
||||
mode Mode,
|
||||
) *Snapshotter {
|
||||
return &Snapshotter{
|
||||
paths: paths,
|
||||
logf: logf,
|
||||
test: test,
|
||||
mode: mode,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Snapshotter) handleSnapshots() error {
|
||||
switch self.mode {
|
||||
case UPDATE_SNAPSHOT:
|
||||
return self.handleUpdate()
|
||||
case CHECK_SNAPSHOT:
|
||||
return self.handleCheck()
|
||||
case ASK_TO_UPDATE_SNAPSHOT:
|
||||
return self.handleAskToUpdate()
|
||||
case SANDBOX:
|
||||
self.logf("Sandbox session exited")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Snapshotter) handleUpdate() error {
|
||||
if err := self.updateSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.logf("Test passed: %s", self.test.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Snapshotter) handleCheck() error {
|
||||
self.logf("Comparing snapshots")
|
||||
if err := self.compareSnapshots(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.logf("Test passed: %s", self.test.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Snapshotter) handleAskToUpdate() error {
|
||||
if _, err := os.Stat(self.paths.Expected()); os.IsNotExist(err) {
|
||||
if err := self.updateSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.logf("No existing snapshot found for %s. Created snapshot.", self.test.Name())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
self.logf("Comparing snapshots...")
|
||||
if err := self.compareSnapshots(); err != nil {
|
||||
self.logf("%s", err)
|
||||
|
||||
// prompt user whether to update the snapshot (Y/N)
|
||||
if promptUserToUpdateSnapshot() {
|
||||
if err := self.updateSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.logf("Snapshot updated: %s", self.test.Name())
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
self.logf("Test passed: %s", self.test.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Snapshotter) updateSnapshot() error {
|
||||
// create/update snapshot
|
||||
err := oscommands.CopyDir(self.paths.Actual(), self.paths.Expected())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := renameSpecialPaths(self.paths.Expected()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Snapshotter) compareSnapshots() 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", self.test.Name())
|
||||
err := oscommands.CopyDir(self.paths.Expected(), 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, self.paths.Actual())
|
||||
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(self.paths.Actual(), 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(self.paths.Config(), "development.log"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.logf("%s", string(bytes))
|
||||
|
||||
return errors.New(getDiff(f.Name(), actualRepo, expectedRepo))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptUserToUpdateSnapshot() bool {
|
||||
fmt.Println("Test failed. Update snapshot? (y/n)")
|
||||
var input string
|
||||
fmt.Scanln(&input)
|
||||
return input == "y"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var actualFiles []os.FileInfo
|
||||
actualFiles, err = ioutil.ReadDir(actualDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
func getFileName(f os.FileInfo) string {
|
||||
return f.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...)
|
||||
}
|
133
pkg/integration/components/test.go
Normal file
133
pkg/integration/components/test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Test describes an integration tests that will be run against the lazygit gui.
|
||||
|
||||
// our unit tests will use this description to avoid a panic caused by attempting
|
||||
// to get the test's name via it's file's path.
|
||||
const unitTestDescription = "test test"
|
||||
|
||||
type IntegrationTest struct {
|
||||
name string
|
||||
description string
|
||||
extraCmdArgs string
|
||||
skip bool
|
||||
setupRepo func(shell *Shell)
|
||||
setupConfig func(config *config.AppConfig)
|
||||
run func(
|
||||
shell *Shell,
|
||||
input *Input,
|
||||
assert *Assert,
|
||||
keys config.KeybindingConfig,
|
||||
)
|
||||
}
|
||||
|
||||
var _ integrationTypes.IntegrationTest = &IntegrationTest{}
|
||||
|
||||
type NewIntegrationTestArgs struct {
|
||||
// Briefly describes what happens in the test and what it's testing for
|
||||
Description string
|
||||
// prepares a repo for testing
|
||||
SetupRepo func(shell *Shell)
|
||||
// takes a config and mutates. The mutated context will end up being passed to the gui
|
||||
SetupConfig func(config *config.AppConfig)
|
||||
// runs the test
|
||||
Run func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig)
|
||||
// additional args passed to lazygit
|
||||
ExtraCmdArgs string
|
||||
// for when a test is flakey
|
||||
Skip bool
|
||||
}
|
||||
|
||||
func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
|
||||
name := ""
|
||||
if args.Description != unitTestDescription {
|
||||
// this panics if we're in a unit test for our integration tests,
|
||||
// so we're using "test test" as a sentinel value
|
||||
name = testNameFromCurrentFilePath()
|
||||
}
|
||||
|
||||
return &IntegrationTest{
|
||||
name: name,
|
||||
description: args.Description,
|
||||
extraCmdArgs: args.ExtraCmdArgs,
|
||||
skip: args.Skip,
|
||||
setupRepo: args.SetupRepo,
|
||||
setupConfig: args.SetupConfig,
|
||||
run: args.Run,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) Name() string {
|
||||
return self.name
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) Description() string {
|
||||
return self.description
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) ExtraCmdArgs() string {
|
||||
return self.extraCmdArgs
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) Skip() bool {
|
||||
return self.skip
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) SetupConfig(config *config.AppConfig) {
|
||||
self.setupConfig(config)
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) 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 *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
|
||||
shell := NewShell()
|
||||
assert := NewAssert(gui)
|
||||
keys := gui.Keys()
|
||||
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 testNameFromCurrentFilePath() string {
|
||||
path := utils.FilePath(3)
|
||||
return TestNameFromFilePath(path)
|
||||
}
|
||||
|
||||
func TestNameFromFilePath(path string) string {
|
||||
name := strings.Split(path, "integration/tests/")[1]
|
||||
|
||||
return name[:len(name)-len(".go")]
|
||||
}
|
||||
|
||||
// this is the delay in milliseconds between keypresses
|
||||
// defaults to zero
|
||||
func KeyPressDelay() int {
|
||||
delayStr := os.Getenv("KEY_PRESS_DELAY")
|
||||
if delayStr == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
delay, err := strconv.Atoi(delayStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return delay
|
||||
}
|
99
pkg/integration/components/test_test.go
Normal file
99
pkg/integration/components/test_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type fakeGuiDriver struct {
|
||||
failureMessage string
|
||||
pressedKeys []string
|
||||
}
|
||||
|
||||
var _ integrationTypes.GuiDriver = &fakeGuiDriver{}
|
||||
|
||||
func (self *fakeGuiDriver) PressKey(key string) {
|
||||
self.pressedKeys = append(self.pressedKeys, key)
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Keys() config.KeybindingConfig {
|
||||
return config.KeybindingConfig{}
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) CurrentContext() types.Context {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Model() *types.Model {
|
||||
return &types.Model{Commits: []*models.Commit{}}
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Fail(message string) {
|
||||
self.failureMessage = message
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Log(message string) {
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) LogUI(message string) {
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) CheckedOutRef() *models.Branch {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) MainView() *gocui.View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) SecondaryView() *gocui.View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAssertionFailure(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
input.PressKeys("a")
|
||||
input.PressKeys("b")
|
||||
assert.CommitCount(2)
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
test.Run(driver)
|
||||
assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys)
|
||||
assert.Equal(t, "Expected 2 commits present, but got 0", driver.failureMessage)
|
||||
}
|
||||
|
||||
func TestManualFailure(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
assert.Fail("blah")
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
test.Run(driver)
|
||||
assert.Equal(t, "blah", driver.failureMessage)
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
input.PressKeys("a")
|
||||
input.PressKeys("b")
|
||||
assert.CommitCount(0)
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
test.Run(driver)
|
||||
assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys)
|
||||
assert.Equal(t, "", driver.failureMessage)
|
||||
}
|
@ -7,11 +7,13 @@ import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// see docs/Integration_Tests.md
|
||||
// Deprecated: This file is part of the old way of doing things.
|
||||
|
||||
// see https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md
|
||||
// This file can be invoked directly, but you might find it easier to go through
|
||||
// test/lazyintegration/main.go, which provides a convenient gui wrapper to integration tests.
|
||||
//
|
||||
@ -20,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.IntegrationTest, f func(*testing.T) error) {
|
||||
if selectedTestName != "" && test.Name != selectedTestName {
|
||||
return
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
||||
// this program lets you manage integration tests in a TUI. See docs/Integration_Tests.md for more info.
|
||||
// Deprecated. See lazy_integration for the new approach.
|
||||
|
||||
// this program lets you manage integration tests in a TUI. See https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md for more info.
|
||||
|
||||
type App struct {
|
||||
tests []*integration.Test
|
||||
tests []*deprecated.IntegrationTest
|
||||
itemIdx int
|
||||
testDir string
|
||||
editing bool
|
||||
g *gocui.Gui
|
||||
}
|
||||
|
||||
func (app *App) getCurrentTest() *integration.Test {
|
||||
func (app *App) getCurrentTest() *deprecated.IntegrationTest {
|
||||
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}
|
||||
@ -106,7 +108,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=record go run test/runner/main.go %s", currentTest.Name))
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=record go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
|
||||
app.runSubprocess(cmd)
|
||||
|
||||
return nil
|
||||
@ -120,7 +122,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=sandbox go run test/runner/main.go %s", currentTest.Name))
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=sandbox go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
|
||||
app.runSubprocess(cmd)
|
||||
|
||||
return nil
|
||||
@ -134,7 +136,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true go run test/runner/main.go %s", currentTest.Name))
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
|
||||
app.runSubprocess(cmd)
|
||||
|
||||
return nil
|
||||
@ -148,7 +150,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=updateSnapshot go run test/runner/main.go %s", currentTest.Name))
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=updateSnapshot go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
|
||||
app.runSubprocess(cmd)
|
||||
|
||||
return nil
|
||||
@ -162,7 +164,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true SPEED=1 go run test/runner/main.go %s", currentTest.Name))
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true SPEED=1 go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
|
||||
app.runSubprocess(cmd)
|
||||
|
||||
return nil
|
@ -1,7 +1,7 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package gui
|
||||
package deprecated
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -13,10 +13,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Deprecated.
|
||||
|
||||
// This file is quite similar to integration/main.go. The main difference is that this file is
|
||||
// run via `go test` whereas the other is run via `test/lazyintegration/main.go` which provides
|
||||
// a convenient gui wrapper around our integration tests. The `go test` approach is better
|
||||
@ -25,10 +26,10 @@ import (
|
||||
// you'll need to take the other approach
|
||||
//
|
||||
// As for this file, to run an integration test, e.g. for test 'commit', go:
|
||||
// go test pkg/gui/gui_test.go -run /commit
|
||||
// go test pkg/gui/old_gui_test.go -run /commit
|
||||
//
|
||||
// To update a snapshot for an integration test, pass UPDATE_SNAPSHOTS=true
|
||||
// UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /commit
|
||||
// UPDATE_SNAPSHOTS=true go test pkg/gui/old_gui_test.go -run /commit
|
||||
//
|
||||
// integration tests are run in test/integration/<test_name>/actual and the final test does
|
||||
// not clean up that directory so you can cd into it to see for yourself what
|
||||
@ -44,7 +45,7 @@ func Test(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") != ""
|
||||
|
||||
@ -52,10 +53,10 @@ func Test(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 *IntegrationTest, f func(*testing.T) error) {
|
||||
defer func() { testNumber += 1 }()
|
||||
if testNumber%parallelTotal != parallelIndex {
|
||||
return
|
||||
@ -78,6 +79,15 @@ func Test(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,
|
||||
@ -94,12 +104,3 @@ func runCmdHeadless(cmd *exec.Cmd) error {
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func tryConvert(numStr string, defaultVal int) int {
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
return num
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package integration
|
||||
package deprecated
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@ -18,9 +18,11 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
)
|
||||
|
||||
// This package is for running our integration test suite. See docs/Integration_Tests.md for more info
|
||||
// Deprecated: This file is part of the old way of doing things. See pkg/integration/integration.go for the new way
|
||||
|
||||
type Test struct {
|
||||
// This package is for running our integration test suite. See https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md for more info.
|
||||
|
||||
type IntegrationTest struct {
|
||||
Name string `json:"name"`
|
||||
Speed float64 `json:"speed"`
|
||||
Description string `json:"description"`
|
||||
@ -53,7 +55,7 @@ func GetModeFromEnv() Mode {
|
||||
case "sandbox":
|
||||
return SANDBOX
|
||||
default:
|
||||
log.Fatalf("unknown test mode: %s, must be one of [test, record, update, sandbox]", os.Getenv("MODE"))
|
||||
log.Fatalf("unknown test mode: %s, must be one of [test, record, updateSnapshot, sandbox]", os.Getenv("MODE"))
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
@ -64,7 +66,7 @@ func GetModeFromEnv() Mode {
|
||||
func RunTests(
|
||||
logf func(format string, formatArgs ...interface{}),
|
||||
runCmd func(cmd *exec.Cmd) error,
|
||||
fnWrapper func(test *Test, f func(*testing.T) error),
|
||||
fnWrapper func(test *IntegrationTest, f func(*testing.T) error),
|
||||
mode Mode,
|
||||
speedEnv string,
|
||||
onFail func(t *testing.T, expected string, actual string, prefix string),
|
||||
@ -313,13 +315,13 @@ func getTestSpeeds(testStartSpeed float64, mode Mode, speedStr string) []float64
|
||||
return speeds
|
||||
}
|
||||
|
||||
func LoadTests(testDir string) ([]*Test, error) {
|
||||
func LoadTests(testDir string) ([]*IntegrationTest, error) {
|
||||
paths, err := filepath.Glob(filepath.Join(testDir, "/*/test.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tests := make([]*Test, len(paths))
|
||||
tests := make([]*IntegrationTest, len(paths))
|
||||
|
||||
for i, path := range paths {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
@ -327,7 +329,7 @@ func LoadTests(testDir string) ([]*Test, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
test := &Test{}
|
||||
test := &IntegrationTest{}
|
||||
|
||||
err = json.Unmarshal(data, test)
|
||||
if err != nil {
|
41
pkg/integration/tests/branch/suggestions.go
Normal file
41
pkg/integration/tests/branch/suggestions.go
Normal file
@ -0,0 +1,41 @@
|
||||
package branch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Suggestions = components.NewIntegrationTest(components.NewIntegrationTestArgs{
|
||||
Description: "Checking out a branch with name suggestions",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *components.Shell) {
|
||||
shell.
|
||||
EmptyCommit("my commit message").
|
||||
NewBranch("new-branch").
|
||||
NewBranch("new-branch-2").
|
||||
NewBranch("new-branch-3").
|
||||
NewBranch("branch-to-checkout").
|
||||
NewBranch("other-new-branch-2").
|
||||
NewBranch("other-new-branch-3")
|
||||
},
|
||||
Run: func(shell *components.Shell, input *components.Input, assert *components.Assert, keys config.KeybindingConfig) {
|
||||
input.SwitchToBranchesWindow()
|
||||
assert.CurrentViewName("localBranches")
|
||||
|
||||
input.PressKeys(keys.Branches.CheckoutBranchByName)
|
||||
assert.CurrentViewName("confirmation")
|
||||
|
||||
input.Type("branch-to")
|
||||
|
||||
input.PressKeys(keys.Universal.TogglePanel)
|
||||
assert.CurrentViewName("suggestions")
|
||||
|
||||
// we expect the first suggestion to be the branch we want because it most
|
||||
// closely matches what we typed in
|
||||
input.Confirm()
|
||||
|
||||
assert.CurrentBranchName("branch-to-checkout")
|
||||
},
|
||||
})
|
32
pkg/integration/tests/commit/commit.go
Normal file
32
pkg/integration/tests/commit/commit.go
Normal file
@ -0,0 +1,32 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Commit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Staging a couple files and committing",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFile("myfile", "myfile content")
|
||||
shell.CreateFile("myfile2", "myfile2 content")
|
||||
},
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
assert.CommitCount(0)
|
||||
|
||||
input.PrimaryAction()
|
||||
input.NextItem()
|
||||
input.PrimaryAction()
|
||||
input.PressKeys(keys.Files.CommitChanges)
|
||||
|
||||
commitMessage := "my commit message"
|
||||
input.Type(commitMessage)
|
||||
input.Confirm()
|
||||
|
||||
assert.CommitCount(1)
|
||||
assert.MatchHeadCommitMessage(Equals(commitMessage))
|
||||
},
|
||||
})
|
38
pkg/integration/tests/commit/new_branch.go
Normal file
38
pkg/integration/tests/commit/new_branch.go
Normal file
@ -0,0 +1,38 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var NewBranch = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Creating a new branch from a commit",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
EmptyCommit("commit 1").
|
||||
EmptyCommit("commit 2").
|
||||
EmptyCommit("commit 3")
|
||||
},
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
assert.CommitCount(3)
|
||||
|
||||
input.SwitchToCommitsWindow()
|
||||
assert.CurrentViewName("commits")
|
||||
input.NextItem()
|
||||
|
||||
input.PressKeys(keys.Universal.New)
|
||||
|
||||
assert.CurrentViewName("confirmation")
|
||||
|
||||
branchName := "my-branch-name"
|
||||
input.Type(branchName)
|
||||
input.Confirm()
|
||||
|
||||
assert.CommitCount(2)
|
||||
assert.MatchHeadCommitMessage(Contains("commit 2"))
|
||||
assert.CurrentBranchName(branchName)
|
||||
},
|
||||
})
|
36
pkg/integration/tests/custom_commands/basic.go
Normal file
36
pkg/integration/tests/custom_commands/basic.go
Normal file
@ -0,0 +1,36 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Basic = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Using a custom command to create a new file",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("blah")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.UserConfig.CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "a",
|
||||
Context: "files",
|
||||
Command: "touch myfile",
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(
|
||||
shell *Shell,
|
||||
input *Input,
|
||||
assert *Assert,
|
||||
keys config.KeybindingConfig,
|
||||
) {
|
||||
assert.WorkingTreeFileCount(0)
|
||||
|
||||
input.PressKeys("a")
|
||||
assert.WorkingTreeFileCount(1)
|
||||
assert.MatchSelectedLine(Contains("myfile"))
|
||||
},
|
||||
})
|
74
pkg/integration/tests/custom_commands/menu_from_command.go
Normal file
74
pkg/integration/tests/custom_commands/menu_from_command.go
Normal file
@ -0,0 +1,74 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// NOTE: we're getting a weird offset in the popup prompt for some reason. Not sure what's behind that.
|
||||
|
||||
var MenuFromCommand = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Using menuFromCommand prompt type",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
EmptyCommit("foo").
|
||||
EmptyCommit("bar").
|
||||
EmptyCommit("baz").
|
||||
NewBranch("feature/foo")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.UserConfig.CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "a",
|
||||
Context: "localBranches",
|
||||
Command: `echo "{{index .PromptResponses 0}} {{index .PromptResponses 1}} {{ .SelectedLocalBranch.Name }}" > output.txt`,
|
||||
Prompts: []config.CustomCommandPrompt{
|
||||
{
|
||||
Type: "menuFromCommand",
|
||||
Title: "Choose commit message",
|
||||
Command: `git log --oneline --pretty=%B`,
|
||||
Filter: `(?P<commit_message>.*)`,
|
||||
ValueFormat: `{{ .commit_message }}`,
|
||||
LabelFormat: `{{ .commit_message | yellow }}`,
|
||||
},
|
||||
{
|
||||
Type: "input",
|
||||
Title: "Description",
|
||||
InitialValue: `{{ if .SelectedLocalBranch.Name }}Branch: #{{ .SelectedLocalBranch.Name }}{{end}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(
|
||||
shell *Shell,
|
||||
input *Input,
|
||||
assert *Assert,
|
||||
keys config.KeybindingConfig,
|
||||
) {
|
||||
assert.WorkingTreeFileCount(0)
|
||||
input.SwitchToBranchesWindow()
|
||||
|
||||
input.PressKeys("a")
|
||||
|
||||
assert.InMenu()
|
||||
assert.MatchCurrentViewTitle(Equals("Choose commit message"))
|
||||
assert.MatchSelectedLine(Equals("baz"))
|
||||
input.NextItem()
|
||||
assert.MatchSelectedLine(Equals("bar"))
|
||||
input.Confirm()
|
||||
|
||||
assert.InPrompt()
|
||||
assert.MatchCurrentViewTitle(Equals("Description"))
|
||||
input.Type(" my branch")
|
||||
input.Confirm()
|
||||
|
||||
input.SwitchToFilesWindow()
|
||||
|
||||
assert.WorkingTreeFileCount(1)
|
||||
assert.MatchSelectedLine(Contains("output.txt"))
|
||||
assert.MatchMainViewContent(Contains("bar Branch: #feature/foo my branch feature/foo"))
|
||||
},
|
||||
})
|
86
pkg/integration/tests/custom_commands/multiple_prompts.go
Normal file
86
pkg/integration/tests/custom_commands/multiple_prompts.go
Normal file
@ -0,0 +1,86 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var MultiplePrompts = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Using a custom command with multiple prompts",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("blah")
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.UserConfig.CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "a",
|
||||
Context: "files",
|
||||
Command: `echo "{{index .PromptResponses 1}}" > {{index .PromptResponses 0}}`,
|
||||
Prompts: []config.CustomCommandPrompt{
|
||||
{
|
||||
Type: "input",
|
||||
Title: "Enter a file name",
|
||||
},
|
||||
{
|
||||
Type: "menu",
|
||||
Title: "Choose file content",
|
||||
Options: []config.CustomCommandMenuOption{
|
||||
{
|
||||
Name: "foo",
|
||||
Description: "Foo",
|
||||
Value: "FOO",
|
||||
},
|
||||
{
|
||||
Name: "bar",
|
||||
Description: "Bar",
|
||||
Value: "BAR",
|
||||
},
|
||||
{
|
||||
Name: "baz",
|
||||
Description: "Baz",
|
||||
Value: "BAZ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "confirm",
|
||||
Title: "Are you sure?",
|
||||
Body: "Are you REALLY sure you want to make this file? Up to you buddy.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(
|
||||
shell *Shell,
|
||||
input *Input,
|
||||
assert *Assert,
|
||||
keys config.KeybindingConfig,
|
||||
) {
|
||||
assert.WorkingTreeFileCount(0)
|
||||
|
||||
input.PressKeys("a")
|
||||
|
||||
assert.InPrompt()
|
||||
assert.MatchCurrentViewTitle(Equals("Enter a file name"))
|
||||
input.Type("myfile")
|
||||
input.Confirm()
|
||||
|
||||
assert.InMenu()
|
||||
assert.MatchCurrentViewTitle(Equals("Choose file content"))
|
||||
assert.MatchSelectedLine(Contains("foo"))
|
||||
input.NextItem()
|
||||
assert.MatchSelectedLine(Contains("bar"))
|
||||
input.Confirm()
|
||||
|
||||
assert.InConfirm()
|
||||
assert.MatchCurrentViewTitle(Equals("Are you sure?"))
|
||||
input.Confirm()
|
||||
|
||||
assert.WorkingTreeFileCount(1)
|
||||
assert.MatchSelectedLine(Contains("myfile"))
|
||||
assert.MatchMainViewContent(Contains("BAR"))
|
||||
},
|
||||
})
|
41
pkg/integration/tests/interactive_rebase/one.go
Normal file
41
pkg/integration/tests/interactive_rebase/one.go
Normal file
@ -0,0 +1,41 @@
|
||||
package interactive_rebase
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var One = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Begins an interactive rebase, then fixups, drops, and squashes some commits",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
CreateNCommits(5) // these will appears at commit 05, 04, 04, down to 01
|
||||
},
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
input.SwitchToCommitsWindow()
|
||||
assert.CurrentViewName("commits")
|
||||
|
||||
input.NavigateToListItemContainingText("commit 02")
|
||||
input.PressKeys(keys.Universal.Edit)
|
||||
assert.MatchSelectedLine(Contains("YOU ARE HERE"))
|
||||
|
||||
input.PreviousItem()
|
||||
input.PressKeys(keys.Commits.MarkCommitAsFixup)
|
||||
assert.MatchSelectedLine(Contains("fixup"))
|
||||
|
||||
input.PreviousItem()
|
||||
input.PressKeys(keys.Universal.Remove)
|
||||
assert.MatchSelectedLine(Contains("drop"))
|
||||
|
||||
input.PreviousItem()
|
||||
input.PressKeys(keys.Commits.SquashDown)
|
||||
assert.MatchSelectedLine(Contains("squash"))
|
||||
|
||||
input.ContinueRebase()
|
||||
|
||||
assert.CommitCount(2)
|
||||
},
|
||||
})
|
74
pkg/integration/tests/tests.go
Normal file
74
pkg/integration/tests/tests.go
Normal file
@ -0,0 +1,74 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/branch"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/commit"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/custom_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// 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 = []*components.IntegrationTest{
|
||||
commit.Commit,
|
||||
commit.NewBranch,
|
||||
branch.Suggestions,
|
||||
interactive_rebase.One,
|
||||
custom_commands.Basic,
|
||||
custom_commands.MultiplePrompts,
|
||||
custom_commands.MenuFromCommand,
|
||||
}
|
||||
|
||||
func GetTests() []*components.IntegrationTest {
|
||||
// first we ensure that each test in this directory has actually been added to the above list.
|
||||
testCount := 0
|
||||
|
||||
testNamesSet := set.NewFromSlice(slices.Map(
|
||||
tests,
|
||||
func(test *components.IntegrationTest) string {
|
||||
return test.Name()
|
||||
},
|
||||
))
|
||||
|
||||
missingTestNames := []string{}
|
||||
|
||||
if err := filepath.Walk(filepath.Join(utils.GetLazygitRootDirectory(), "pkg/integration/tests"), func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && strings.HasSuffix(path, ".go") {
|
||||
// ignoring this current file
|
||||
if filepath.Base(path) == "tests.go" {
|
||||
return nil
|
||||
}
|
||||
|
||||
nameFromPath := components.TestNameFromFilePath(path)
|
||||
if !testNamesSet.Includes(nameFromPath) {
|
||||
missingTestNames = append(missingTestNames, nameFromPath)
|
||||
}
|
||||
testCount++
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("failed to walk tests: %v", err))
|
||||
}
|
||||
|
||||
if len(missingTestNames) > 0 {
|
||||
panic(fmt.Sprintf("The following tests are missing from the list of tests: %s. You need to add them to `pkg/integration/tests/tests.go`.", strings.Join(missingTestNames, ", ")))
|
||||
}
|
||||
|
||||
if testCount > len(tests) {
|
||||
panic("you have not added all of the tests to the tests list in `pkg/integration/tests/tests.go`")
|
||||
} else if testCount < len(tests) {
|
||||
panic("There are more tests in `pkg/integration/tests/tests.go` than there are test files in the tests directory. Ensure that you only have one test per file and you haven't included the same test twice in the tests list.")
|
||||
}
|
||||
|
||||
return tests
|
||||
}
|
37
pkg/integration/types/types.go
Normal file
37
pkg/integration/types/types.go
Normal file
@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// 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 IntegrationTest interface {
|
||||
Run(GuiDriver)
|
||||
SetupConfig(config *config.AppConfig)
|
||||
}
|
||||
|
||||
// this is the interface through which our integration tests interact with the lazygit gui
|
||||
type GuiDriver 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
|
||||
// the view that appears to the right of the side panel
|
||||
MainView() *gocui.View
|
||||
// the other view that sometimes appears to the right of the side panel
|
||||
// e.g. when we're showing both staged and unstaged changes
|
||||
SecondaryView() *gocui.View
|
||||
}
|
@ -128,3 +128,37 @@ func StackTrace() string {
|
||||
n := runtime.Stack(buf, false)
|
||||
return fmt.Sprintf("%s\n", buf[:n])
|
||||
}
|
||||
|
||||
// returns the path of the file that calls the function.
|
||||
// 'skip' is the number of stack frames to skip.
|
||||
func FilePath(skip int) string {
|
||||
_, path, _, _ := runtime.Caller(skip)
|
||||
return path
|
||||
}
|
||||
|
||||
// for our cheatsheet script and integration tests. Not to be confused with finding the
|
||||
// root directory of _any_ random repo.
|
||||
func GetLazygitRootDirectory() string {
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
_, err := os.Stat(filepath.Join(path, ".git"))
|
||||
|
||||
if err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path = filepath.Dir(path)
|
||||
|
||||
if path == "/" {
|
||||
log.Fatal("must run in lazygit folder or child folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# How to use:
|
||||
# 1) find a commit that is working fine.
|
||||
# 2) Create an integration test capturing the fact that it works (Don't commit it). See https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md
|
||||
# 2) Create an integration test capturing the fact that it works (Don't commit it). See https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md
|
||||
# 3) checkout the commit that's known to be failing
|
||||
# 4) run this script supplying the commit sha / tag name that works and the name of the newly created test
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
file0
|
@ -1 +0,0 @@
|
||||
ref: refs/heads/new-branch-3
|
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 commit (initial): file0
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from master to new-branch
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from new-branch to new-branch-2
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from new-branch-2 to new-branch-3
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from new-branch-3 to old-branch
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from old-branch to old-branch-2
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from old-branch-2 to old-branch-3
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675450 +1000 checkout: moving from old-branch-3 to new-branch-3
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 commit (initial): file0
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
Binary file not shown.
Binary file not shown.
@ -1,2 +0,0 @@
|
||||
xŤÍA
|
||||
Â0Fa×9Ĺ왉“Ä€�ĐUŹ‘4°Đ)<ľ=‚ŰÇoé�Dő4v€Wř�cŚ%‹>ř–«V¶ąTˇQŐšôŻľÓ4Ó}šźř¦öŢpYz{�x >8UGgafsÔc2đ'7uÝŔćÜď+ö
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
test0
|
@ -1 +0,0 @@
|
||||
{"KeyEvents":[{"Timestamp":639,"Mod":0,"Key":259,"Ch":0},{"Timestamp":1752,"Mod":0,"Key":256,"Ch":99},{"Timestamp":2183,"Mod":0,"Key":256,"Ch":110},{"Timestamp":2271,"Mod":0,"Key":256,"Ch":101},{"Timestamp":2327,"Mod":0,"Key":256,"Ch":119},{"Timestamp":2599,"Mod":0,"Key":256,"Ch":45},{"Timestamp":3583,"Mod":0,"Key":9,"Ch":9},{"Timestamp":3880,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4175,"Mod":0,"Key":13,"Ch":13},{"Timestamp":4815,"Mod":0,"Key":256,"Ch":113}],"ResizeEvents":[{"Timestamp":0,"Width":272,"Height":74}]}
|
@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cd $1
|
||||
|
||||
git init
|
||||
|
||||
git config user.email "CI@example.com"
|
||||
git config user.name "CI"
|
||||
|
||||
echo test0 > file0
|
||||
git add .
|
||||
git commit -am file0
|
||||
|
||||
git checkout -b new-branch
|
||||
git checkout -b new-branch-2
|
||||
git checkout -b new-branch-3
|
||||
git checkout -b old-branch
|
||||
git checkout -b old-branch-2
|
||||
git checkout -b old-branch-3
|
@ -1 +0,0 @@
|
||||
{ "description": "Checking out a branch with name suggestions", "speed": 100 }
|
@ -1 +0,0 @@
|
||||
commit
|
Binary file not shown.
@ -1,5 +0,0 @@
|
||||
0000000000000000000000000000000000000000 3df3d8761bc0f0828596b11845aeac175b7b7393 CI <CI@example.com> 1617671339 +1000 commit (initial): myfile1
|
||||
3df3d8761bc0f0828596b11845aeac175b7b7393 a7d53cc21fd53100f955377be379423b0e386274 CI <CI@example.com> 1617671339 +1000 commit: myfile2
|
||||
a7d53cc21fd53100f955377be379423b0e386274 4ba4f1ed711a9081fab21bc222469aa5176a01f8 CI <CI@example.com> 1617671339 +1000 commit: myfile3
|
||||
4ba4f1ed711a9081fab21bc222469aa5176a01f8 1440bc6cc888a09dca2329d1060eec6de78d9d21 CI <CI@example.com> 1617671339 +1000 commit: myfile4
|
||||
1440bc6cc888a09dca2329d1060eec6de78d9d21 e7560e2cd4783a261ad32496cefed2d9f69a46e7 CI <CI@example.com> 1617671342 +1000 commit: commit
|
@ -1,5 +0,0 @@
|
||||
0000000000000000000000000000000000000000 3df3d8761bc0f0828596b11845aeac175b7b7393 CI <CI@example.com> 1617671339 +1000 commit (initial): myfile1
|
||||
3df3d8761bc0f0828596b11845aeac175b7b7393 a7d53cc21fd53100f955377be379423b0e386274 CI <CI@example.com> 1617671339 +1000 commit: myfile2
|
||||
a7d53cc21fd53100f955377be379423b0e386274 4ba4f1ed711a9081fab21bc222469aa5176a01f8 CI <CI@example.com> 1617671339 +1000 commit: myfile3
|
||||
4ba4f1ed711a9081fab21bc222469aa5176a01f8 1440bc6cc888a09dca2329d1060eec6de78d9d21 CI <CI@example.com> 1617671339 +1000 commit: myfile4
|
||||
1440bc6cc888a09dca2329d1060eec6de78d9d21 e7560e2cd4783a261ad32496cefed2d9f69a46e7 CI <CI@example.com> 1617671342 +1000 commit: commit
|
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,3 +0,0 @@
|
||||
x�ÍA
|
||||
Â0@Q×9Åì™iÆI
|
||||
"BW=FšL°Ð!R"èííÜ~üÜÌÖÄrê»* J®˜d £ÆÂ¬¥DÕÀû"\S¾.½û³í0Íp›æ‡~’½6½äfw ¡ �¼áLˆèŽzLºþÉ�}ëº)¹2r,Ï
|
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user