mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-23 12:18:51 +02:00
Capture test code coverage stats (#3135)
This commit is contained in:
commit
f9b4bcde38
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@ -36,7 +36,14 @@ jobs:
|
|||||||
- name: Test code
|
- name: Test code
|
||||||
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
||||||
run: |
|
run: |
|
||||||
go test ./... -short
|
mkdir -p /tmp/code_coverage
|
||||||
|
go test ./... -short -cover -args "-test.gocoverdir=/tmp/code_coverage"
|
||||||
|
- name: Upload code coverage artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage-unit-${{ matrix.os }}-${{ github.run_id }}
|
||||||
|
path: /tmp/code_coverage
|
||||||
|
|
||||||
integration-tests:
|
integration-tests:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -86,8 +93,17 @@ jobs:
|
|||||||
- name: Print git version
|
- name: Print git version
|
||||||
run: git --version
|
run: git --version
|
||||||
- name: Test code
|
- name: Test code
|
||||||
|
env:
|
||||||
|
# See https://go.dev/blog/integration-test-coverage
|
||||||
|
LAZYGIT_GOCOVERDIR: /tmp/code_coverage
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p /tmp/code_coverage
|
||||||
./scripts/run_integration_tests.sh
|
./scripts/run_integration_tests.sh
|
||||||
|
- name: Upload code coverage artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: coverage-integration-${{ matrix.git-version }}-${{ github.run_id }}
|
||||||
|
path: /tmp/code_coverage
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
@ -169,3 +185,31 @@ jobs:
|
|||||||
mode: exactly
|
mode: exactly
|
||||||
count: 1
|
count: 1
|
||||||
labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n"
|
labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n"
|
||||||
|
upload-coverage:
|
||||||
|
# List all jobs that produce coverage files
|
||||||
|
needs: [unit-tests, integration-tests]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Download all coverage artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/code_coverage
|
||||||
|
|
||||||
|
- name: Combine coverage files
|
||||||
|
run: |
|
||||||
|
# Find all directories in /tmp/code_coverage and create a comma-separated list
|
||||||
|
COVERAGE_DIRS=$(find /tmp/code_coverage -mindepth 1 -maxdepth 1 -type d -printf '/tmp/code_coverage/%f,' | sed 's/,$//')
|
||||||
|
echo "Coverage directories: $COVERAGE_DIRS"
|
||||||
|
# Run the combine command with the generated list
|
||||||
|
go tool covdata textfmt -i=$COVERAGE_DIRS -o coverage.out
|
||||||
|
echo "Combined coverage:"
|
||||||
|
go tool cover -func coverage.out | tail -1 | awk '{print $3}'
|
||||||
|
|
||||||
|
- name: Upload to Codacy
|
||||||
|
run: |
|
||||||
|
CODACY_PROJECT_TOKEN=${{ secrets.CODACY_PROJECT_TOKEN }} \
|
||||||
|
bash <(curl -Ls https://coverage.codacy.com/get.sh) report \
|
||||||
|
--force-coverage-parser go -r coverage.out
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -42,3 +42,5 @@ __debug_bin
|
|||||||
|
|
||||||
.worktrees
|
.worktrees
|
||||||
demo/output/*
|
demo/output/*
|
||||||
|
|
||||||
|
coverage.out
|
||||||
|
@ -29,17 +29,18 @@ func RunCLI(testNames []string, slow bool, sandbox bool, waitForDebugger bool, r
|
|||||||
inputDelay = SLOW_INPUT_DELAY
|
inputDelay = SLOW_INPUT_DELAY
|
||||||
}
|
}
|
||||||
|
|
||||||
err := components.RunTests(
|
err := components.RunTests(components.RunTestArgs{
|
||||||
getTestsToRun(testNames),
|
Tests: getTestsToRun(testNames),
|
||||||
log.Printf,
|
Logf: log.Printf,
|
||||||
runCmdInTerminal,
|
RunCmd: runCmdInTerminal,
|
||||||
runAndPrintFatalError,
|
TestWrapper: runAndPrintFatalError,
|
||||||
sandbox,
|
Sandbox: sandbox,
|
||||||
waitForDebugger,
|
WaitForDebugger: waitForDebugger,
|
||||||
raceDetector,
|
RaceDetector: raceDetector,
|
||||||
inputDelay,
|
CodeCoverageDir: "",
|
||||||
1,
|
InputDelay: inputDelay,
|
||||||
)
|
MaxAttempts: 1,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err.Error())
|
log.Print(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -28,13 +28,17 @@ func TestIntegration(t *testing.T) {
|
|||||||
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
|
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
|
||||||
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
|
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
|
||||||
raceDetector := os.Getenv("LAZYGIT_RACE_DETECTOR") != ""
|
raceDetector := os.Getenv("LAZYGIT_RACE_DETECTOR") != ""
|
||||||
|
// LAZYGIT_GOCOVERDIR is the directory where we write coverage files to. If this directory
|
||||||
|
// is defined, go binaries built with the -cover flag will write coverage files to
|
||||||
|
// to it.
|
||||||
|
codeCoverageDir := os.Getenv("LAZYGIT_GOCOVERDIR")
|
||||||
testNumber := 0
|
testNumber := 0
|
||||||
|
|
||||||
err := components.RunTests(
|
err := components.RunTests(components.RunTestArgs{
|
||||||
tests.GetTests(),
|
Tests: tests.GetTests(),
|
||||||
t.Logf,
|
Logf: t.Logf,
|
||||||
runCmdHeadless,
|
RunCmd: runCmdHeadless,
|
||||||
func(test *components.IntegrationTest, f func() error) {
|
TestWrapper: func(test *components.IntegrationTest, f func() error) {
|
||||||
defer func() { testNumber += 1 }()
|
defer func() { testNumber += 1 }()
|
||||||
if testNumber%parallelTotal != parallelIndex {
|
if testNumber%parallelTotal != parallelIndex {
|
||||||
return
|
return
|
||||||
@ -52,13 +56,14 @@ func TestIntegration(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
false,
|
Sandbox: false,
|
||||||
false,
|
WaitForDebugger: false,
|
||||||
raceDetector,
|
RaceDetector: raceDetector,
|
||||||
0,
|
CodeCoverageDir: codeCoverageDir,
|
||||||
|
InputDelay: 0,
|
||||||
// Allow two attempts at each test to get around flakiness
|
// Allow two attempts at each test to get around flakiness
|
||||||
2,
|
MaxAttempts: 2,
|
||||||
)
|
})
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -385,17 +385,18 @@ func quit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
|
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
|
||||||
err := components.RunTests(
|
err := components.RunTests(components.RunTestArgs{
|
||||||
[]*components.IntegrationTest{test},
|
Tests: []*components.IntegrationTest{test},
|
||||||
log.Printf,
|
Logf: log.Printf,
|
||||||
runCmdInTerminal,
|
RunCmd: runCmdInTerminal,
|
||||||
runAndPrintError,
|
TestWrapper: runAndPrintError,
|
||||||
sandbox,
|
Sandbox: sandbox,
|
||||||
waitForDebugger,
|
WaitForDebugger: waitForDebugger,
|
||||||
raceDetector,
|
RaceDetector: raceDetector,
|
||||||
inputDelay,
|
CodeCoverageDir: "",
|
||||||
1,
|
InputDelay: inputDelay,
|
||||||
)
|
MaxAttempts: 1,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -20,21 +20,24 @@ const (
|
|||||||
GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL"
|
GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RunTestArgs struct {
|
||||||
|
Tests []*IntegrationTest
|
||||||
|
Logf func(format string, formatArgs ...interface{})
|
||||||
|
RunCmd func(cmd *exec.Cmd) (int, error)
|
||||||
|
TestWrapper func(test *IntegrationTest, f func() error)
|
||||||
|
Sandbox bool
|
||||||
|
WaitForDebugger bool
|
||||||
|
RaceDetector bool
|
||||||
|
CodeCoverageDir string
|
||||||
|
InputDelay int
|
||||||
|
MaxAttempts int
|
||||||
|
}
|
||||||
|
|
||||||
// This function lets you run tests either from within `go test` or from a regular binary.
|
// This function lets you run tests either from within `go test` or from a regular binary.
|
||||||
// The reason for having two separate ways of testing is that `go test` isn't great at
|
// The reason for having two separate ways of testing is that `go test` isn't great at
|
||||||
// showing what's actually happening during the test, but it's still good at running
|
// showing what's actually happening during the test, but it's still good at running
|
||||||
// tests in telling you about their results.
|
// tests in telling you about their results.
|
||||||
func RunTests(
|
func RunTests(args RunTestArgs) error {
|
||||||
tests []*IntegrationTest,
|
|
||||||
logf func(format string, formatArgs ...interface{}),
|
|
||||||
runCmd func(cmd *exec.Cmd) (int, error),
|
|
||||||
testWrapper func(test *IntegrationTest, f func() error),
|
|
||||||
sandbox bool,
|
|
||||||
waitForDebugger bool,
|
|
||||||
raceDetector bool,
|
|
||||||
inputDelay int,
|
|
||||||
maxAttempts int,
|
|
||||||
) error {
|
|
||||||
projectRootDir := lazycoreUtils.GetLazyRootDirectory()
|
projectRootDir := lazycoreUtils.GetLazyRootDirectory()
|
||||||
err := os.Chdir(projectRootDir)
|
err := os.Chdir(projectRootDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -42,8 +45,7 @@ func RunTests(
|
|||||||
}
|
}
|
||||||
|
|
||||||
testDir := filepath.Join(projectRootDir, "test", "_results")
|
testDir := filepath.Join(projectRootDir, "test", "_results")
|
||||||
|
if err := buildLazygit(args); err != nil {
|
||||||
if err := buildLazygit(waitForDebugger, raceDetector); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,21 +54,21 @@ func RunTests(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range args.Tests {
|
||||||
test := test
|
test := test
|
||||||
|
|
||||||
testWrapper(test, func() error { //nolint: thelper
|
args.TestWrapper(test, func() error { //nolint: thelper
|
||||||
paths := NewPaths(
|
paths := NewPaths(
|
||||||
filepath.Join(testDir, test.Name()),
|
filepath.Join(testDir, test.Name()),
|
||||||
)
|
)
|
||||||
|
|
||||||
for i := 0; i < maxAttempts; i++ {
|
for i := 0; i < args.MaxAttempts; i++ {
|
||||||
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, waitForDebugger, raceDetector, inputDelay, gitVersion)
|
err := runTest(test, args, paths, projectRootDir, gitVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if i == maxAttempts-1 {
|
if i == args.MaxAttempts-1 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logf("retrying test %s", test.Name())
|
args.Logf("retrying test %s", test.Name())
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -81,23 +83,18 @@ func RunTests(
|
|||||||
|
|
||||||
func runTest(
|
func runTest(
|
||||||
test *IntegrationTest,
|
test *IntegrationTest,
|
||||||
|
args RunTestArgs,
|
||||||
paths Paths,
|
paths Paths,
|
||||||
projectRootDir string,
|
projectRootDir string,
|
||||||
logf func(format string, formatArgs ...interface{}),
|
|
||||||
runCmd func(cmd *exec.Cmd) (int, error),
|
|
||||||
sandbox bool,
|
|
||||||
waitForDebugger bool,
|
|
||||||
raceDetector bool,
|
|
||||||
inputDelay int,
|
|
||||||
gitVersion *git_commands.GitVersion,
|
gitVersion *git_commands.GitVersion,
|
||||||
) error {
|
) error {
|
||||||
if test.Skip() {
|
if test.Skip() {
|
||||||
logf("Skipping test %s", test.Name())
|
args.Logf("Skipping test %s", test.Name())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !test.ShouldRunForGitVersion(gitVersion) {
|
if !test.ShouldRunForGitVersion(gitVersion) {
|
||||||
logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch)
|
args.Logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,18 +102,18 @@ func runTest(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, err := getLazygitCommand(test, paths, projectRootDir, sandbox, waitForDebugger, inputDelay)
|
cmd, err := getLazygitCommand(test, args, paths, projectRootDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pid, err := runCmd(cmd)
|
pid, err := args.RunCmd(cmd)
|
||||||
|
|
||||||
// Print race detector log regardless of the command's exit status
|
// Print race detector log regardless of the command's exit status
|
||||||
if raceDetector {
|
if args.RaceDetector {
|
||||||
logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid)
|
logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid)
|
||||||
if bytes, err := os.ReadFile(logPath); err == nil {
|
if bytes, err := os.ReadFile(logPath); err == nil {
|
||||||
logf("Race detector log:\n" + string(bytes))
|
args.Logf("Race detector log:\n" + string(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,20 +136,19 @@ func prepareTestDir(
|
|||||||
return createFixture(test, paths, rootDir)
|
return createFixture(test, paths, rootDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildLazygit(debug bool, raceDetector bool) error {
|
func buildLazygit(testArgs RunTestArgs) error {
|
||||||
// // TODO: remove this line!
|
|
||||||
// // skipping this because I'm not making changes to the app code atm.
|
|
||||||
// return nil
|
|
||||||
|
|
||||||
args := []string{"go", "build"}
|
args := []string{"go", "build"}
|
||||||
if debug {
|
if testArgs.WaitForDebugger {
|
||||||
// Disable compiler optimizations (-N) and inlining (-l) because this
|
// Disable compiler optimizations (-N) and inlining (-l) because this
|
||||||
// makes debugging work better
|
// makes debugging work better
|
||||||
args = append(args, "-gcflags=all=-N -l")
|
args = append(args, "-gcflags=all=-N -l")
|
||||||
}
|
}
|
||||||
if raceDetector {
|
if testArgs.RaceDetector {
|
||||||
args = append(args, "-race")
|
args = append(args, "-race")
|
||||||
}
|
}
|
||||||
|
if testArgs.CodeCoverageDir != "" {
|
||||||
|
args = append(args, "-cover")
|
||||||
|
}
|
||||||
args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"))
|
args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"))
|
||||||
osCommand := oscommands.NewDummyOSCommand()
|
osCommand := oscommands.NewDummyOSCommand()
|
||||||
return osCommand.Cmd.New(args).Run()
|
return osCommand.Cmd.New(args).Run()
|
||||||
@ -183,7 +179,7 @@ func getGitVersion() (*git_commands.GitVersion, error) {
|
|||||||
return git_commands.ParseGitVersion(versionStr)
|
return git_commands.ParseGitVersion(versionStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, waitForDebugger bool, inputDelay int) (*exec.Cmd, error) {
|
func getLazygitCommand(test *IntegrationTest, args RunTestArgs, paths Paths, rootDir string) (*exec.Cmd, error) {
|
||||||
osCommand := oscommands.NewDummyOSCommand()
|
osCommand := oscommands.NewDummyOSCommand()
|
||||||
|
|
||||||
err := os.RemoveAll(paths.Config())
|
err := os.RemoveAll(paths.Config())
|
||||||
@ -211,11 +207,18 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb
|
|||||||
|
|
||||||
cmdObj := osCommand.Cmd.New(cmdArgs)
|
cmdObj := osCommand.Cmd.New(cmdArgs)
|
||||||
|
|
||||||
|
if args.CodeCoverageDir != "" {
|
||||||
|
// We set this explicitly here rather than inherit it from the test runner's
|
||||||
|
// environment because the test runner has its own coverage directory that
|
||||||
|
// it writes to and so if we pass GOCOVERDIR to that, it will be overwritten.
|
||||||
|
cmdObj.AddEnvVars("GOCOVERDIR=" + args.CodeCoverageDir)
|
||||||
|
}
|
||||||
|
|
||||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", TEST_NAME_ENV_VAR, test.Name()))
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", TEST_NAME_ENV_VAR, test.Name()))
|
||||||
if sandbox {
|
if args.Sandbox {
|
||||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true"))
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true"))
|
||||||
}
|
}
|
||||||
if waitForDebugger {
|
if args.WaitForDebugger {
|
||||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=true", WAIT_FOR_DEBUGGER_ENV_VAR))
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=true", WAIT_FOR_DEBUGGER_ENV_VAR))
|
||||||
}
|
}
|
||||||
// Set a race detector log path only to avoid spamming the terminal with the
|
// Set a race detector log path only to avoid spamming the terminal with the
|
||||||
@ -227,8 +230,8 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if inputDelay > 0 {
|
if args.InputDelay > 0 {
|
||||||
cmdObj.AddEnvVars(fmt.Sprintf("INPUT_DELAY=%d", inputDelay))
|
cmdObj.AddEnvVars(fmt.Sprintf("INPUT_DELAY=%d", args.InputDelay))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir)))
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir)))
|
||||||
|
@ -10,7 +10,25 @@ fi
|
|||||||
|
|
||||||
cp test/global_git_config ~/.gitconfig
|
cp test/global_git_config ~/.gitconfig
|
||||||
|
|
||||||
go test pkg/integration/clients/*.go
|
# if the LAZYGIT_GOCOVERDIR env var is set, we'll capture code coverage data
|
||||||
|
if [ -n "$LAZYGIT_GOCOVERDIR" ]; then
|
||||||
|
# Go expects us to either be running the test binary directly or running `go test`, but because
|
||||||
|
# we're doing both and because we want to combine coverage data for both, we need to be a little
|
||||||
|
# hacky. To capture the coverage data for the test runner we pass the test.gocoverdir positional
|
||||||
|
# arg, but if we do that then the GOCOVERDIR env var (which you typically pass to the test binary) will be overwritten by the test runner. So we're passing LAZYGIT_COCOVERDIR instead
|
||||||
|
# and then internally passing that to the test binary as GOCOVERDIR.
|
||||||
|
go test -cover -coverpkg=github.com/jesseduffield/lazygit/pkg/... pkg/integration/clients/*.go -args -test.gocoverdir="/tmp/code_coverage"
|
||||||
|
|
||||||
|
# We're merging the coverage data for the sake of having fewer artefacts to upload.
|
||||||
|
# We can't merge inline so we're merging to a tmp dir then moving back to the original.
|
||||||
|
mkdir -p /tmp/code_coverage_merged
|
||||||
|
go tool covdata merge -i=/tmp/code_coverage -o=/tmp/code_coverage_merged
|
||||||
|
rm -rf /tmp/code_coverage
|
||||||
|
mv /tmp/code_coverage_merged /tmp/code_coverage
|
||||||
|
else
|
||||||
|
go test pkg/integration/clients/*.go
|
||||||
|
fi
|
||||||
|
|
||||||
EXITCODE=$?
|
EXITCODE=$?
|
||||||
|
|
||||||
if test -f ~/.gitconfig.lazygit.bak; then
|
if test -f ~/.gitconfig.lazygit.bak; then
|
||||||
|
Loading…
x
Reference in New Issue
Block a user