mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Merge branch 'master' into gh-integration
This commit is contained in:
commit
4a31f3fd50
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -4,7 +4,6 @@ about: Create a report to help us improve
|
|||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
|
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
|||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
|
|
@ -23,10 +23,15 @@ A clear and concise description of what you expected to happen.
|
|||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Windows]
|
||||
- Lazygit Version [e.g. v0.1.45]
|
||||
- The last commit id if you built project from sources (run : ```git rev-parse HEAD```)
|
||||
**Version info:**
|
||||
_Run `lazygit --version` and paste the result here_
|
||||
_Run `git --version` and paste the result here_
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Note:** please try updating to the latest version or [manually building](https://github.com/jesseduffield/lazygit/#manual) the latest `master` to see if the issue still occurs.
|
||||
|
||||
<!--
|
||||
If you want to try and debug this issue yourself, you can run `lazygit --debug` in one terminal panel and `lazygit --logs` in another to view the logs.
|
||||
-->
|
||||
|
|
|
|||
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -4,7 +4,6 @@ about: Suggest an idea for this project
|
|||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
|
@ -18,3 +17,13 @@ A clear and concise description of any alternative solutions or features you've
|
|||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
<!--
|
||||
You may be able to add your desired feature with a custom command. Check out the examples here: https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium
|
||||
|
||||
If a custom command does what you want but you still want to see the feature built-in to lazygit, feel free to paste the custom command into the issue to help us better understand the functionality you want.
|
||||
|
||||
We also encourage you to put up a PR yourself! Who cares if you've never written Go before, neither did any of the existing contributors before their first lazygit PR! Check out the PR tutorial here: https://www.youtube.com/watch?v=kNavnhzZHtk&ab_channel=JesseDuffield
|
||||
|
||||
Also check out the contributing guide here: https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md
|
||||
-->
|
||||
|
|
|
|||
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
|
||||
4
.github/workflows/cd.yml
vendored
4
.github/workflows/cd.yml
vendored
|
|
@ -16,13 +16,13 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.18.x
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
|
||||
homebrew:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Bump Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
|
|
|
|||
109
.github/workflows/ci.yml
vendored
109
.github/workflows/ci.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: Continuous Integration
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.18
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
@ -7,13 +10,18 @@ on:
|
|||
pull_request:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
unit-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
cache_path: ~/.cache/go-build
|
||||
- os: windows-latest
|
||||
cache_path: ~\AppData\Local\go-build
|
||||
name: ci - ${{matrix.os}}
|
||||
runs-on: ${{matrix.os}}
|
||||
env:
|
||||
|
|
@ -24,17 +32,73 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.18.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{matrix.cache_path}}
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
# we're passing -short so that we skip the integration tests, which will be run in parallel below
|
||||
run: |
|
||||
go test ./... -short
|
||||
integration-tests-old:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
parallelism: [5]
|
||||
index: [0,1,2,3,4]
|
||||
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
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
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
run: |
|
||||
bash ./test.sh
|
||||
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:
|
||||
|
|
@ -46,11 +110,13 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.18.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
|
|
@ -63,6 +129,12 @@ jobs:
|
|||
- name: Build darwin binary
|
||||
run: |
|
||||
GOOS=darwin go build
|
||||
- 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-cheatsheet:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
|
@ -74,11 +146,13 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
go-version: 1.18.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
|
|
@ -87,11 +161,26 @@ jobs:
|
|||
go run scripts/cheatsheet/main.go check
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout
|
||||
- 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: Lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
uses: golangci/golangci-lint-action@v3.1.0
|
||||
with:
|
||||
version: latest
|
||||
- name: Format code
|
||||
|
|
|
|||
28
.github/workflows/sponsors.yml
vendored
Normal file
28
.github/workflows/sponsors.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# see https://github.com/JamesIves/github-sponsors-readme-action
|
||||
name: Generate Sponsors README
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@v1.0.8
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_TOKEN }}
|
||||
file: 'README.md'
|
||||
if: ${{ github.repository == 'jesseduffield/lazygit' }}
|
||||
|
||||
- name: Commit and push if changed
|
||||
run: |-
|
||||
git diff
|
||||
git config --global user.email "actions@users.noreply.github.com"
|
||||
git config --global user.name "README-bot"
|
||||
git add README.md
|
||||
git commit -m "Updated README.md" || exit 0
|
||||
git push
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -23,16 +23,29 @@ lazygit.exe
|
|||
# Exceptions
|
||||
!.gitignore
|
||||
!.goreleaser.yml
|
||||
!.golangci.yml
|
||||
!.circleci/
|
||||
!.github/
|
||||
!.vscode/
|
||||
|
||||
# these are for our integration tests
|
||||
!.git_keep
|
||||
!.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/*/actual_remote/
|
||||
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
|
||||
|
|
|
|||
29
.golangci.yml
Normal file
29
.golangci.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
linters:
|
||||
disable:
|
||||
- structcheck # gives false positives
|
||||
enable:
|
||||
- gofumpt
|
||||
- thelper
|
||||
- goimports
|
||||
- tparallel
|
||||
- wastedassign
|
||||
- exportloopref
|
||||
- unparam
|
||||
- prealloc
|
||||
- unconvert
|
||||
- exhaustive
|
||||
- makezero
|
||||
- nakedret
|
||||
# - goconst # TODO: enable and fix issues
|
||||
fast: false
|
||||
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
nakedret:
|
||||
# the gods will judge me but I just don't like naked returns at all
|
||||
max-func-lines: 0
|
||||
|
||||
run:
|
||||
go: 1.18
|
||||
1
.vscode/debugger_config.yml
vendored
Normal file
1
.vscode/debugger_config.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
disableStartupPopups: true
|
||||
36
.vscode/launch.json
vendored
Normal file
36
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Lazygit",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["--debug", "--use-config-file=.vscode/debugger_config.yml"],
|
||||
"console": "integratedTerminal",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Tail Lazygit logs",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["--logs", "--use-config-file=.vscode/debugger_config.yml"],
|
||||
"console": "integratedTerminal",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Run with logs",
|
||||
"configurations": ["Tail Lazygit logs", "Debug Lazygit"],
|
||||
"stopAll": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,6 +6,10 @@ When contributing to this repository, please first discuss the change you wish
|
|||
to make via issue, email, or any other method with the owners of this repository
|
||||
before making a change.
|
||||
|
||||
## PR walkthrough
|
||||
|
||||
[This video](https://www.youtube.com/watch?v=kNavnhzZHtk) walks through the process of adding a small feature to lazygit. If you have no idea where to start, watching that video is a good first step.
|
||||
|
||||
## All code changes happen through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively
|
||||
|
|
@ -50,6 +54,21 @@ var _ MyInterface = &MyStruct{}
|
|||
|
||||
This makes the intent clearer and means that if we fail to satisfy the interface we'll get an error in the file that needs fixing.
|
||||
|
||||
### Code Formatting
|
||||
|
||||
To check code formatting [gofumpt](https://pkg.go.dev/mvdan.cc/gofumpt#section-readme) (which is a bit stricter than [gofmt](https://pkg.go.dev/cmd/gofmt)) is used.
|
||||
VSCode will format the code correctly if you tell the Go extension to use `gofumpt` via your [`settings.json`](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson)
|
||||
by setting [`formatting.gofumpt`](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#gofumpt-bool) to `true`:
|
||||
|
||||
```jsonc
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Internationalisation
|
||||
|
||||
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
|
||||
|
|
@ -58,13 +77,36 @@ Boy that's a hard word to spell. Anyway, lazygit is translated into several lang
|
|||
|
||||
The easiest way to debug lazygit is to have two terminal tabs open at once: one for running lazygit (via `go run main.go -debug` in the project root) and one for viewing lazygit's logs (which can be done via `go run main.go --logs` or just `lazygit --logs`).
|
||||
|
||||
From most places in the codebase you have access to a logger e.g. `gui.Log.Warn("blah")`
|
||||
From most places in the codebase you have access to a logger e.g. `gui.Log.Warn("blah")`.
|
||||
|
||||
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`
|
||||
logPath := "/Users/jesseduffield/Library/Application Support/jesseduffield/lazygit/development.log"
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic("unable to log to file")
|
||||
}
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(logrus.WarnLevel)
|
||||
logger.SetOutput(file)
|
||||
return logger.WithFields(logrus.Fields{})
|
||||
}
|
||||
|
||||
var Log = newLogger()
|
||||
...
|
||||
Log.Warn("blah")
|
||||
```
|
||||
|
||||
If you keep having to do some setup steps to reproduce an issue, read the Testing section below to see how to create an integration test by recording a lazygit session. It's pretty easy!
|
||||
|
||||
If you want to trigger a debug session from VSCode, you can use the following snippet. Note that the 'console' key is not, at the time of writing, in a stable release.
|
||||
### VSCode debugger
|
||||
|
||||
If you want to trigger a debug session from VSCode, you can use the following snippet. Note that the `console` key is, at the time of writing, still an experimental feature.
|
||||
|
||||
```jsonc
|
||||
// .vscode/launch.json
|
||||
|
|
@ -77,6 +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"],
|
||||
"console": "externalTerminal" // <-- you need this to actually see the lazygit UI in a window while debugging
|
||||
}
|
||||
]
|
||||
|
|
@ -85,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
|
||||
|
||||
|
|
|
|||
14
Dockerfile
14
Dockerfile
|
|
@ -2,16 +2,18 @@
|
|||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh
|
||||
|
||||
FROM golang:1.14-alpine3.11
|
||||
FROM golang:1.18 as build
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY ./ .
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build
|
||||
|
||||
FROM alpine:3.11
|
||||
RUN apk add -U git xdg-utils
|
||||
FROM alpine:3.15
|
||||
RUN apk add --no-cache -U git xdg-utils
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
|
||||
COPY --from=build /go/src/github.com/jesseduffield/lazygit ./
|
||||
COPY --from=build /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
|
||||
RUN echo "alias gg=lazygit" >> ~/.profile
|
||||
|
||||
ENTRYPOINT [ "lazygit" ]
|
||||
|
|
|
|||
49
cmd/integration_test/main.go
Normal file
49
cmd/integration_test/main.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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 <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":
|
||||
clients.RunCLI(os.Args[2:])
|
||||
case "tui":
|
||||
clients.RunTUI()
|
||||
default:
|
||||
log.Fatal(usage)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,24 +22,27 @@ gui:
|
|||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl'
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl' | 'ja' | 'ko'
|
||||
timeFormat: '02 Jan 06 15:04 MST' # https://pkg.go.dev/time#Time.Format
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- green
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
- white
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
- blue # set to `default` to have no background colour
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
cherryPickedCommitBgColor:
|
||||
- blue
|
||||
cherryPickedCommitFgColor:
|
||||
- cyan
|
||||
cherryPickedCommitFgColor:
|
||||
- blue
|
||||
unstagedChangesColor:
|
||||
- red
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
|
|
@ -48,8 +51,11 @@ gui:
|
|||
showFileTree: true # for rendering changes files in a tree format
|
||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||
showRandomTip: true
|
||||
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
|
||||
showCommandLog: true
|
||||
showIcons: false
|
||||
commandLogSize: 8
|
||||
splitDiff: 'auto' # one of 'auto' | 'always'
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
|
|
@ -69,8 +75,11 @@ git:
|
|||
# one of always, never, when-maximised
|
||||
# this determines whether the git graph is rendered in the commits panel
|
||||
showGraph: 'when-maximised'
|
||||
# displays the whole git graph by default in the commits panel (equivalent to passing the `--all` argument to `git log`)
|
||||
showWholeGraph: false
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
autoRefresh: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
|
|
@ -80,11 +89,11 @@ git:
|
|||
diffContextSize: 3 # how many lines of context are shown around a change in diffs
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
editCommandTemplate: '{{editor}} {{filename}}'
|
||||
editCommandTemplate: ''
|
||||
openCommand: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
refreshInterval: 10 # File/submodule refresh interval in seconds. Auto-refresh can be disabled via option 'git.autoRefresh'.
|
||||
fetchInterval: 60 # Re-fetch interval in seconds. Auto-fetch can be disabled via option 'git.autoFetch'.
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
|
|
@ -93,7 +102,8 @@ 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:
|
||||
quit: 'q'
|
||||
|
|
@ -179,6 +189,7 @@ keybinding:
|
|||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
renameBranch: 'R'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
|
|
@ -264,12 +275,12 @@ os:
|
|||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
You can specify a line number you are currently at when in the line-by-line mode.
|
||||
You can specify the current line number when you're in the patch explorer.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim'
|
||||
editCommandTemplate: '{{editor}} +{{line}} {{filename}}'
|
||||
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
|
||||
```
|
||||
|
||||
or
|
||||
|
|
@ -277,23 +288,23 @@ or
|
|||
```yaml
|
||||
os:
|
||||
editCommand: 'code'
|
||||
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
|
||||
editCommandTemplate: '{{editor}} --goto -- {{filename}}:{{line}}'
|
||||
```
|
||||
|
||||
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
|
||||
|
||||
### Overriding default config file location
|
||||
|
||||
To override the default config directory, use `$CONFIG_DIR="~/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
|
||||
To override the default config directory, use `CONFIG_DIR="$HOME/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
|
||||
|
||||
To override the individual config file used, use the `--use-config-file` arg or the `LG_CONFIG_FILE` env var.
|
||||
|
||||
If you want to merge a specific config file into a more general config file, perhaps for the sake of setting some theme-specific options, you can supply a list of comma-separated config file paths, like so:
|
||||
|
||||
```sh
|
||||
lazygit --use-config-file=~/.base_lg_conf,~/.light_theme_lg_conf
|
||||
lazygit --use-config-file="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf"
|
||||
or
|
||||
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
|
||||
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
|
|
@ -346,9 +357,20 @@ gui:
|
|||
- default
|
||||
```
|
||||
|
||||
## Struggling to see selected line
|
||||
## Highlighting the selected line
|
||||
|
||||
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
|
||||
If you don't like the default behaviour of highlighting the selected line with a blue background, you can use the `selectedLineBgColor` and `selectedRangeBgColor` keys to customise the behaviour. If you just want to embolden the selected line (this was the original default), you can do the following:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
You can also use the reverse attribute like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
|
|
@ -359,25 +381,6 @@ gui:
|
|||
- reverse
|
||||
```
|
||||
|
||||
The following has also worked for a couple of people:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
- blue
|
||||
```
|
||||
|
||||
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
|
||||
|
||||
If you're still having trouble please raise an issue.
|
||||
|
||||
## Custom Author Color
|
||||
|
||||
Lazygit will assign a random color for every commit author in the commits pane by default.
|
||||
|
|
@ -387,7 +390,8 @@ You can customize the color in case you're not happy with the randomly assigned
|
|||
```yaml
|
||||
gui:
|
||||
authorColors:
|
||||
'John Smith': '#ff0000' # use red for John Smith
|
||||
'John Smith': 'red' # use red for John Smith
|
||||
'Alan Smithee': '#00ff00' # use green for Alan Smithee
|
||||
```
|
||||
|
||||
You can use wildcard to set a unified color in case your are lazy to customize the color for every author or you just want a single color for all/other authors:
|
||||
|
|
@ -396,7 +400,7 @@ You can use wildcard to set a unified color in case your are lazy to customize t
|
|||
gui:
|
||||
authorColors:
|
||||
# use red for John Smith
|
||||
'John Smith': '#ff0000'
|
||||
'John Smith': 'red'
|
||||
# use blue for other authors
|
||||
'*': '#0000ff'
|
||||
```
|
||||
|
|
@ -415,6 +419,15 @@ gui:
|
|||
|
||||

|
||||
|
||||
## Display Nerd Fonts Icons
|
||||
|
||||
If you are using [Nerd Fonts](https://www.nerdfonts.com), you can display icons.
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
showIcons: true
|
||||
```
|
||||
|
||||
## Keybindings
|
||||
|
||||
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
|
||||
|
|
@ -465,7 +478,7 @@ services:
|
|||
Where:
|
||||
|
||||
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
|
||||
- `provider` is one of `github`, `bitbucket` or `gitlab`
|
||||
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
|
|
@ -519,3 +532,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'
|
||||
```
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ customCommands:
|
|||
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
|
||||
valueFormat: '{{ .branch }}'
|
||||
labelFormat: '{{ .branch | green }}'
|
||||
- key: '<f1>'
|
||||
command: 'git reset --soft {{.CheckedOutBranch.UpstreamRemote}}'
|
||||
context: 'files'
|
||||
prompts:
|
||||
- type: 'confirm'
|
||||
title: "Confirm:"
|
||||
body: "Are you sure you want to reset HEAD to {{.CheckedOutBranch.UpstreamRemote}}?"
|
||||
```
|
||||
|
||||
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
||||
|
|
@ -70,6 +77,7 @@ For a given custom command, here are the allowed fields:
|
|||
| loadingText | text to display while waiting for command to finish | no |
|
||||
| description | text to display in the keybindings menu that appears when you press 'x' | no |
|
||||
| stream | whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | whether you want to show the command's output in a gui prompt | no |
|
||||
|
||||
### Contexts
|
||||
|
||||
|
|
@ -94,28 +102,18 @@ The permitted contexts are:
|
|||
|
||||
The permitted prompt fields are:
|
||||
|
||||
| _field_ | _description_ | _required_ |
|
||||
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
||||
| type | one of 'input' or 'menu' | yes |
|
||||
| title | the title to display in the popup panel | no |
|
||||
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
||||
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
||||
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
|
||||
| | menu options | |
|
||||
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
|
||||
| | groups which are going to be kept from the command's output | |
|
||||
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
|
||||
| | the filter to construct a menu item's value (What gets appended to prompt | |
|
||||
| | responses when the item is selected). You can use named groups, | |
|
||||
| | or `{{ .group_GROUPID }}`. | |
|
||||
| | PS: named groups keep first match only | |
|
||||
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
|
||||
| | the filter to construct the item's label (What's shown on screen). You can use | |
|
||||
| | named groups, or `{{ .group_GROUPID }}`. You can also color each match with | |
|
||||
| | `{{ .group_GROUPID | colorname }}` (Color names from | |
|
||||
| | [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)) | |
|
||||
| | If `labelFormat` is not specified, `valueFormat` is shown instead. | |
|
||||
| | PS: named groups keep first match only | |
|
||||
| _field_ | _description_ | _required_ |
|
||||
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
|
||||
| type | one of 'input', 'menu', or 'confirm' | yes |
|
||||
| title | the title to display in the popup panel | no |
|
||||
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
||||
| body | (only applicable to 'confirm' prompts) the immutable body text to appear in the text box | no |
|
||||
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
||||
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
|
||||
| | menu options | |
|
||||
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying groups which are going to be kept from the command's output | yes |
|
||||
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from the filter to construct a menu item's value (What gets appended to prompt responses when the item is selected). You can use named groups, or `{{ .group_GROUPID }}`. PS: named groups keep first match only | yes |
|
||||
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from the filter to construct the item's label (What's shown on screen). You can use named groups, or `{{ .group_GROUPID }}`. You can also color each match with `{{ .group_GROUPID \| colorname }}` (Color names from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)). If `labelFormat` is not specified, `valueFormat` is shown instead. PS: named groups keep first match only | no |
|
||||
|
||||
The permitted option fields are:
|
||||
| _field_ | _description_ | _required_ |
|
||||
|
|
@ -138,13 +136,14 @@ 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
|
||||
SelectedReflogCommit
|
||||
SelectedSubCommit
|
||||
SelectedFile
|
||||
SelectedPath
|
||||
SelectedLocalBranch
|
||||
SelectedRemoteBranch
|
||||
SelectedRemote
|
||||
|
|
@ -154,7 +153,7 @@ SelectedCommitFile
|
|||
CheckedOutBranch
|
||||
```
|
||||
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
|
||||
|
||||
### Keybinding collisions
|
||||
|
||||
|
|
@ -162,7 +161,7 @@ If your custom keybinding collides with an inbuilt keybinding that is defined fo
|
|||
|
||||
### Debugging
|
||||
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `subprocess: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
|
||||
|
||||
### More Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -1,119 +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": "stage a file and commit the change", "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"
|
||||
```
|
||||
|
||||
## 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 after running the test, ignored by git)
|
||||
expected/ (the 'snapshot' repo)
|
||||
config/ (need not be present)
|
||||
test.json
|
||||
setup.sh
|
||||
recording.json
|
||||
```
|
||||
|
||||
Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature.
|
||||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -5,36 +5,38 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
## Global Keybindings
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: refresh
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
|
@ -43,8 +45,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>o</kbd>: create / open pull request
|
||||
<kbd>O</kbd>: create / open pull request options
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
|
|
@ -116,93 +118,223 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Commits)
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>r</kbd>: reword commit
|
||||
<kbd>R</kbd>: reword commit with editor
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>e</kbd>: edit commit
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
<kbd>e</kbd>: edit commit
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Extras Panel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Files Panel
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+b</kbd>: Filter commit files
|
||||
</pre>
|
||||
|
||||
## Files Panel (Files)
|
||||
## Files
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: amend last commit
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: add to .gitignore
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>s</kbd>: stash changes
|
||||
<kbd>s</kbd>: stash all changes
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
||||
## Files Panel (Submodules)
|
||||
## Local Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Main Panel (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: undo
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: scroll down (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## Main Panel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Remote Branches
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: Return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Remotes
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: show all branch logs
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Submodules
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
|
|
@ -215,94 +347,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Main Panel (Merging)
|
||||
## Tags
|
||||
|
||||
<pre>
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: undo
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
</pre>
|
||||
|
||||
## Main Panel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
</pre>
|
||||
|
||||
## Stash Panel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: new branch
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: show all branch logs
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
|
|
|||
291
docs/keybindings/Keybindings_ja.md
Normal file
291
docs/keybindings/Keybindings_ja.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||
|
||||
# Lazygit キーバインド
|
||||
|
||||
## グローバルキーバインド
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd>pgup</kbd>: メインパネルを上にスクロール (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: メインパネルを下にスクロール (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: リフレッシュ
|
||||
<kbd>x</kbd>: メニューを開く
|
||||
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 前のスクリーンモード
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: 差分メニューを開く
|
||||
<kbd>ctrl+e</kbd>: 差分メニューを開く
|
||||
<kbd>@</kbd>: コマンドログメニューを開く
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: カスタムコマンドを実行
|
||||
<kbd>z</kbd>: アンドゥ (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: リドゥ (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
</pre>
|
||||
|
||||
## 一覧パネルの操作
|
||||
|
||||
<pre>
|
||||
<kbd>,</kbd>: 前のページ
|
||||
<kbd>.</kbd>: 次のページ
|
||||
<kbd><</kbd>: 最上部までスクロール
|
||||
<kbd>/</kbd>: 検索を開始
|
||||
<kbd>></kbd>: 最下部までスクロール
|
||||
<kbd>H</kbd>: 左スクロール
|
||||
<kbd>L</kbd>: 右スクロール
|
||||
<kbd>]</kbd>: 次のタブ
|
||||
<kbd>[</kbd>: 前のタブ
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 適用
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd>space</kbd>: コミットをチェックアウト
|
||||
<kbd>y</kbd>: コミットの情報をコピー
|
||||
<kbd>o</kbd>: ブラウザでコミットを開く
|
||||
<kbd>n</kbd>: コミットにブランチを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: コミットをコピー (cherry-pick)
|
||||
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## コミット
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>r</kbd>: コミットメッセージを変更
|
||||
<kbd>R</kbd>: エディタでコミットメッセージを編集
|
||||
<kbd>d</kbd>: コミットを削除
|
||||
<kbd>e</kbd>: コミットを編集
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: このコミットに対するfixupコミットを作成
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>ctrl+j</kbd>: コミットを1つ下に移動
|
||||
<kbd>ctrl+k</kbd>: コミットを1つ上に移動
|
||||
<kbd>v</kbd>: コミットを貼り付け (cherry-pick)
|
||||
<kbd>A</kbd>: ステージされた変更でamendコミット
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: コミットをrevert
|
||||
<kbd>T</kbd>: タグを作成
|
||||
<kbd>ctrl+l</kbd>: ログメニューを開く
|
||||
<kbd>space</kbd>: コミットをチェックアウト
|
||||
<kbd>y</kbd>: コミットの情報をコピー
|
||||
<kbd>o</kbd>: ブラウザでコミットを開く
|
||||
<kbd>n</kbd>: コミットにブランチを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: コミットをコピー (cherry-pick)
|
||||
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## コミットファイル
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットされたファイル名をクリップボードにコピー
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: ファイルツリーの表示を切り替え
|
||||
</pre>
|
||||
|
||||
## サブモジュール
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: サブモジュール名をクリップボードにコピー
|
||||
<kbd>enter</kbd>: サブモジュールを開く
|
||||
<kbd>d</kbd>: サブモジュールを削除
|
||||
<kbd>u</kbd>: サブモジュールを更新
|
||||
<kbd>n</kbd>: サブモジュールを新規追加
|
||||
<kbd>e</kbd>: サブモジュールのURLを更新
|
||||
<kbd>i</kbd>: サブモジュールを初期化
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## ステータス
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 設定ファイルを編集
|
||||
<kbd>o</kbd>: 設定ファイルを開く
|
||||
<kbd>u</kbd>: 更新を確認
|
||||
<kbd>enter</kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd>a</kbd>: すべてのブランチログを表示
|
||||
</pre>
|
||||
|
||||
## タグ
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd>d</kbd>: タグを削除
|
||||
<kbd>P</kbd>: タグをpush
|
||||
<kbd>n</kbd>: タグを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## ファイル
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: ファイル名をクリップボードにコピー
|
||||
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: ステージ/アンステージ
|
||||
<kbd>ctrl+b</kbd>: ファイルをフィルタ (ステージ/アンステージ)
|
||||
<kbd>c</kbd>: 変更をコミット
|
||||
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
|
||||
<kbd>A</kbd>: 最新のコミットにamend
|
||||
<kbd>C</kbd>: gitエディタを使用して変更をコミット
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>i</kbd>: ファイルをignore
|
||||
<kbd>r</kbd>: ファイルをリフレッシュ
|
||||
<kbd>s</kbd>: 変更をstash
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: すべての変更をステージ/アンステージ
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>`</kbd>: ファイルツリーの表示を切り替え
|
||||
<kbd>M</kbd>: git mergetoolを開く
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
||||
## ブランチ
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: ブランチ名をクリップボードにコピー
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>o</kbd>: Pull Requestを作成
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: Pull RequestのURLをクリップボードにコピー
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>d</kbd>: ブランチを削除
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: 現在のブランチにマージ
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: ブランチ名を変更
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## メインパネル (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>◄</kbd>: 前のコンフリクトを選択
|
||||
<kbd>►</kbd>: 次のコンフリクトを選択
|
||||
<kbd>▲</kbd>: 前のhunkを選択
|
||||
<kbd>▼</kbd>: 次のhunkを選択
|
||||
<kbd>z</kbd>: アンドゥ
|
||||
<kbd>M</kbd>: git mergetoolを開く
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
</pre>
|
||||
|
||||
## メインパネル (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: 下にスクロール (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 上にスクロール (fn+down)
|
||||
</pre>
|
||||
|
||||
## メインパネル (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 前のhunkを選択
|
||||
<kbd>►</kbd>: 次のhunkを選択
|
||||
<kbd>v</kbd>: 範囲選択を切り替え
|
||||
<kbd>V</kbd>: 範囲選択を切り替え
|
||||
<kbd>a</kbd>: hunk選択を切り替え
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>space</kbd>: 行をパッチに追加/削除
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## メインパネル (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 前のhunkを選択
|
||||
<kbd>►</kbd>: 次のhunkを選択
|
||||
<kbd>v</kbd>: 範囲選択を切り替え
|
||||
<kbd>V</kbd>: 範囲選択を切り替え
|
||||
<kbd>a</kbd>: hunk選択を切り替え
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
<kbd>tab</kbd>: パネルを切り替え
|
||||
<kbd>space</kbd>: 選択行をステージ/アンステージ
|
||||
<kbd>d</kbd>: 変更を削除 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
</pre>
|
||||
|
||||
## リモート
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: リモートをfetch
|
||||
<kbd>n</kbd>: リモートを新規追加
|
||||
<kbd>d</kbd>: リモートを削除
|
||||
<kbd>e</kbd>: リモートを編集
|
||||
</pre>
|
||||
|
||||
## リモートブランチ
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>M</kbd>: 現在のブランチにマージ
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>d</kbd>: ブランチを削除
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: リモート一覧に戻る
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## 参照ログ
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
|
||||
<kbd>space</kbd>: コミットをチェックアウト
|
||||
<kbd>y</kbd>: コミットの情報をコピー
|
||||
<kbd>o</kbd>: ブラウザでコミットを開く
|
||||
<kbd>n</kbd>: コミットにブランチを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: コミットをコピー (cherry-pick)
|
||||
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
</pre>
|
||||
291
docs/keybindings/Keybindings_ko.md
Normal file
291
docs/keybindings/Keybindings_ko.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
|
||||
|
||||
# Lazygit 키 바인딩
|
||||
|
||||
## 글로벌 키 바인딩
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd>pgup</kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
|
||||
<kbd>R</kbd>: 새로고침
|
||||
<kbd>x</kbd>: 매뉴 열기
|
||||
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 이전 스크린 모드
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: Diff 메뉴 열기
|
||||
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
|
||||
<kbd>@</kbd>: 명령어 로그 메뉴 열기
|
||||
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
|
||||
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>z</kbd>: 되돌리기 (reflog) (실험적)
|
||||
<kbd>ctrl+z</kbd>: 다시 실행 (reflog) (실험적)
|
||||
<kbd>P</kbd>: 푸시
|
||||
<kbd>p</kbd>: 업데이트
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>,</kbd>: 이전 페이지
|
||||
<kbd>.</kbd>: 다음 페이지
|
||||
<kbd><</kbd>: 맨 위로 스크롤
|
||||
<kbd>/</kbd>: 검색 시작
|
||||
<kbd>></kbd>: 맨 아래로 스크롤
|
||||
<kbd>H</kbd>: 우 스크롤
|
||||
<kbd>L</kbd>: 좌 스크롤
|
||||
<kbd>]</kbd>: 이전 탭
|
||||
<kbd>[</kbd>: 다음 탭
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd>space</kbd>: 커밋을 체크아웃
|
||||
<kbd>y</kbd>: 커밋 attribute 복사
|
||||
<kbd>o</kbd>: 브라우저에서 커밋 열기
|
||||
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
|
||||
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 적용
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd>space</kbd>: 커밋을 체크아웃
|
||||
<kbd>y</kbd>: 커밋 attribute 복사
|
||||
<kbd>o</kbd>: 브라우저에서 커밋 열기
|
||||
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
|
||||
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>◄</kbd>: 이전 충돌을 선택
|
||||
<kbd>►</kbd>: 다음 충돌을 선택
|
||||
<kbd>▲</kbd>: 이전 hunk를 선택
|
||||
<kbd>▼</kbd>: 다음 hunk를 선택
|
||||
<kbd>z</kbd>: 되돌리기
|
||||
<kbd>M</kbd>: git mergetool를 열기
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: 아래로 스크롤 (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 위로 스크롤 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 이전 hunk를 선택
|
||||
<kbd>►</kbd>: 다음 hunk를 선택
|
||||
<kbd>v</kbd>: 드래그 선택 전환
|
||||
<kbd>V</kbd>: 드래그 선택 전환
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>space</kbd>: line(s)을 패치에 추가/삭제
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 이전 hunk를 선택
|
||||
<kbd>►</kbd>: 다음 hunk를 선택
|
||||
<kbd>v</kbd>: 드래그 선택 전환
|
||||
<kbd>V</kbd>: 드래그 선택 전환
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
<kbd>tab</kbd>: 패널 전환
|
||||
<kbd>space</kbd>: 선택한 행을 staged / unstaged
|
||||
<kbd>d</kbd>: 변경을 삭제 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
</pre>
|
||||
|
||||
## 브랜치
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 브랜치명을 클립보드에 복사
|
||||
<kbd>i</kbd>: git-flow 옵션 보기
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>o</kbd>: 풀 리퀘스트 생성
|
||||
<kbd>O</kbd>: 풀 리퀘스트 생성 옵션
|
||||
<kbd>ctrl+y</kbd>: 풀 리퀘스트 URL을 클립보드에 복사
|
||||
<kbd>c</kbd>: 이름으로 체크아웃
|
||||
<kbd>F</kbd>: 강제 체크아웃
|
||||
<kbd>d</kbd>: 브랜치 삭제
|
||||
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
|
||||
<kbd>M</kbd>: 현재 브랜치에 병합
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: 브랜치 이름 변경
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## 상태
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 설정 파일 수정
|
||||
<kbd>o</kbd>: 설정 파일 열기
|
||||
<kbd>u</kbd>: 업데이트 확인
|
||||
<kbd>enter</kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd>a</kbd>: 모든 브랜치 로그 표시
|
||||
</pre>
|
||||
|
||||
## 서브모듈
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 서브모듈 이름을 클립보드에 복사
|
||||
<kbd>enter</kbd>: 서브모듈 열기
|
||||
<kbd>d</kbd>: 서브모듈 삭제
|
||||
<kbd>u</kbd>: 서브모듈 업데이트
|
||||
<kbd>n</kbd>: 새로운 서브모듈 추가
|
||||
<kbd>e</kbd>: 서브모듈의 URL을 수정
|
||||
<kbd>i</kbd>: 서브모듈 초기화
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## 원격
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: 원격을 업데이트
|
||||
<kbd>n</kbd>: 새로운 Remote 추가
|
||||
<kbd>d</kbd>: Remote를 삭제
|
||||
<kbd>e</kbd>: Remote를 수정
|
||||
</pre>
|
||||
|
||||
## 원격 브랜치
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>M</kbd>: 현재 브랜치에 병합
|
||||
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
|
||||
<kbd>d</kbd>: 브랜치 삭제
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: 원격목록으로 돌아가기
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## 커밋
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: bisect 옵션 보기
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>r</kbd>: 커밋메시지 변경
|
||||
<kbd>R</kbd>: 에디터에서 커밋메시지 수정
|
||||
<kbd>d</kbd>: 커밋 삭제
|
||||
<kbd>e</kbd>: 커밋을 편집
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>ctrl+j</kbd>: 커밋을 1개 아래로 이동
|
||||
<kbd>ctrl+k</kbd>: 커밋을 1개 위로 이동
|
||||
<kbd>v</kbd>: 커밋을 붙여넣기 (cherry-pick)
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: 커밋 되돌리기
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+l</kbd>: 로그 메뉴 열기
|
||||
<kbd>space</kbd>: 커밋을 체크아웃
|
||||
<kbd>y</kbd>: 커밋 attribute 복사
|
||||
<kbd>o</kbd>: 브라우저에서 커밋 열기
|
||||
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
|
||||
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## 커밋 파일
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 커밋한 파일명을 클립보드에 복사
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: 파일 트리뷰로 전환
|
||||
</pre>
|
||||
|
||||
## 태그
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd>d</kbd>: 태그 삭제
|
||||
<kbd>P</kbd>: 태그를 push
|
||||
<kbd>n</kbd>: 태그를 생성
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: 커밋 보기
|
||||
</pre>
|
||||
|
||||
## 파일
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 파일명을 클립보드에 복사
|
||||
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: Staged 전환
|
||||
<kbd>ctrl+b</kbd>: 파일을 필터하기 (Staged/unstaged)
|
||||
<kbd>c</kbd>: 커밋 변경내용
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: 마지맛 커밋 수정
|
||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>i</kbd>: ignore file
|
||||
<kbd>r</kbd>: 파일 새로고침
|
||||
<kbd>s</kbd>: 변경사항을 Stash
|
||||
<kbd>S</kbd>: Stash 옵션 보기
|
||||
<kbd>a</kbd>: 모든 변경을 Staged/unstaged으로 전환
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>`</kbd>: 파일 트리뷰로 전환
|
||||
<kbd>M</kbd>: git mergetool를 열기
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
|
@ -5,109 +5,92 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
## Globale Sneltoetsen
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: wissel naar een recente repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down)
|
||||
<kbd>ctrl+r</kbd>: wissel naar een recente repo
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: vorige scherm modus
|
||||
<kbd>:</kbd>: voor aangepaste commando uit
|
||||
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: voer aangepaste commando uit
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
</pre>
|
||||
|
||||
## Lijstpaneel Navigatie
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>/</kbd>: start met zoeken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: volgende tabblad
|
||||
<kbd>[</kbd>: vorige tabblad
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Branches Tabblad)
|
||||
## Bestanden
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: wijzig laatste commit
|
||||
<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>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
<kbd>a</kbd>: toggle staged alle
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
||||
## Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||
<kbd>i</kbd>: laat git-flow opties zien
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>o</kbd>: maak of laat een pull-request zien
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>o</kbd>: maak een pull-request
|
||||
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
|
||||
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>i</kbd>: laat git-flow opties zien
|
||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remote Branches (in Remotes tabblad))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remotes Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: voeg een nieuwe remote toe
|
||||
<kbd>d</kbd>: verwijder remote
|
||||
<kbd>e</kbd>: wijzig remote
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Sub-commits)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Tags Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>d</kbd>: verwijder tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Commit bestanden Paneel
|
||||
## Commit bestanden
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
|
||||
|
|
@ -116,93 +99,174 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: toggle bestand inbegrepen in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits)
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: squash beneden
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>r</kbd>: hernoem commit
|
||||
<kbd>R</kbd>: hernoem commit met editor
|
||||
<kbd>g</kbd>: reset naar deze commit
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>d</kbd>: verwijder commit
|
||||
<kbd>e</kbd>: wijzig commit
|
||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||
<kbd>F</kbd>: creëer fixup commit voor deze commit
|
||||
<kbd>S</kbd>: squash bovenstaande commits
|
||||
<kbd>d</kbd>: verwijder commit
|
||||
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
|
||||
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
|
||||
<kbd>e</kbd>: wijzig commit
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||
<kbd>t</kbd>: commit ongedaan maken
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: commit ongedaan maken
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Reflog Tabblad)
|
||||
## Mergen
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
<kbd>►</kbd>: selecteer volgende conflict
|
||||
<kbd>▲</kbd>: selecteer bovenste hunk
|
||||
<kbd>▼</kbd>: selecteer onderste hunk
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
</pre>
|
||||
|
||||
## Normaal
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel ▼</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Patch Bouwen
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Extras Paneel
|
||||
## Remote Branches
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
<kbd>esc</kbd>: ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel
|
||||
## Remotes
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+b</kbd>: Commit dossiers filteren
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: voeg een nieuwe remote toe
|
||||
<kbd>d</kbd>: verwijder remote
|
||||
<kbd>e</kbd>: wijzig remote
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Bestanden)
|
||||
## Staging
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: wijzig laatste commit
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: voeg toe aan .gitignore
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
<kbd>a</kbd>: toggle staged alle
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>tab</kbd>: ga naar een ander paneel
|
||||
<kbd>space</kbd>: toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Submodules)
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle logs van de branch laten zien
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Submodules
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
|
||||
|
|
@ -215,94 +279,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>b</kbd>: bekijk bulk submodule opties
|
||||
</pre>
|
||||
|
||||
## Hoofd Paneel (Mergen)
|
||||
## Tags
|
||||
|
||||
<pre>
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
<kbd>►</kbd>: selecteer volgende conflict
|
||||
<kbd>▲</kbd>: selecteer bovenste hunk
|
||||
<kbd>▼</kbd>: selecteer onderste hunk
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hoofd Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>ő</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hoofd Paneel (Patch Bouwen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
</pre>
|
||||
|
||||
## Hoofd Paneel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert change (git reset)
|
||||
<kbd>tab</kbd>: ga naar een ander paneel
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
</pre>
|
||||
|
||||
## Menu Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit menu
|
||||
</pre>
|
||||
|
||||
## Stash Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bekijk bestanden van stash entry
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
</pre>
|
||||
|
||||
## Status Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle logs van de branch laten zien
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>d</kbd>: verwijder tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -5,109 +5,140 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: odśwież
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: wykonaj własną komendę
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: wykonaj własną komendę
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Branches Tab)
|
||||
## Commity
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>s</kbd>: ściśnij
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>r</kbd>: zmień nazwę commita
|
||||
<kbd>R</kbd>: zmień nazwę commita w edytorze
|
||||
<kbd>d</kbd>: usuń commit
|
||||
<kbd>e</kbd>: edytuj commit
|
||||
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
|
||||
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
|
||||
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
|
||||
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
|
||||
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
|
||||
<kbd>v</kbd>: wklej commity (przebieranie)
|
||||
<kbd>A</kbd>: popraw commit zmianami z poczekalni
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: odwróć commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Local Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>o</kbd>: maak of laat een pull-request zien
|
||||
<kbd>O</kbd>: utwórz opcje żądania
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>o</kbd>: utwórz żądanie pobrania
|
||||
<kbd>O</kbd>: utwórz opcje żądania ściągnięcia
|
||||
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania pobrania do schowka
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: zmiana bazy gałęzi
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remote Branches (in Remotes tab))
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do listy repozytoriów zdalnych
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: zmiana bazy gałęzi
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>esc</kbd>: wyście z trybu "linia po linii"
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remotes Tab)
|
||||
## Pliki
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: pokaż opcje porzucania zmian
|
||||
<kbd>space</kbd>: przełącz stan poczekalni
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: Zatwierdź zmiany
|
||||
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>A</kbd>: Zmień ostatni commit
|
||||
<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>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj zmiany
|
||||
<kbd>S</kbd>: wyświetl opcje schowka
|
||||
<kbd>a</kbd>: przełącz stan poczekalni wszystkich
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>D</kbd>: wyświetl opcje resetu
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>f</kbd>: pobierz
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Sub-commits)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Pliki commita Panel
|
||||
## Pliki commita
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
|
||||
|
|
@ -116,93 +147,119 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Commity)
|
||||
## Poczekalnia
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>s</kbd>: ściśnij
|
||||
<kbd>r</kbd>: zmień nazwę commita
|
||||
<kbd>R</kbd>: zmień nazwę commita w edytorze
|
||||
<kbd>g</kbd>: zresetuj do tego commita
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
|
||||
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
|
||||
<kbd>d</kbd>: usuń commit
|
||||
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
|
||||
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
|
||||
<kbd>e</kbd>: edytuj commit
|
||||
<kbd>A</kbd>: popraw commit zmianami z poczekalni
|
||||
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
|
||||
<kbd>t</kbd>: odwróć commit
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>v</kbd>: wklej commity (przebieranie)
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Reflog Tab)
|
||||
## Reflog
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Extras Panel
|
||||
## Remote Branches
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>r</kbd>: zmiana bazy gałęzi
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>esc</kbd>: wróć do listy repozytoriów zdalnych
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Pliki Panel
|
||||
## Remotes
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+b</kbd>: Filtrowanie commitów
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Pliki)
|
||||
## Scalanie
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: Zatwierdź zmiany
|
||||
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>A</kbd>: Zmień ostatni commit
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
<kbd>space</kbd>: przełącz stan poczekalni
|
||||
<kbd>d</kbd>: pokaż opcje porzucania zmian
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: dodaj do .gitignore
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj zmiany
|
||||
<kbd>S</kbd>: wyświetl opcje schowka
|
||||
<kbd>a</kbd>: przełącz stan poczekalni wszystkich
|
||||
<kbd>D</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>f</kbd>: pobierz
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>◄</kbd>: poprzedni konflikt
|
||||
<kbd>►</kbd>: następny konflikt
|
||||
<kbd>▲</kbd>: wybierz poprzedni kawałek
|
||||
<kbd>▼</kbd>: wybierz następny kawałek
|
||||
<kbd>z</kbd>: cofnij
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>space</kbd>: wybierz kawałek
|
||||
<kbd>b</kbd>: wybierz wszystkie kawałki
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Submodules)
|
||||
## Schowek
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj konfigurację
|
||||
<kbd>o</kbd>: otwórz konfigurację
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: pokaż wszystkie logi gałęzi
|
||||
</pre>
|
||||
|
||||
## Sub-commits
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>c</kbd>: kopiuj commit (przebieranie)
|
||||
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Submodules
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
|
|
@ -215,94 +272,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Główne Panel (Scalanie)
|
||||
## Tags
|
||||
|
||||
<pre>
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: wybierz kawałek
|
||||
<kbd>b</kbd>: wybierz wszystkie kawałki
|
||||
<kbd>◄</kbd>: poprzedni konflikt
|
||||
<kbd>►</kbd>: następny konflikt
|
||||
<kbd>▲</kbd>: wybierz poprzedni kawałek
|
||||
<kbd>▼</kbd>: wybierz następny kawałek
|
||||
<kbd>z</kbd>: cofnij
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Główne Panel (Zwykłe)
|
||||
## Zwykłe
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: przewiń w dół (fn+up)
|
||||
<kbd>ő</kbd>: przewiń w górę (fn+down)
|
||||
</pre>
|
||||
|
||||
## Główne Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wyście z trybu "linia po linii"
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: poprzednia linia
|
||||
<kbd>▼</kbd>: następna linia
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
</pre>
|
||||
|
||||
## Główne Panel (Poczekalnia)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: poprzednia linia
|
||||
<kbd>▼</kbd>: następna linia
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>c</kbd>: Zatwierdź zmiany
|
||||
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
</pre>
|
||||
|
||||
## Schowek Panel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj konfigurację
|
||||
<kbd>o</kbd>: otwórz konfigurację
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: pokaż wszystkie logi gałęzi
|
||||
<kbd>mouse wheel ▼</kbd>: przewiń w dół (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: przewiń w górę (fn+down)
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -5,204 +5,95 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
## 全局键绑定
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: 切换到最近的仓库 (<c-r>)
|
||||
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up)
|
||||
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down)
|
||||
<kbd>ctrl+r</kbd>: 切换到最近的仓库
|
||||
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down/shift+j)
|
||||
<kbd>m</kbd>: 查看 合并/变基 选项
|
||||
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
|
||||
<kbd>P</kbd>: 推送
|
||||
<kbd>p</kbd>: 拉取
|
||||
<kbd>R</kbd>: 刷新
|
||||
<kbd>x</kbd>: 打开菜单
|
||||
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」
|
||||
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
|
||||
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
|
||||
<kbd>_</kbd>: 上一屏模式
|
||||
<kbd>:</kbd>: 执行自定义命令
|
||||
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
|
||||
<kbd>W</kbd>: 打开 diff 菜单
|
||||
<kbd>ctrl+e</kbd>: 打开 diff 菜单
|
||||
<kbd>@</kbd>: 打开命令日志菜单
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
|
||||
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
|
||||
<kbd>:</kbd>: 执行自定义命令
|
||||
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」
|
||||
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
|
||||
<kbd>P</kbd>: 推送
|
||||
<kbd>p</kbd>: 拉取
|
||||
</pre>
|
||||
|
||||
## 列表面板导航
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: 下一页
|
||||
<kbd>,</kbd>: 上一页
|
||||
<kbd>.</kbd>: 下一页
|
||||
<kbd><</kbd>: 滚动到顶部
|
||||
<kbd>></kbd>: 滚动到底部
|
||||
<kbd>/</kbd>: 开始搜索
|
||||
<kbd>></kbd>: 滚动到底部
|
||||
<kbd>H</kbd>: 向左滚动
|
||||
<kbd>L</kbd>: 向右滚动
|
||||
<kbd>]</kbd>: 下一个标签
|
||||
<kbd>[</kbd>: 上一个标签
|
||||
</pre>
|
||||
|
||||
## 分支 面板 (分支标签)
|
||||
## Reflog 页面
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: 在浏览器中打开提交
|
||||
<kbd>n</kbd>: 从提交创建新分支
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 分支页面
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
|
||||
<kbd>i</kbd>: 显示 git-flow 选项
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>o</kbd>: 创建抓取请求
|
||||
<kbd>O</kbd>: 创建抓取请求
|
||||
<kbd>ctrl+y</kbd>: 将抓取请求 URL 复制到剪贴板
|
||||
<kbd>c</kbd>: 按名称检出
|
||||
<kbd>F</kbd>: 强制检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>d</kbd>: 删除分支
|
||||
<kbd>r</kbd>: 将已检出的分支变基到该分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
<kbd>i</kbd>: 显示 git-flow 选项
|
||||
<kbd>f</kbd>: 从上游快进此分支
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>R</kbd>: 重命名分支
|
||||
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 分支 面板 (远程分支(在远程页面中))
|
||||
## 子提交
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 返回远程仓库列表
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
<kbd>d</kbd>: 删除分支
|
||||
<kbd>r</kbd>: 将已检出的分支变基到该分支
|
||||
<kbd>u</kbd>: 设置为检出分支的上游
|
||||
</pre>
|
||||
|
||||
## 分支 面板 (远程页面)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: 抓取远程仓库
|
||||
<kbd>n</kbd>: 添加新的远程仓库
|
||||
<kbd>d</kbd>: 删除远程
|
||||
<kbd>e</kbd>: 编辑远程仓库
|
||||
</pre>
|
||||
|
||||
## 分支 面板 (子提交)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
</pre>
|
||||
|
||||
## 分支 面板 (标签页面)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd>d</kbd>: 删除标签
|
||||
<kbd>P</kbd>: 推送标签
|
||||
<kbd>n</kbd>: 创建标签
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 提交文件 面板
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的文件名复制到剪贴板
|
||||
<kbd>c</kbd>: 检出文件
|
||||
<kbd>d</kbd>: 放弃对此文件的提交更改
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>space</kbd>: 补丁中包含的切换文件
|
||||
<kbd>enter</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
</pre>
|
||||
|
||||
## 提交 面板 (提交)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+l</kbd>: open log menu
|
||||
<kbd>s</kbd>: 向下压缩
|
||||
<kbd>r</kbd>: 改写提交
|
||||
<kbd>R</kbd>: 使用编辑器重命名提交
|
||||
<kbd>g</kbd>: 重置为此提交
|
||||
<kbd>f</kbd>: 修正提交(fixup)
|
||||
<kbd>F</kbd>: 为此提交创建修正
|
||||
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
|
||||
<kbd>d</kbd>: 删除提交
|
||||
<kbd>ctrl+j</kbd>: 下移提交
|
||||
<kbd>ctrl+k</kbd>: 上移提交
|
||||
<kbd>e</kbd>: 编辑提交
|
||||
<kbd>A</kbd>: 用已暂存的更改来修补提交
|
||||
<kbd>p</kbd>: 选择提交(变基过程中)
|
||||
<kbd>t</kbd>: 还原提交
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>v</kbd>: 粘贴提交(拣选)
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: 在浏览器中打开提交
|
||||
<kbd>n</kbd>: 从提交创建新分支
|
||||
<kbd>T</kbd>: 标签提交
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
</pre>
|
||||
|
||||
## 提交 面板 (Reflog)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## Extras 面板
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: 打开命令日志菜单
|
||||
</pre>
|
||||
|
||||
## 文件 面板
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+b</kbd>: 过滤提交文件
|
||||
</pre>
|
||||
|
||||
## 文件 面板 (文件)
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: 提交更改
|
||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||
<kbd>A</kbd>: 修补最后一次提交
|
||||
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
||||
<kbd>space</kbd>: 切换暂存状态
|
||||
<kbd>d</kbd>: 查看'放弃更改‘选项
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>i</kbd>: 添加到 .gitignore
|
||||
<kbd>r</kbd>: 刷新文件
|
||||
<kbd>s</kbd>: 将所有更改加入贮藏
|
||||
<kbd>S</kbd>: 查看隐藏选项
|
||||
<kbd>a</kbd>: 切换所有文件的暂存状态
|
||||
<kbd>D</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
|
||||
<kbd>f</kbd>: 抓取
|
||||
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
|
||||
<kbd>g</kbd>: 查看上游重置选项
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
<kbd>M</kbd>: 打开合并工具
|
||||
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白更改
|
||||
</pre>
|
||||
|
||||
## 文件 面板 (子模块)
|
||||
## 子模块
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将子模块名称复制到剪贴板
|
||||
|
|
@ -215,89 +106,148 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>b</kbd>: 查看批量子模块选项
|
||||
</pre>
|
||||
|
||||
## 主要 面板 (合并中)
|
||||
## 提交
|
||||
|
||||
<pre>
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd>M</kbd>: 打开合并工具
|
||||
<kbd>space</kbd>: 选中区块
|
||||
<kbd>b</kbd>: 选中所有区块
|
||||
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
|
||||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>b</kbd>: 查看二分查找选项
|
||||
<kbd>s</kbd>: 向下压缩
|
||||
<kbd>f</kbd>: 修正提交(fixup)
|
||||
<kbd>r</kbd>: 改写提交
|
||||
<kbd>R</kbd>: 使用编辑器重命名提交
|
||||
<kbd>d</kbd>: 删除提交
|
||||
<kbd>e</kbd>: 编辑提交
|
||||
<kbd>p</kbd>: 选择提交(变基过程中)
|
||||
<kbd>F</kbd>: 为此提交创建修正
|
||||
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
|
||||
<kbd>ctrl+j</kbd>: 下移提交
|
||||
<kbd>ctrl+k</kbd>: 上移提交
|
||||
<kbd>v</kbd>: 粘贴提交(拣选)
|
||||
<kbd>A</kbd>: 用已暂存的更改来修补提交
|
||||
<kbd>a</kbd>: reset commit author
|
||||
<kbd>t</kbd>: 还原提交
|
||||
<kbd>T</kbd>: 标签提交
|
||||
<kbd>ctrl+l</kbd>: 打开日志菜单
|
||||
<kbd>space</kbd>: 检出提交
|
||||
<kbd>y</kbd>: copy commit attribute
|
||||
<kbd>o</kbd>: 在浏览器中打开提交
|
||||
<kbd>n</kbd>: 从提交创建新分支
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>c</kbd>: 复制提交(拣选)
|
||||
<kbd>C</kbd>: 复制提交范围(拣选)
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## 提交文件
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将提交的文件名复制到剪贴板
|
||||
<kbd>c</kbd>: 检出文件
|
||||
<kbd>d</kbd>: 放弃对此文件的提交更改
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>space</kbd>: 补丁中包含的切换文件
|
||||
<kbd>a</kbd>: toggle all files included in patch
|
||||
<kbd>enter</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
</pre>
|
||||
|
||||
## 文件
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
|
||||
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
|
||||
<kbd>d</kbd>: 查看'放弃更改'选项
|
||||
<kbd>space</kbd>: 切换暂存状态
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
<kbd>c</kbd>: 提交更改
|
||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||
<kbd>A</kbd>: 修补最后一次提交
|
||||
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>i</kbd>: 忽略文件
|
||||
<kbd>r</kbd>: 刷新文件
|
||||
<kbd>s</kbd>: 将所有更改加入贮藏
|
||||
<kbd>S</kbd>: 查看贮藏选项
|
||||
<kbd>a</kbd>: 切换所有文件的暂存状态
|
||||
<kbd>enter</kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
|
||||
<kbd>g</kbd>: 查看上游重置选项
|
||||
<kbd>D</kbd>: 查看重置选项
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
|
||||
<kbd>f</kbd>: 抓取
|
||||
</pre>
|
||||
|
||||
## 构建补丁中
|
||||
|
||||
<pre>
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>space</kbd>: 添加/移除 行到补丁
|
||||
<kbd>esc</kbd>: 退出逐行模式
|
||||
</pre>
|
||||
|
||||
## 标签页面
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd>d</kbd>: 删除标签
|
||||
<kbd>P</kbd>: 推送标签
|
||||
<kbd>n</kbd>: 创建标签
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 正在合并
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>◄</kbd>: 选择上一个冲突
|
||||
<kbd>►</kbd>: 选择下一个冲突
|
||||
<kbd>▲</kbd>: 选择顶部块
|
||||
<kbd>▼</kbd>: 选择底部块
|
||||
<kbd>z</kbd>: 撤销
|
||||
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
|
||||
<kbd>space</kbd>: 选中区块
|
||||
<kbd>b</kbd>: 选中所有区块
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
</pre>
|
||||
|
||||
## 主要 面板 (正常)
|
||||
## 正在暂存
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: 向下滚动 (fn+up)
|
||||
<kbd>ő</kbd>: 向上滚动 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 主要 面板 (构建补丁中)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 退出逐行模式
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>▲</kbd>: 选择上一行
|
||||
<kbd>▼</kbd>: 选择下一行
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: 添加/移除 行到补丁
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
</pre>
|
||||
|
||||
## 主要 面板 (正在暂存)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd>tab</kbd>: 切换到其他面板
|
||||
<kbd>space</kbd>: 切换行暂存状态
|
||||
<kbd>d</kbd>: 取消变更 (git reset)
|
||||
<kbd>tab</kbd>: 切换到其他面板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>▲</kbd>: 选择上一行
|
||||
<kbd>▼</kbd>: 选择下一行
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>c</kbd>: 提交更改
|
||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
</pre>
|
||||
|
||||
## 菜单 面板
|
||||
## 正常
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 关闭菜单
|
||||
<kbd>mouse wheel ▼</kbd>: 向下滚动 (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 向上滚动 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 贮藏 面板
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 查看贮藏条目中的文件
|
||||
<kbd>space</kbd>: 应用
|
||||
<kbd>g</kbd>: 应用并删除
|
||||
<kbd>d</kbd>: 删除
|
||||
<kbd>n</kbd>: 新分支
|
||||
</pre>
|
||||
|
||||
## 状态 面板
|
||||
## 状态
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 编辑配置文件
|
||||
|
|
@ -306,3 +256,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>enter</kbd>: 切换到最近的仓库
|
||||
<kbd>a</kbd>: 显示所有分支的日志
|
||||
</pre>
|
||||
|
||||
## 贮藏
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 应用
|
||||
<kbd>g</kbd>: 应用并删除
|
||||
<kbd>d</kbd>: 删除
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## 远程分支
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
<kbd>r</kbd>: 将已检出的分支变基到该分支
|
||||
<kbd>d</kbd>: 删除分支
|
||||
<kbd>u</kbd>: 设置为检出分支的上游
|
||||
<kbd>esc</kbd>: 返回远程仓库列表
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>enter</kbd>: 查看提交
|
||||
</pre>
|
||||
|
||||
## 远程页面
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: 抓取远程仓库
|
||||
<kbd>n</kbd>: 添加新的远程仓库
|
||||
<kbd>d</kbd>: 删除远程
|
||||
<kbd>e</kbd>: 编辑远程仓库
|
||||
</pre>
|
||||
|
|
|
|||
62
go.mod
62
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module github.com/jesseduffield/lazygit
|
||||
|
||||
go 1.14
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0
|
||||
|
|
@ -9,42 +9,66 @@ require (
|
|||
github.com/cli/safeexec v1.0.0
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b // indirect
|
||||
github.com/go-errors/errors v1.4.1
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
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.20220108045521-1945d7b9ed8b
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220813101052-3a3ab26faa15
|
||||
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
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/kyokomi/emoji/v2 v2.2.8
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/samber/lo v1.10.1
|
||||
github.com/sanity-io/litter v1.5.2
|
||||
github.com/sasha-s/go-deadlock v0.3.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
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/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
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/rivo/uniseg v0.3.4 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||
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-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
|
||||
)
|
||||
|
|
|
|||
50
go.sum
50
go.sum
|
|
@ -27,21 +27,21 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3
|
|||
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b h1:eoaSI4eEwM5eTx/HvmRSwmicxuMhL73AyoEfM1oCJLc=
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b/go.mod h1:ZPwXnysybtQqdqKcWMWXux9aGdtMHe+kr+cwEZEe+A4=
|
||||
github.com/gdamore/tcell/v2 v2.5.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY=
|
||||
github.com/gdamore/tcell/v2 v2.5.2/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
|
||||
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
|
|
@ -53,11 +53,7 @@ github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih
|
|||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
|
|
@ -72,10 +68,14 @@ github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM
|
|||
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8TIcC6Y4RI+1ZbJDOHfGJ570tPeYVCqo7/tws=
|
||||
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.20220108045521-1945d7b9ed8b h1:AUK5nDiPiaahBtGIsf8rITgZ9SC+uddvnNKs0/mrYA8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220108045521-1945d7b9ed8b/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220813101052-3a3ab26faa15 h1:DTVj8aCmINqLj5AXBEGmpWwfN1HJ3EWtUiYfcyIaSxs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220813101052-3a3ab26faa15/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=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
|
|
@ -125,18 +125,25 @@ github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
|
|||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
|
||||
github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/samber/lo v1.10.1 h1:0D3h7i0U3hRAbaCeQ82DLe67n0A7Bbl0/cEoWqFGp+U=
|
||||
github.com/samber/lo v1.10.1/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
||||
github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87myw=
|
||||
github.com/sanity-io/litter v1.5.2/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
|
|
@ -152,6 +159,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
|
|
@ -163,6 +171,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -181,20 +191,19 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-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-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
@ -212,5 +221,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
143
main.go
143
main.go
|
|
@ -1,151 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
// These values may be set by the build script via the LDFLAGS argument
|
||||
var (
|
||||
commit string
|
||||
version = "unversioned"
|
||||
date string
|
||||
version string
|
||||
buildSource = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
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")
|
||||
|
||||
dump := ""
|
||||
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
|
||||
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
|
||||
|
||||
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 seperated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
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")
|
||||
ldFlagsBuildInfo := &app.BuildInfo{
|
||||
Commit: commit,
|
||||
Date: date,
|
||||
Version: version,
|
||||
BuildSource: buildSource,
|
||||
}
|
||||
|
||||
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 {
|
||||
app.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
if err := os.Chdir(workTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
app, err := app.NewApp(appConfig, filterPath)
|
||||
|
||||
if err == nil {
|
||||
err = app.Run()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errorMessage, known := app.KnownError(err); known {
|
||||
log.Fatal(errorMessage)
|
||||
}
|
||||
newErr := errors.Wrap(err, 0)
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
|
||||
}
|
||||
app.Start(ldFlagsBuildInfo, nil)
|
||||
}
|
||||
|
|
|
|||
317
pkg/app/app.go
317
pkg/app/app.go
|
|
@ -2,10 +2,8 @@ package app
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -13,117 +11,91 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// App struct
|
||||
// App is the struct that's instantiated from within main.go and it manages
|
||||
// bootstrapping and running the application.
|
||||
type App struct {
|
||||
*common.Common
|
||||
closers []io.Closer
|
||||
Config config.AppConfigurer
|
||||
OSCommand *oscommands.OSCommand
|
||||
Gui *gui.Gui
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
ClientContext string
|
||||
closers []io.Closer
|
||||
Config config.AppConfigurer
|
||||
OSCommand *oscommands.OSCommand
|
||||
Gui *gui.Gui
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
}
|
||||
|
||||
type errorMapping struct {
|
||||
originalError string
|
||||
newError string
|
||||
}
|
||||
func Run(
|
||||
config config.AppConfigurer,
|
||||
common *common.Common,
|
||||
startArgs appTypes.StartArgs,
|
||||
) {
|
||||
app, err := NewApp(config, common)
|
||||
|
||||
func newProductionLogger() *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
return log
|
||||
}
|
||||
if err == nil {
|
||||
err = app.Run(startArgs)
|
||||
}
|
||||
|
||||
func getLogLevel() logrus.Level {
|
||||
strLevel := os.Getenv("LOG_LEVEL")
|
||||
level, err := logrus.ParseLevel(strLevel)
|
||||
if err != nil {
|
||||
return logrus.DebugLevel
|
||||
if errorMessage, known := knownError(common.Tr, err); known {
|
||||
log.Fatal(errorMessage)
|
||||
}
|
||||
newErr := errors.Wrap(err, 0)
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatalf("%s: %s\n\n%s", common.Tr.ErrorOccurred, constants.Links.Issues, stackTrace)
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func newDevelopmentLogger() *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(getLogLevel())
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to log to log file: %v", err)
|
||||
}
|
||||
logger.SetOutput(file)
|
||||
return logger
|
||||
}
|
||||
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
|
||||
log = newDevelopmentLogger()
|
||||
} else {
|
||||
log = newProductionLogger()
|
||||
}
|
||||
|
||||
// highly recommended: tail -f development.log | humanlog
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
return log.WithFields(logrus.Fields{
|
||||
"debug": config.GetDebug(),
|
||||
"version": config.GetVersion(),
|
||||
"commit": config.GetCommit(),
|
||||
"buildDate": config.GetBuildDate(),
|
||||
})
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
func NewCommon(config config.AppConfigurer) (*common.Common, error) {
|
||||
userConfig := config.GetUserConfig()
|
||||
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
}
|
||||
var err error
|
||||
log := newLogger(config)
|
||||
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
|
||||
if err != nil {
|
||||
return app, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
app.Common = &common.Common{
|
||||
return &common.Common{
|
||||
Log: log,
|
||||
Tr: tr,
|
||||
UserConfig: userConfig,
|
||||
Debug: config.GetDebug(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
Common: common,
|
||||
}
|
||||
|
||||
// if we are being called in 'demon' mode, we can just return here
|
||||
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
||||
if app.ClientContext != "" {
|
||||
return app, nil
|
||||
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
|
||||
|
||||
var err error
|
||||
app.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.OSCommand = oscommands.NewOSCommand(app.Common, oscommands.GetPlatform(), oscommands.NewNullGuiIO(log))
|
||||
|
||||
app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
|
||||
dirName, err := os.Getwd()
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
|
@ -135,7 +107,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
|||
|
||||
gitConfig := git_config.NewStdCachedGitConfig(app.Log)
|
||||
|
||||
app.Gui, err = gui.NewGui(app.Common, config, gitConfig, app.Updater, filterPath, showRecentRepos)
|
||||
app.Gui, err = gui.NewGui(common, config, gitConfig, app.Updater, showRecentRepos, dirName)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
|
@ -200,27 +172,9 @@ func isGitVersionValid(versionStr string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func isGhVersionValid(versionStr string) bool {
|
||||
// output should be something like:
|
||||
// gh version 2.0.0 (2021-08-23)
|
||||
// https://github.com/cli/cli/releases/tag/v2.0.0
|
||||
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
|
||||
matches := re.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
ghVersion := matches[1]
|
||||
majorVersion, err := strconv.Atoi(ghVersion[0:1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if majorVersion < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
func isDirectoryAGitRepository(dir string) (bool, error) {
|
||||
info, err := os.Stat(filepath.Join(dir, ".git"))
|
||||
return info != nil, err
|
||||
}
|
||||
|
||||
func (app *App) setupRepo() (bool, error) {
|
||||
|
|
@ -239,146 +193,69 @@ func (app *App) setupRepo() (bool, error) {
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
info, _ := os.Stat(filepath.Join(cwd, ".git"))
|
||||
if info != nil && info.IsDir() {
|
||||
return false, err // Current directory appears to be a git repository.
|
||||
if isRepo, err := isDirectoryAGitRepository(cwd); isRepo {
|
||||
return false, err
|
||||
}
|
||||
|
||||
shouldInitRepo := true
|
||||
notARepository := app.UserConfig.NotARepository
|
||||
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, " \n") != "y" {
|
||||
shouldInitRepo = false
|
||||
}
|
||||
} else if notARepository == "skip" {
|
||||
shouldInitRepo = false
|
||||
}
|
||||
|
||||
if !shouldInitRepo {
|
||||
// check if we have a recent repo we can open
|
||||
recentRepos := app.Config.GetAppState().RecentRepos
|
||||
if len(recentRepos) > 0 {
|
||||
var err error
|
||||
// try opening each repo in turn, in case any have been deleted
|
||||
for _, repoDir := range recentRepos {
|
||||
if err = os.Chdir(repoDir); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
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 {
|
||||
initialBranchArg += "--initial-branch=" + app.OSCommand.Quote(trimmedResponse)
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
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 err := app.OSCommand.Cmd.New("git init").Run(); err != nil {
|
||||
return false, err
|
||||
|
||||
if shouldInitRepo {
|
||||
if err := app.OSCommand.Cmd.New("git init " + initialBranchArg).Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
if app.ClientContext == "INTERACTIVE_REBASE" {
|
||||
return app.Rebase()
|
||||
}
|
||||
|
||||
if app.ClientContext == "EXIT_IMMEDIATELY" {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err := app.Gui.RunAndHandleError()
|
||||
func (app *App) Run(startArgs appTypes.StartArgs) error {
|
||||
err := app.Gui.RunAndHandleError(startArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// Rebase contains logic for when we've been run in demon mode, meaning we've
|
||||
// given lazygit as a command for git to call e.g. to edit a file
|
||||
func (app *App) Rebase() error {
|
||||
app.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
app.Log.Info("args: ", os.Args)
|
||||
|
||||
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
|
||||
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
|
||||
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
|
||||
// but in this case we don't need to edit it, so we'll just return
|
||||
} else {
|
||||
app.Log.Info("Lazygit demon did not match on any use cases")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes any resources
|
||||
func (app *App) Close() error {
|
||||
for _, closer := range app.closers {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
|
||||
func (app *App) KnownError(err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
knownErrorMessages := []string{app.Tr.MinGitVersionError}
|
||||
|
||||
for _, message := range knownErrorMessages {
|
||||
if errorMessage == message {
|
||||
return message, true
|
||||
}
|
||||
}
|
||||
|
||||
mappings := []errorMapping{
|
||||
{
|
||||
originalError: "fatal: not a git repository",
|
||||
newError: app.Tr.NotARepository,
|
||||
},
|
||||
}
|
||||
|
||||
for _, mapping := range mappings {
|
||||
if strings.Contains(errorMessage, mapping.originalError) {
|
||||
return mapping.newError, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func TailLogs() {
|
||||
logFilePath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Tailing log file %s\n\n", logFilePath)
|
||||
|
||||
opts := humanlog.DefaultOptions
|
||||
opts.Truncates = false
|
||||
|
||||
_, err = os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatal("Log file does not exist. Run `lazygit --debug` first to create the log file")
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
TailLogsForPlatform(logFilePath, opts)
|
||||
return slices.TryForEach(app.closers, func(closer io.Closer) error {
|
||||
return closer.Close()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
107
pkg/app/daemon/daemon.go
Normal file
107
pkg/app/daemon/daemon.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
)
|
||||
|
||||
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
|
||||
// We do this when git lets us supply a program to run within a git command.
|
||||
// For example, if we want to ensure that a git command doesn't hang due to
|
||||
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
|
||||
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
|
||||
// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit
|
||||
// as a daemon which simply exits immediately. Any additional arguments we want
|
||||
// to pass to a daemon can be done via other env vars.
|
||||
|
||||
type DaemonKind string
|
||||
|
||||
const (
|
||||
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
|
||||
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
|
||||
)
|
||||
|
||||
const (
|
||||
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
|
||||
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
|
||||
)
|
||||
|
||||
type Daemon interface {
|
||||
Run() error
|
||||
}
|
||||
|
||||
func Handle(common *common.Common) {
|
||||
d := getDaemon(common)
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := d.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func InDaemonMode() bool {
|
||||
return getDaemonKind() != ""
|
||||
}
|
||||
|
||||
func getDaemon(common *common.Common) Daemon {
|
||||
switch getDaemonKind() {
|
||||
case InteractiveRebase:
|
||||
return &rebaseDaemon{c: common}
|
||||
case ExitImmediately:
|
||||
return &exitImmediatelyDaemon{c: common}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDaemonKind() DaemonKind {
|
||||
return DaemonKind(os.Getenv(DaemonKindEnvKey))
|
||||
}
|
||||
|
||||
type rebaseDaemon struct {
|
||||
c *common.Common
|
||||
}
|
||||
|
||||
func (self *rebaseDaemon) Run() error {
|
||||
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
self.c.Log.Info("args: ", os.Args)
|
||||
|
||||
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
|
||||
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
|
||||
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
|
||||
// but in this case we don't need to edit it, so we'll just return
|
||||
} else {
|
||||
self.c.Log.Info("Lazygit demon did not match on any use cases")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
type exitImmediatelyDaemon struct {
|
||||
c *common.Common
|
||||
}
|
||||
|
||||
func (self *exitImmediatelyDaemon) Run() error {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
39
pkg/app/errors.go
Normal file
39
pkg/app/errors.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
)
|
||||
|
||||
type errorMapping struct {
|
||||
originalError string
|
||||
newError string
|
||||
}
|
||||
|
||||
// knownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
|
||||
func knownError(tr *i18n.TranslationSet, err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
knownErrorMessages := []string{tr.MinGitVersionError}
|
||||
|
||||
if slices.Contains(knownErrorMessages, errorMessage) {
|
||||
return errorMessage, true
|
||||
}
|
||||
|
||||
mappings := []errorMapping{
|
||||
{
|
||||
originalError: "fatal: not a git repository",
|
||||
newError: tr.NotARepository,
|
||||
},
|
||||
}
|
||||
|
||||
if mapping, ok := slices.Find(mappings, func(mapping errorMapping) bool {
|
||||
return strings.Contains(errorMessage, mapping.originalError)
|
||||
}); ok {
|
||||
return mapping.newError, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
|
@ -1,31 +1,56 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
|
||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
if config.GetDebug() {
|
||||
log = newDevelopmentLogger()
|
||||
} else {
|
||||
log = newProductionLogger()
|
||||
}
|
||||
|
||||
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// highly recommended: tail -f development.log | humanlog
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
return log.WithFields(logrus.Fields{})
|
||||
}
|
||||
|
||||
func newProductionLogger() *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
return log
|
||||
}
|
||||
|
||||
func newDevelopmentLogger() *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(getLogLevel())
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to log to log file: %v", err)
|
||||
}
|
||||
logger.SetOutput(file)
|
||||
return logger
|
||||
}
|
||||
|
||||
func getLogLevel() logrus.Level {
|
||||
strLevel := os.Getenv("LOG_LEVEL")
|
||||
level, err := logrus.ParseLevel(strLevel)
|
||||
if err != nil {
|
||||
return logrus.DebugLevel
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/aybabtme/humanlog"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
|
||||
var lastModified int64 = 0
|
||||
var lastOffset int64 = 0
|
||||
for {
|
||||
stat, err := os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if stat.ModTime().Unix() > lastModified {
|
||||
err = TailFrom(lastOffset, logFilePath, opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
lastOffset = stat.Size()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func OpenAndSeek(filepath string, offset int64) (*os.File, error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = file.Seek(offset, 0)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func TailFrom(lastOffset int64, logFilePath string, opts *humanlog.HandlerOptions) error {
|
||||
file, err := OpenAndSeek(logFilePath, lastOffset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
var lines []string
|
||||
for fileScanner.Scan() {
|
||||
lines = append(lines, fileScanner.Text())
|
||||
}
|
||||
file.Close()
|
||||
lineCount := len(lines)
|
||||
lastTen := lines
|
||||
if lineCount > 10 {
|
||||
lastTen = lines[lineCount-10:]
|
||||
}
|
||||
for _, line := range lastTen {
|
||||
reader := strings.NewReader(line)
|
||||
if err := humanlog.Scanner(reader, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
33
pkg/app/types/types.go
Normal file
33
pkg/app/types/types.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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 {
|
||||
// FilterPath determines which path we're going to filter on so that we only see commits from that file.
|
||||
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
|
||||
|
||||
const (
|
||||
GitArgNone GitArg = ""
|
||||
GitArgStatus GitArg = "status"
|
||||
GitArgBranch GitArg = "branch"
|
||||
GitArgLog GitArg = "log"
|
||||
GitArgStash GitArg = "stash"
|
||||
)
|
||||
|
||||
func NewStartArgs(filterPath string, gitArg GitArg, test integrationTypes.IntegrationTest) StartArgs {
|
||||
return StartArgs{
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
IntegrationTest: test,
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,11 @@ func Check() {
|
|||
tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet")
|
||||
err := os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
|
||||
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||
}
|
||||
err = os.Mkdir(tmpDir, 0700)
|
||||
err = os.Mkdir(tmpDir, 0o700)
|
||||
if err != nil {
|
||||
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
|
||||
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||
}
|
||||
|
||||
generateAtDir(tmpDir)
|
||||
|
|
@ -45,9 +45,9 @@ func Check() {
|
|||
Context: 1,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
|
||||
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||
}
|
||||
fmt.Printf("\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes\n", CommandToRun())
|
||||
fmt.Printf("\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes. If you run the script and no keybindings files are updated as a result, try rebasing onto master and trying again.\n", CommandToRun())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ func obtainContent(dir string) string {
|
|||
if re.MatchString(path) {
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
|
||||
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||
}
|
||||
content += fmt.Sprintf("\n%s\n\n", filepath.Base(path))
|
||||
content += string(bytes)
|
||||
|
|
@ -70,9 +70,8 @@ func obtainContent(dir string) string {
|
|||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
|
||||
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||
}
|
||||
|
||||
return content
|
||||
|
|
|
|||
|
|
@ -12,18 +12,32 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"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"
|
||||
)
|
||||
|
||||
type bindingSection struct {
|
||||
title string
|
||||
bindings []*gui.Binding
|
||||
bindings []*types.Binding
|
||||
}
|
||||
|
||||
type header struct {
|
||||
// priority decides the order of the headers in the cheatsheet (lower means higher)
|
||||
priority int
|
||||
title string
|
||||
}
|
||||
|
||||
type headerWithBindings struct {
|
||||
header header
|
||||
bindings []*types.Binding
|
||||
}
|
||||
|
||||
func CommandToRun() string {
|
||||
|
|
@ -31,25 +45,28 @@ func CommandToRun() string {
|
|||
}
|
||||
|
||||
func GetDir() string {
|
||||
return integration.GetRootDirectory() + "/docs/keybindings"
|
||||
return utils.GetLazygitRootDirectory() + "/docs/keybindings"
|
||||
}
|
||||
|
||||
func generateAtDir(cheatsheetDir string) {
|
||||
os.Setenv("LANG", "en")
|
||||
|
||||
translationSetsByLang := i18n.GetTranslationSets()
|
||||
mConfig := config.NewDummyAppConfig()
|
||||
|
||||
for lang := range translationSetsByLang {
|
||||
os.Setenv("LC_ALL", lang)
|
||||
mApp, _ := app.NewApp(mConfig, "")
|
||||
mConfig.GetUserConfig().Gui.Language = lang
|
||||
common, err := app.NewCommon(mConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mApp, _ := app.NewApp(mConfig, common)
|
||||
path := cheatsheetDir + "/Keybindings_" + lang + ".md"
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bindingSections := getBindingSections(mApp)
|
||||
bindings := mApp.Gui.GetCheatsheetKeybindings()
|
||||
bindingSections := getBindingSections(bindings, mApp.Tr)
|
||||
content := formatSections(mApp.Tr, bindingSections)
|
||||
content = fmt.Sprintf("_This file is auto-generated. To update, make the changes in the "+
|
||||
"pkg/i18n directory and then run `%s` from the project root._\n\n%s", CommandToRun(), content)
|
||||
|
|
@ -68,9 +85,7 @@ func writeString(file *os.File, str string) {
|
|||
}
|
||||
}
|
||||
|
||||
func localisedTitle(mApp *app.App, str string) string {
|
||||
tr := mApp.Tr
|
||||
|
||||
func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
contextTitleMap := map[string]string{
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
|
|
@ -88,12 +103,10 @@ func localisedTitle(mApp *app.App, str string) string {
|
|||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"credentials": tr.CredentialsTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.MainTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"merging": tr.MergingTitle,
|
||||
"normal": tr.NormalTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
|
|
@ -111,142 +124,59 @@ func localisedTitle(mApp *app.App, str string) string {
|
|||
return title
|
||||
}
|
||||
|
||||
func formatTitle(title string) string {
|
||||
return fmt.Sprintf("\n## %s\n\n", title)
|
||||
}
|
||||
|
||||
func formatBinding(binding *gui.Binding) string {
|
||||
if binding.Alternative != "" {
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative)
|
||||
}
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", gui.GetKeyDisplay(binding.Key), binding.Description)
|
||||
}
|
||||
|
||||
func getBindingSections(mApp *app.App) []*bindingSection {
|
||||
bindingSections := []*bindingSection{}
|
||||
|
||||
bindings := mApp.Gui.GetInitialKeybindings()
|
||||
|
||||
type contextAndViewType struct {
|
||||
subtitle string
|
||||
title string
|
||||
}
|
||||
|
||||
contextAndViewBindingMap := map[contextAndViewType][]*gui.Binding{}
|
||||
|
||||
outer:
|
||||
for _, binding := range bindings {
|
||||
if binding.Tag == "navigation" {
|
||||
key := contextAndViewType{subtitle: "", title: "navigation"}
|
||||
existing := contextAndViewBindingMap[key]
|
||||
if existing == nil {
|
||||
contextAndViewBindingMap[key] = []*gui.Binding{binding}
|
||||
} else {
|
||||
for _, navBinding := range contextAndViewBindingMap[key] {
|
||||
if navBinding.Description == binding.Description {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding)
|
||||
}
|
||||
|
||||
continue outer
|
||||
}
|
||||
|
||||
contexts := []string{}
|
||||
if len(binding.Contexts) == 0 {
|
||||
contexts = append(contexts, "")
|
||||
} else {
|
||||
contexts = append(contexts, binding.Contexts...)
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
key := contextAndViewType{subtitle: context, title: binding.ViewName}
|
||||
existing := contextAndViewBindingMap[key]
|
||||
if existing == nil {
|
||||
contextAndViewBindingMap[key] = []*gui.Binding{binding}
|
||||
} else {
|
||||
contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type groupedBindingsType struct {
|
||||
contextAndView contextAndViewType
|
||||
bindings []*gui.Binding
|
||||
}
|
||||
|
||||
groupedBindings := make([]groupedBindingsType, len(contextAndViewBindingMap))
|
||||
|
||||
for contextAndView, contextBindings := range contextAndViewBindingMap {
|
||||
groupedBindings = append(groupedBindings, groupedBindingsType{contextAndView: contextAndView, bindings: contextBindings})
|
||||
}
|
||||
|
||||
sort.Slice(groupedBindings, func(i, j int) bool {
|
||||
first := groupedBindings[i].contextAndView
|
||||
second := groupedBindings[j].contextAndView
|
||||
if first.title == "" {
|
||||
return true
|
||||
}
|
||||
if second.title == "" {
|
||||
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
|
||||
excludedViews := []string{"stagingSecondary", "patchBuildingSecondary"}
|
||||
bindingsToDisplay := slices.Filter(bindings, func(binding *types.Binding) bool {
|
||||
if lo.Contains(excludedViews, binding.ViewName) {
|
||||
return false
|
||||
}
|
||||
if first.title == "navigation" {
|
||||
return true
|
||||
}
|
||||
if second.title == "navigation" {
|
||||
return false
|
||||
}
|
||||
return first.title < second.title || (first.title == second.title && first.subtitle < second.subtitle)
|
||||
|
||||
return (binding.Description != "" || binding.Alternative != "")
|
||||
})
|
||||
|
||||
for _, group := range groupedBindings {
|
||||
contextAndView := group.contextAndView
|
||||
contextBindings := group.bindings
|
||||
mApp.Log.Info("viewname: " + contextAndView.title + ", context: " + contextAndView.subtitle)
|
||||
viewName := contextAndView.title
|
||||
if viewName == "" {
|
||||
viewName = "global"
|
||||
}
|
||||
translatedView := localisedTitle(mApp, viewName)
|
||||
var title string
|
||||
if contextAndView.subtitle == "" {
|
||||
addendum := " " + mApp.Tr.Panel
|
||||
if viewName == "global" || viewName == "navigation" {
|
||||
addendum = ""
|
||||
bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header {
|
||||
return getHeader(binding, tr)
|
||||
})
|
||||
|
||||
bindingGroups := maps.MapToSlice(
|
||||
bindingsByHeader,
|
||||
func(header header, hBindings []*types.Binding) headerWithBindings {
|
||||
uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string {
|
||||
return binding.Description + keybindings.LabelFromKey(binding.Key)
|
||||
})
|
||||
|
||||
return headerWithBindings{
|
||||
header: header,
|
||||
bindings: uniqBindings,
|
||||
}
|
||||
title = fmt.Sprintf("%s%s", translatedView, addendum)
|
||||
} else {
|
||||
translatedContextName := localisedTitle(mApp, contextAndView.subtitle)
|
||||
title = fmt.Sprintf("%s %s (%s)", translatedView, mApp.Tr.Panel, translatedContextName)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
for _, binding := range contextBindings {
|
||||
bindingSections = addBinding(title, bindingSections, binding)
|
||||
slices.SortFunc(bindingGroups, func(a, b headerWithBindings) bool {
|
||||
if a.header.priority != b.header.priority {
|
||||
return a.header.priority > b.header.priority
|
||||
}
|
||||
}
|
||||
return a.header.title < b.header.title
|
||||
})
|
||||
|
||||
return bindingSections
|
||||
return slices.Map(bindingGroups, func(hb headerWithBindings) *bindingSection {
|
||||
return &bindingSection{
|
||||
title: hb.header.title,
|
||||
bindings: hb.bindings,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection {
|
||||
if binding.Description == "" && binding.Alternative == "" {
|
||||
return bindingSections
|
||||
func getHeader(binding *types.Binding, tr *i18n.TranslationSet) header {
|
||||
if binding.Tag == "navigation" {
|
||||
return header{priority: 2, title: localisedTitle(tr, "navigation")}
|
||||
}
|
||||
|
||||
for _, section := range bindingSections {
|
||||
if title == section.title {
|
||||
section.bindings = append(section.bindings, binding)
|
||||
return bindingSections
|
||||
}
|
||||
if binding.ViewName == "" {
|
||||
return header{priority: 3, title: localisedTitle(tr, "global")}
|
||||
}
|
||||
|
||||
section := &bindingSection{
|
||||
title: title,
|
||||
bindings: []*gui.Binding{binding},
|
||||
}
|
||||
|
||||
return append(bindingSections, section)
|
||||
return header{priority: 1, title: localisedTitle(tr, binding.ViewName)}
|
||||
}
|
||||
|
||||
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
|
||||
|
|
@ -263,3 +193,19 @@ func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection)
|
|||
|
||||
return content
|
||||
}
|
||||
|
||||
func formatTitle(title string) string {
|
||||
return fmt.Sprintf("\n## %s\n\n", title)
|
||||
}
|
||||
|
||||
func formatBinding(binding *types.Binding) string {
|
||||
if binding.Alternative != "" {
|
||||
return fmt.Sprintf(
|
||||
" <kbd>%s</kbd>: %s (%s)\n",
|
||||
keybindings.LabelFromKey(binding.Key),
|
||||
binding.Description,
|
||||
binding.Alternative,
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", keybindings.LabelFromKey(binding.Key), binding.Description)
|
||||
}
|
||||
|
|
|
|||
240
pkg/cheatsheet/generate_test.go
Normal file
240
pkg/cheatsheet/generate_test.go
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
package cheatsheet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetBindingSections(t *testing.T) {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
|
||||
tests := []struct {
|
||||
testName string
|
||||
bindings []*types.Binding
|
||||
expected []*bindingSection
|
||||
}{
|
||||
{
|
||||
testName: "no bindings",
|
||||
bindings: []*types.Binding{},
|
||||
expected: []*bindingSection{},
|
||||
},
|
||||
{
|
||||
testName: "one binding",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "Files",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "global binding",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "",
|
||||
Description: "quit",
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "Global Keybindings",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "",
|
||||
Description: "quit",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "grouped bindings",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
},
|
||||
{
|
||||
ViewName: "submodules",
|
||||
Description: "drop submodule",
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "Files",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Submodules",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "submodules",
|
||||
Description: "drop submodule",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "with navigation bindings",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "scroll",
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "revert commit",
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "List Panel Navigation",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "scroll",
|
||||
Tag: "navigation",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Commits",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "revert commit",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Files",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "with duplicate navigation bindings",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "scroll",
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "revert commit",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "scroll",
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "page up",
|
||||
Tag: "navigation",
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
{
|
||||
title: "List Panel Navigation",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "scroll",
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "page up",
|
||||
Tag: "navigation",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Commits",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "commits",
|
||||
Description: "revert commit",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Files",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
actual := getBindingSections(test.bindings, &tr)
|
||||
assert.EqualValues(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
|
|
@ -57,12 +58,13 @@ func NewGitCommand(
|
|||
cmn *common.Common,
|
||||
osCommand *oscommands.OSCommand,
|
||||
gitConfig git_config.IGitConfig,
|
||||
syncMutex *deadlock.Mutex,
|
||||
) (*GitCommand, error) {
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo, err := setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr)
|
||||
repo, err := setupRepository(gogit.PlainOpenWithOptions, gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}, cmn.Tr.GitconfigParseErr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -78,6 +80,7 @@ func NewGitCommand(
|
|||
gitConfig,
|
||||
dotGitDir,
|
||||
repo,
|
||||
syncMutex,
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +90,7 @@ func NewGitCommandAux(
|
|||
gitConfig git_config.IGitConfig,
|
||||
dotGitDir string,
|
||||
repo *gogit.Repository,
|
||||
syncMutex *deadlock.Mutex,
|
||||
) *GitCommand {
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
|
||||
|
||||
|
|
@ -96,7 +100,7 @@ func NewGitCommandAux(
|
|||
// on the one struct.
|
||||
// common ones are: cmn, osCommand, dotGitDir, configCommands
|
||||
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
|
||||
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands)
|
||||
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
|
||||
|
||||
statusCommands := git_commands.NewStatusCommands(gitCommon)
|
||||
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)
|
||||
|
|
@ -206,7 +210,7 @@ func resolvePath(path string) (string, error) {
|
|||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
|
||||
func setupRepository(openGitRepository func(string) (*gogit.Repository, error), gitConfigParseErrorStr string) (*gogit.Repository, error) {
|
||||
func setupRepository(openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), options gogit.PlainOpenOptions, gitConfigParseErrorStr string) (*gogit.Repository, error) {
|
||||
unresolvedPath := env.GetGitDirEnv()
|
||||
if unresolvedPath == "" {
|
||||
var err error
|
||||
|
|
@ -221,8 +225,7 @@ func setupRepository(openGitRepository func(string) (*gogit.Repository, error),
|
|||
return nil, err
|
||||
}
|
||||
|
||||
repository, err := openGitRepository(path)
|
||||
|
||||
repository, err := openGitRepository(path, &options)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
|
||||
return nil, errors.New(gitConfigParseErrorStr)
|
||||
|
|
@ -254,7 +257,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
|||
}
|
||||
fileContent := string(fileBytes)
|
||||
if !strings.HasPrefix(fileContent, "gitdir: ") {
|
||||
return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory")
|
||||
return "", errors.New(".git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory")
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package git_commands
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
import (
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// although the typical terms in a git bisect are 'bad' and 'good', they're more
|
||||
// generally known as 'new' and 'old'. Semi-recently git allowed the user to define
|
||||
|
|
@ -93,11 +97,5 @@ func (self *BisectInfo) Bisecting() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
for _, status := range self.statusMap {
|
||||
if status == BisectStatusOld {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return slices.Contains(maps.Values(self.statusMap), BisectStatusOld)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
// this takes something like:
|
||||
// * (HEAD detached at 264fc6f5)
|
||||
// remotes
|
||||
// remotes
|
||||
// and returns '264fc6f5' as the second match
|
||||
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
|
||||
|
||||
|
|
@ -109,6 +109,10 @@ func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName stri
|
|||
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) UnsetUpstream(branchName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git branch --unset-upstream %s", self.cmd.Quote(branchName))).Run()
|
||||
}
|
||||
|
||||
func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
return self.GetCommitDifferences("HEAD", "HEAD@{u}")
|
||||
}
|
||||
|
|
@ -142,7 +146,7 @@ func (self *BranchCommands) Rename(oldName string, newName string) error {
|
|||
}
|
||||
|
||||
func (self *BranchCommands) GetRawBranches() (string, error) {
|
||||
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
|
||||
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
type MergeOpts struct {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
|
|
@ -22,6 +23,17 @@ func (self *CommitCommands) RewordLastCommit(message string) error {
|
|||
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
||||
// ResetAuthor resets the author of the topmost commit
|
||||
func (self *CommitCommands) ResetAuthor() error {
|
||||
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
|
||||
}
|
||||
|
||||
// Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>'
|
||||
func (self *CommitCommands) SetAuthor(value string) error {
|
||||
commandStr := fmt.Sprintf("git commit --allow-empty --only --no-edit --amend --author=%s", self.cmd.Quote(value))
|
||||
return self.cmd.New(commandStr).Run()
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
|
||||
|
|
@ -70,10 +82,37 @@ func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
|
|||
func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
|
||||
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
|
||||
messageWithHeader, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
|
||||
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "")
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitDiff(commitSha string) (string, error) {
|
||||
cmdStr := "git show --no-color " + commitSha
|
||||
diff, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
return diff, err
|
||||
}
|
||||
|
||||
type Author struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitAuthor(commitSha string) (Author, error) {
|
||||
cmdStr := "git show --no-patch --pretty=format:'%an%x00%ae' " + commitSha
|
||||
output, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return Author{}, err
|
||||
}
|
||||
|
||||
split := strings.SplitN(strings.TrimSpace(output), "\x00", 2)
|
||||
if len(split) < 2 {
|
||||
return Author{}, errors.New("unexpected git output")
|
||||
}
|
||||
|
||||
author := Author{Name: split[0], Email: split[1]}
|
||||
return author, err
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) {
|
||||
return self.GetCommitMessagesFirstLine([]string{sha})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,3 +161,49 @@ func TestCommitShowCmdObj(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitMsg(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
input string
|
||||
expectedOutput string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"empty",
|
||||
` commit deadbeef`,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"no line breaks (single line)",
|
||||
`commit deadbeef
|
||||
use generics to DRY up context code`,
|
||||
`use generics to DRY up context code`,
|
||||
},
|
||||
{
|
||||
"with line breaks",
|
||||
`commit deadbeef
|
||||
Merge pull request #1750 from mark2185/fix-issue-template
|
||||
|
||||
'git-rev parse' should be 'git rev-parse'`,
|
||||
`Merge pull request #1750 from mark2185/fix-issue-template
|
||||
|
||||
'git-rev parse' should be 'git rev-parse'`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{
|
||||
runner: oscommands.NewFakeRunner(t).Expect("git rev-list --format=%B --max-count=1 deadbeef", s.input, nil),
|
||||
})
|
||||
|
||||
output, err := instance.GetCommitMessage("deadbeef")
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, s.expectedOutput, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type GitCommon struct {
|
||||
|
|
@ -13,6 +14,8 @@ type GitCommon struct {
|
|||
dotGitDir string
|
||||
repo *gogit.Repository
|
||||
config *ConfigCommands
|
||||
// mutex for doing things like push/pull/fetch
|
||||
syncMutex *deadlock.Mutex
|
||||
}
|
||||
|
||||
func NewGitCommon(
|
||||
|
|
@ -22,6 +25,7 @@ func NewGitCommon(
|
|||
dotGitDir string,
|
||||
repo *gogit.Repository,
|
||||
config *ConfigCommands,
|
||||
syncMutex *deadlock.Mutex,
|
||||
) *GitCommon {
|
||||
return &GitCommon{
|
||||
Common: cmn,
|
||||
|
|
@ -30,5 +34,6 @@ func NewGitCommon(
|
|||
dotGitDir: dotGitDir,
|
||||
repo: repo,
|
||||
config: config,
|
||||
syncMutex: syncMutex,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,5 +58,17 @@ func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string
|
|||
}
|
||||
|
||||
editCmdTemplate := self.UserConfig.OS.EditCommandTemplate
|
||||
if len(editCmdTemplate) == 0 {
|
||||
switch editor {
|
||||
case "emacs", "nano", "vi", "vim", "nvim":
|
||||
editCmdTemplate = "{{editor}} +{{line}} -- {{filename}}"
|
||||
case "subl":
|
||||
editCmdTemplate = "{{editor}} -- {{filename}}:{{line}}"
|
||||
case "code":
|
||||
editCmdTemplate = "{{editor}} -r --goto -- {{filename}}:{{line}}"
|
||||
default:
|
||||
editCmdTemplate = "{{editor}} -- {{filename}}"
|
||||
}
|
||||
}
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
|||
gitConfigMockResponses: nil,
|
||||
test: func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `nano "test"`, cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -143,6 +144,20 @@ func TestEditFileCmdStr(t *testing.T) {
|
|||
assert.Equal(t, `vim +1 "open file/at line"`, cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "default edit command template",
|
||||
configEditCommand: "vim",
|
||||
configEditCommandTemplate: "",
|
||||
runner: oscommands.NewFakeRunner(t),
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
gitConfigMockResponses: nil,
|
||||
test: func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `vim +1 -- "default edit command template"`, cmdStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
|
|
|
|||
|
|
@ -105,16 +105,16 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
|||
}
|
||||
|
||||
baseIndex := sourceCommitIdx + 1
|
||||
todo := ""
|
||||
for i, commit := range commits[0:baseIndex] {
|
||||
a := "pick"
|
||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
||||
a = "edit"
|
||||
}
|
||||
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true).Run()
|
||||
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
||||
return "edit"
|
||||
} else {
|
||||
return "pick"
|
||||
}
|
||||
})
|
||||
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
|
@ -52,7 +54,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
|
|||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||
todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, "reword")
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -60,6 +62,38 @@ func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index
|
|||
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
|
||||
return self.GenericAmend(commits, index, func() error {
|
||||
return self.commit.ResetAuthor()
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int, value string) error {
|
||||
return self.GenericAmend(commits, index, func() error {
|
||||
return self.commit.SetAuthor(value)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
|
||||
if index == 0 {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return f()
|
||||
}
|
||||
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now the selected commit should be our head so we'll amend it
|
||||
err = f()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.ContinueRebase()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
|
||||
// we must ensure that we have at least two commits after the selected one
|
||||
if len(commits) <= index+2 {
|
||||
|
|
@ -67,17 +101,15 @@ func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int)
|
|||
return errors.New(self.Tr.NoRoom)
|
||||
}
|
||||
|
||||
todo := ""
|
||||
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
|
||||
for _, commit := range orderedCommits {
|
||||
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true).Run()
|
||||
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
|
||||
todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, action)
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -88,7 +120,8 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index in
|
|||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
||||
// lazygit a todo string to write to the todo file
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) oscommands.ICmdObj {
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
|
||||
todo := self.buildTodo(todoLines)
|
||||
ex := oscommands.GetLazygitPath()
|
||||
|
||||
debug := "FALSE"
|
||||
|
|
@ -97,7 +130,7 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
|
|||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
|
||||
self.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
self.Log.WithField("command", cmdStr).Debug("RunCommand")
|
||||
|
||||
cmdObj := self.cmd.New(cmdStr)
|
||||
|
||||
|
|
@ -109,8 +142,8 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
|
|||
}
|
||||
|
||||
cmdObj.AddEnvVars(
|
||||
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
|
||||
"LAZYGIT_REBASE_TODO="+todo,
|
||||
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
|
||||
daemon.RebaseTODOEnvKey+"="+todo,
|
||||
"DEBUG="+debug,
|
||||
"LANG=en_US.UTF-8", // Force using EN as language
|
||||
"LC_ALL=en_US.UTF-8", // Force using EN as language
|
||||
|
|
@ -124,38 +157,37 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
|
|||
return cmdObj
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
|
||||
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
|
||||
// will have the given action applied to it.
|
||||
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
|
||||
baseIndex := actionIndex + 1
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return "", "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
|
||||
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
|
||||
}
|
||||
|
||||
if action == "squash" || action == "fixup" {
|
||||
baseIndex++
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return "", "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
|
||||
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
|
||||
}
|
||||
}
|
||||
|
||||
todo := ""
|
||||
for i, commit := range commits[0:baseIndex] {
|
||||
var commitAction string
|
||||
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
||||
if i == actionIndex {
|
||||
commitAction = action
|
||||
return action
|
||||
} else if commit.IsMerge() {
|
||||
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
|
||||
// doing this means we don't need to worry about rebasing over merges which always causes problems.
|
||||
// you typically shouldn't be doing rebases that pass over merge commits anyway.
|
||||
commitAction = "drop"
|
||||
return "drop"
|
||||
} else {
|
||||
commitAction = "pick"
|
||||
return "pick"
|
||||
}
|
||||
todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
})
|
||||
|
||||
return todo, commits[baseIndex].Sha, nil
|
||||
return todoLines, commits[baseIndex].Sha, nil
|
||||
}
|
||||
|
||||
// AmendTo amends the given commit with whatever files are staged
|
||||
|
|
@ -185,7 +217,7 @@ func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
|
|||
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
|
||||
result := strings.Join(content, "\n")
|
||||
|
||||
return ioutil.WriteFile(fileName, []byte(result), 0644)
|
||||
return ioutil.WriteFile(fileName, []byte(result), 0o644)
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
|
||||
|
|
@ -215,7 +247,7 @@ func (self *RebaseCommands) MoveTodoDown(index int) error {
|
|||
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
|
||||
result := strings.Join(rearrangedContent, "\n")
|
||||
|
||||
return ioutil.WriteFile(fileName, []byte(result), 0644)
|
||||
return ioutil.WriteFile(fileName, []byte(result), 0o644)
|
||||
}
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
|
|
@ -244,7 +276,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
|
|||
return errors.New(self.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
todo, sha, err := self.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -254,7 +286,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
|
|||
|
||||
// RebaseBranch interactive rebases onto a branch
|
||||
func (self *RebaseCommands) RebaseBranch(branchName string) error {
|
||||
return self.PrepareInteractiveRebaseCommand(branchName, "", false).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
|
||||
|
|
@ -298,7 +330,7 @@ func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) erro
|
|||
lazyGitPath := oscommands.GetLazygitPath()
|
||||
return cmdObj.
|
||||
AddEnvVars(
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
|
|
@ -336,10 +368,36 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
|
|||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
|
||||
todo := ""
|
||||
for _, commit := range commits {
|
||||
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand("HEAD", todo, false).Run()
|
||||
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
|
||||
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
|
||||
return todoLine.ToString()
|
||||
})
|
||||
|
||||
return strings.Join(slices.Reverse(lines), "")
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
|
||||
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
|
||||
return TodoLine{Action: f(commit, i), Commit: commit}
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
|
||||
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
|
||||
return action
|
||||
})
|
||||
}
|
||||
|
||||
type TodoLine struct {
|
||||
Action string
|
||||
Commit *models.Commit
|
||||
}
|
||||
|
||||
func (self *TodoLine) ToString() string {
|
||||
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -61,10 +62,10 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
|
|||
`^VISUAL=.*$`,
|
||||
`^EDITOR=.*$`,
|
||||
`^GIT_EDITOR=.*$`,
|
||||
"^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$",
|
||||
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
|
||||
} {
|
||||
regexStr := regexStr
|
||||
foundMatch := utils.IncludesStringFunc(envVars, func(envVar string) bool {
|
||||
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
|
||||
return regexp.MustCompile(regexStr).MatchString(envVar)
|
||||
})
|
||||
if !foundMatch {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
|
|||
|
||||
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
|
||||
command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName))
|
||||
return self.cmd.New(command).PromptOnCredentialRequest().Run()
|
||||
return self.cmd.New(command).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ func NewStashCommands(
|
|||
}
|
||||
}
|
||||
|
||||
func (self *StashCommands) DropNewest() error {
|
||||
return self.cmd.New("git stash drop").Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Drop(index int) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash drop stash@{%d}", index)).Run()
|
||||
}
|
||||
|
|
@ -38,7 +42,6 @@ func (self *StashCommands) Apply(index int) error {
|
|||
}
|
||||
|
||||
// Save save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (self *StashCommands) Save(message string) error {
|
||||
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
|
@ -49,6 +52,23 @@ func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
|
|||
return self.cmd.New(cmdStr).DontLog()
|
||||
}
|
||||
|
||||
func (self *StashCommands) StashAndKeepIndex(message string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash save %s --keep-index", self.cmd.Quote(message))).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) StashUnstagedChanges(message string) error {
|
||||
if err := self.cmd.New("git commit --no-verify -m \"[lazygit] stashing unstaged changes\"").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.Save(message); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.cmd.New("git reset --soft HEAD^").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
|
|||
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest()
|
||||
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
|
||||
return cmdObj, nil
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ func (self *SyncCommands) Fetch(opts FetchOptions) error {
|
|||
} else {
|
||||
cmdObj.PromptOnCredentialRequest()
|
||||
}
|
||||
return cmdObj.Run()
|
||||
return cmdObj.WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
type PullOptions struct {
|
||||
|
|
@ -108,15 +108,15 @@ func (self *SyncCommands) Pull(opts PullOptions) error {
|
|||
|
||||
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
|
||||
// has 'pull.rebase = interactive' configured.
|
||||
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().Run()
|
||||
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
|
||||
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
|
||||
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
|
||||
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FetchRemote(remoteName string) error {
|
||||
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
|
||||
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
|
||||
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,5 +27,5 @@ func (self *TagCommands) Delete(tagName string) error {
|
|||
}
|
||||
|
||||
func (self *TagCommands) Push(remoteName string, tagName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().Run()
|
||||
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
|
|
@ -46,10 +47,9 @@ func (self *WorkingTreeCommands) StageFile(path string) error {
|
|||
}
|
||||
|
||||
func (self *WorkingTreeCommands) StageFiles(paths []string) error {
|
||||
quotedPaths := make([]string, len(paths))
|
||||
for i, path := range paths {
|
||||
quotedPaths[i] = self.cmd.Quote(path)
|
||||
}
|
||||
quotedPaths := slices.Map(paths, func(path string) string {
|
||||
return self.cmd.Quote(path)
|
||||
})
|
||||
return self.cmd.New(fmt.Sprintf("git add -- %s", strings.Join(quotedPaths, " "))).Run()
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +218,11 @@ func (self *WorkingTreeCommands) Ignore(filename string) error {
|
|||
return self.os.AppendLineToFile(".gitignore", filename)
|
||||
}
|
||||
|
||||
// Exclude adds a file to the .git/info/exclude for the repo
|
||||
func (self *WorkingTreeCommands) Exclude(filename string) error {
|
||||
return self.os.AppendLineToFile(".git/info/exclude", filename)
|
||||
}
|
||||
|
||||
// WorktreeFileDiff returns the diff of a file
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
|
|
@ -230,6 +235,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
|
|||
trackedArg := "--"
|
||||
colorArg := self.UserConfig.Git.Paging.ColorArg
|
||||
quotedPath := self.cmd.Quote(node.GetPath())
|
||||
quotedPrevPath := ""
|
||||
ignoreWhitespaceArg := ""
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
if cached {
|
||||
|
|
@ -244,19 +250,25 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
|
|||
if ignoreWhitespace {
|
||||
ignoreWhitespaceArg = " --ignore-all-space"
|
||||
}
|
||||
if prevPath := node.GetPreviousPath(); prevPath != "" {
|
||||
quotedPrevPath = " " + self.cmd.Quote(prevPath)
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
|
||||
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s%s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath, quotedPrevPath)
|
||||
|
||||
return self.cmd.New(cmdStr).DontLog()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
|
||||
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
self.Log.Infof("saving temporary patch to %s", filepath)
|
||||
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
|
||||
filepath, err := self.SaveTemporaryPatch(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.ApplyPatchFile(filepath, flags...)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ApplyPatchFile(filepath string, flags ...string) error {
|
||||
flagStr := ""
|
||||
for _, flag := range flags {
|
||||
flagStr += " --" + flag
|
||||
|
|
@ -265,6 +277,15 @@ func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error
|
|||
return self.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, self.cmd.Quote(filepath))).Run()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error) {
|
||||
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
self.Log.Infof("saving temporary patch to %s", filepath)
|
||||
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath, nil
|
||||
}
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when getting path informations",
|
||||
"An error occurred when getting path information",
|
||||
func(string) (os.FileInfo, error) {
|
||||
return nil, fmt.Errorf("An error occurred")
|
||||
},
|
||||
|
|
@ -115,18 +116,20 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
|
|||
func TestSetupRepository(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
openGitRepository func(string) (*gogit.Repository, error)
|
||||
openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error)
|
||||
errorStr string
|
||||
options gogit.PlainOpenOptions
|
||||
test func(*gogit.Repository, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"A gitconfig parsing error occurred",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
|
||||
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
|
||||
},
|
||||
"error translated",
|
||||
gogit.PlainOpenOptions{},
|
||||
func(r *gogit.Repository, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "error translated")
|
||||
|
|
@ -134,10 +137,11 @@ func TestSetupRepository(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"A gogit error occurred",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
|
||||
return nil, fmt.Errorf("Error from inside gogit")
|
||||
},
|
||||
"",
|
||||
gogit.PlainOpenOptions{},
|
||||
func(r *gogit.Repository, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "Error from inside gogit")
|
||||
|
|
@ -145,13 +149,14 @@ func TestSetupRepository(t *testing.T) {
|
|||
},
|
||||
{
|
||||
"Setup done properly",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
|
||||
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
|
||||
r, err := gogit.PlainInit("/tmp/lazygit-test", false)
|
||||
assert.NoError(t, err)
|
||||
return r, nil
|
||||
},
|
||||
"",
|
||||
gogit.PlainOpenOptions{},
|
||||
func(r *gogit.Repository, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, r)
|
||||
|
|
@ -162,7 +167,7 @@ func TestSetupRepository(t *testing.T) {
|
|||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(setupRepository(s.openGitRepository, s.errorStr))
|
||||
s.test(setupRepository(s.openGitRepository, s.options, s.errorStr))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -211,7 +216,12 @@ func TestNewGitCommand(t *testing.T) {
|
|||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.setup()
|
||||
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
|
||||
s.test(
|
||||
NewGitCommand(utils.NewDummyCommon(),
|
||||
oscommands.NewDummyOSCommand(),
|
||||
git_config.NewFakeGitConfig(nil),
|
||||
&deadlock.Mutex{},
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ var defaultUrlRegexStrings = []string{
|
|||
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
}
|
||||
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
|
||||
|
||||
// we've got less type safety using go templates but this lends itself better to
|
||||
// users adding custom service definitions in their config
|
||||
|
|
@ -15,6 +16,7 @@ var githubServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var bitbucketServiceDef = ServiceDefinition{
|
||||
|
|
@ -22,7 +24,11 @@ var bitbucketServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1",
|
||||
pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1",
|
||||
commitURL: "/commits/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
regexStrings: []string{
|
||||
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var gitLabServiceDef = ServiceDefinition{
|
||||
|
|
@ -31,9 +37,40 @@ var gitLabServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var serviceDefinitions = []ServiceDefinition{githubServiceDef, bitbucketServiceDef, gitLabServiceDef}
|
||||
var azdoServiceDef = ServiceDefinition{
|
||||
provider: "azuredevops",
|
||||
pullRequestURLIntoDefaultBranch: "/pullrequestcreate?sourceRef={{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/pullrequestcreate?sourceRef={{.From}}&targetRef={{.To}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: []string{
|
||||
`^git@ssh.dev.azure.com.*/(?P<org>.*)/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^https://.*@dev.azure.com/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
|
||||
}
|
||||
|
||||
var bitbucketServerServiceDef = ServiceDefinition{
|
||||
provider: "bitbucketServer",
|
||||
pullRequestURLIntoDefaultBranch: "/pull-requests?create&sourceBranch={{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/pull-requests?create&targetBranch={{.To}}&sourceBranch={{.From}}",
|
||||
commitURL: "/commits/{{.CommitSha}}",
|
||||
regexStrings: []string{
|
||||
`^ssh://git@.*/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
|
||||
}
|
||||
|
||||
var serviceDefinitions = []ServiceDefinition{
|
||||
githubServiceDef,
|
||||
bitbucketServiceDef,
|
||||
gitLabServiceDef,
|
||||
azdoServiceDef,
|
||||
bitbucketServerServiceDef,
|
||||
}
|
||||
|
||||
var defaultServiceDomains = []ServiceDomain{
|
||||
{
|
||||
|
|
@ -51,4 +88,9 @@ var defaultServiceDomains = []ServiceDomain{
|
|||
gitDomain: "gitlab.com",
|
||||
webDomain: "gitlab.com",
|
||||
},
|
||||
{
|
||||
serviceDefinition: azdoServiceDef,
|
||||
gitDomain: "dev.azure.com",
|
||||
webDomain: "dev.azure.com",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package hosting_service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
|
@ -10,6 +9,8 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
)
|
||||
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
|
||||
|
|
@ -66,13 +67,13 @@ func (self *HostingServiceMgr) getService() (*Service, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
root, err := serviceDomain.getRootFromRemoteURL(self.remoteURL)
|
||||
repoURL, err := serviceDomain.serviceDefinition.getRepoURLFromRemoteURL(self.remoteURL, serviceDomain.webDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
root: root,
|
||||
repoURL: repoURL,
|
||||
ServiceDefinition: serviceDomain.serviceDefinition,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -95,8 +96,7 @@ func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
|
|||
serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition
|
||||
}
|
||||
|
||||
var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains))
|
||||
copy(serviceDomains, defaultServiceDomains)
|
||||
serviceDomains := slices.Clone(defaultServiceDomains)
|
||||
|
||||
if len(self.configServiceDomains) > 0 {
|
||||
for gitDomain, typeAndDomain := range self.configServiceDomains {
|
||||
|
|
@ -111,10 +111,10 @@ func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
|
|||
|
||||
serviceDefinition, ok := serviceDefinitionByProvider[provider]
|
||||
if !ok {
|
||||
providerNames := []string{}
|
||||
for _, serviceDefinition := range serviceDefinitions {
|
||||
providerNames = append(providerNames, serviceDefinition.provider)
|
||||
}
|
||||
providerNames := slices.Map(serviceDefinitions, func(serviceDefinition ServiceDefinition) string {
|
||||
return serviceDefinition.provider
|
||||
})
|
||||
|
||||
self.log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", "))
|
||||
continue
|
||||
}
|
||||
|
|
@ -139,47 +139,32 @@ type ServiceDomain struct {
|
|||
serviceDefinition ServiceDefinition
|
||||
}
|
||||
|
||||
func (self ServiceDomain) getRootFromRemoteURL(repoURL string) (string, error) {
|
||||
// we may want to make this more specific to the service in future e.g. if
|
||||
// some new service comes along which has a different root url structure.
|
||||
repoInfo, err := self.serviceDefinition.getRepoInfoFromURL(repoURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("https://%s/%s/%s", self.webDomain, repoInfo.Owner, repoInfo.Repository), nil
|
||||
}
|
||||
|
||||
// RepoInformation holds some basic information about the repo
|
||||
type RepoInformation struct {
|
||||
Owner string
|
||||
Repository string
|
||||
}
|
||||
|
||||
type ServiceDefinition struct {
|
||||
provider string
|
||||
pullRequestURLIntoDefaultBranch string
|
||||
pullRequestURLIntoTargetBranch string
|
||||
commitURL string
|
||||
regexStrings []string
|
||||
|
||||
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
|
||||
repoURLTemplate string
|
||||
}
|
||||
|
||||
func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, error) {
|
||||
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
|
||||
for _, regexStr := range self.regexStrings {
|
||||
re := regexp.MustCompile(regexStr)
|
||||
matches := utils.FindNamedMatches(re, url)
|
||||
if matches != nil {
|
||||
return &RepoInformation{
|
||||
Owner: matches["owner"],
|
||||
Repository: matches["repo"],
|
||||
}, nil
|
||||
input := utils.FindNamedMatches(re, url)
|
||||
if input != nil {
|
||||
input["webDomain"] = webDomain
|
||||
return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("Failed to parse repo information from url")
|
||||
return "", errors.New("Failed to parse repo information from url")
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
root string
|
||||
repoURL string
|
||||
ServiceDefinition
|
||||
}
|
||||
|
||||
|
|
@ -196,5 +181,5 @@ func (self *Service) getCommitURL(commitSha string) string {
|
|||
}
|
||||
|
||||
func (self *Service) resolveUrl(templateString string, args map[string]string) string {
|
||||
return self.root + utils.ResolvePlaceholderString(templateString, args)
|
||||
return self.repoURL + utils.ResolvePlaceholderString(templateString, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,63 +8,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
serviceDefinition ServiceDefinition
|
||||
testName string
|
||||
repoURL string
|
||||
test func(*RepoInformation)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for git remote url",
|
||||
"git@github.com:petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for git remote url, trimming trailing '.git'",
|
||||
"git@github.com:petersmith/super_calculator.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for ssh remote url",
|
||||
"ssh://git@github.com/petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for http remote url",
|
||||
"https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "johndoe")
|
||||
assert.EqualValues(t, repoInfo.Repository, "social_network")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result, err := s.serviceDefinition.getRepoInfoFromURL(s.repoURL)
|
||||
assert.NoError(t, err)
|
||||
s.test(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPullRequestURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
|
|
@ -172,6 +115,107 @@ func TestGetPullRequestURL(t *testing.T) {
|
|||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with a custom SSH username",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "john@bitbucket.org:johndoe/social_network.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Azure DevOps (SSH)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Azure DevOps (SSH) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew&targetRef=dev", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Azure DevOps (HTTP)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "https://myorg@dev.azure.com/myorg/myproject/_git/myrepo",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Azure DevOps (HTTP) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "https://myorg@dev.azure.com/myorg/myproject/_git/myrepo",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew&targetRef=dev", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Bitbucket Server (SSH)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "ssh://git@mycompany.bitbucket.com/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a bitbucket server URL
|
||||
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Bitbucket Server (SSH) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "ssh://git@mycompany.bitbucket.com/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a bitbucket server URL
|
||||
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Bitbucket Server (HTTP)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "https://mycompany.bitbucket.com/scm/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a bitbucket server URL
|
||||
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Bitbucket Server (HTTP) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "https://mycompany.bitbucket.com/scm/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a bitbucket server URL
|
||||
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
|
|
@ -218,7 +262,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
|
||||
},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab"},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
|
||||
},
|
||||
{
|
||||
testName: "Escapes reserved URL characters in from branch name",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/go-git/v5/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
|
|
@ -64,21 +66,20 @@ outer:
|
|||
if strings.EqualFold(reflogBranch.Name, branch.Name) {
|
||||
branch.Recency = reflogBranch.Recency
|
||||
branchesWithRecency = append(branchesWithRecency, branch)
|
||||
branches = append(branches[0:j], branches[j+1:]...)
|
||||
branches = slices.Remove(branches, j)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = append(branchesWithRecency, branches...)
|
||||
branches = slices.Prepend(branches, branchesWithRecency...)
|
||||
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = append(branches[0:i], branches[i+1:]...)
|
||||
branches = append([]*models.Branch{branch}, branches...)
|
||||
branches = slices.Move(branches, i, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +88,7 @@ outer:
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
|
||||
branches = slices.Prepend(branches, &models.Branch{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"})
|
||||
}
|
||||
|
||||
configBranches, err := self.config.Branches()
|
||||
|
|
@ -114,38 +115,47 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
|
|||
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
outputLines := strings.Split(trimmedOutput, "\n")
|
||||
branches := make([]*models.Branch, 0, len(outputLines))
|
||||
for _, line := range outputLines {
|
||||
|
||||
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
|
||||
if line == "" {
|
||||
continue
|
||||
return nil, false
|
||||
}
|
||||
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
split := strings.Split(line, "\x00")
|
||||
if len(split) != 4 {
|
||||
// Ignore line if it isn't separated into 4 parts
|
||||
// This is probably a warning message, for more info see:
|
||||
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
|
||||
continue
|
||||
return nil, false
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
Pushables: "?",
|
||||
Head: split[0] == "*",
|
||||
}
|
||||
return obtainBranch(split), true
|
||||
})
|
||||
}
|
||||
|
||||
upstreamName := split[2]
|
||||
if upstreamName == "" {
|
||||
// if we're here then it means we do not have a local version of the remote.
|
||||
// The branch might still be tracking a remote though, we just don't know
|
||||
// how many commits ahead/behind it is
|
||||
branches = append(branches, branch)
|
||||
continue
|
||||
}
|
||||
// Obtain branch information from parsed line output of getRawBranches()
|
||||
// split contains the '|' separated tokens in the line of output
|
||||
func obtainBranch(split []string) *models.Branch {
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
Pushables: "?",
|
||||
Head: split[0] == "*",
|
||||
}
|
||||
|
||||
track := split[3]
|
||||
upstreamName := split[2]
|
||||
if upstreamName == "" {
|
||||
// if we're here then it means we do not have a local version of the remote.
|
||||
// The branch might still be tracking a remote though, we just don't know
|
||||
// how many commits ahead/behind it is
|
||||
return branch
|
||||
}
|
||||
|
||||
track := split[3]
|
||||
if track == "[gone]" {
|
||||
branch.UpstreamGone = true
|
||||
} else {
|
||||
re := regexp.MustCompile(`ahead (\d+)`)
|
||||
match := re.FindStringSubmatch(track)
|
||||
if len(match) > 1 {
|
||||
|
|
@ -161,30 +171,32 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
|
|||
} else {
|
||||
branch.Pullables = "0"
|
||||
}
|
||||
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
|
||||
return branches
|
||||
return branch
|
||||
}
|
||||
|
||||
// TODO: only look at the new reflog commits, and otherwise store the recencies in
|
||||
// int form against the branch to recalculate the time ago
|
||||
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
|
||||
foundBranchesMap := map[string]bool{}
|
||||
foundBranches := set.New[string]()
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
|
||||
|
||||
for _, commit := range reflogCommits {
|
||||
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranchesMap[branchName] {
|
||||
foundBranchesMap[branchName] = true
|
||||
reflogBranches = append(reflogBranches, &models.Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
match := re.FindStringSubmatch(commit.Name)
|
||||
if len(match) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranches.Includes(branchName) {
|
||||
foundBranches.Add(branchName)
|
||||
reflogBranches = append(reflogBranches, &models.Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
pkg/commands/loaders/branches_test.go
Normal file
52
pkg/commands/loaders/branches_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package loaders
|
||||
|
||||
// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestObtainBanch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
input []string
|
||||
expectedBranch *models.Branch
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "TrimHeads",
|
||||
input: []string{"", "heads/a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "NoUpstream",
|
||||
input: []string{"", "a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "IsHead",
|
||||
input: []string{"*", "a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
|
||||
},
|
||||
{
|
||||
testName: "IsBehindAndAhead",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "RemoteBranchIsGone",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
branch := obtainBranch(s.input)
|
||||
assert.EqualValues(t, s.expectedBranch, branch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type CommitFileLoader struct {
|
||||
|
|
@ -33,25 +35,22 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return self.getCommitFilesFromFilenames(filenames), nil
|
||||
return getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (self *CommitFileLoader) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
|
||||
// so we need to split it by the null character and then map each status-name pair to a commit file
|
||||
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
})
|
||||
if len(lines) == 1 {
|
||||
return []*models.CommitFile{}
|
||||
}
|
||||
|
||||
return commitFiles
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
|
||||
return &models.CommitFile{
|
||||
ChangeStatus: chunk[0],
|
||||
Name: chunk[1],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
71
pkg/commands/loaders/commit_files_test.go
Normal file
71
pkg/commands/loaders/commit_files_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCommitFilesFromFilenames(t *testing.T) {
|
||||
tests := []struct {
|
||||
testName string
|
||||
input string
|
||||
output []*models.CommitFile
|
||||
}{
|
||||
{
|
||||
testName: "no files",
|
||||
input: "",
|
||||
output: []*models.CommitFile{},
|
||||
},
|
||||
{
|
||||
testName: "one file",
|
||||
input: "MM\x00Myfile\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "two files",
|
||||
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "MyOtherFile",
|
||||
ChangeStatus: "M ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "three files",
|
||||
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "MyOtherFile",
|
||||
ChangeStatus: "M ",
|
||||
},
|
||||
{
|
||||
Name: "YetAnother",
|
||||
ChangeStatus: " M",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
result := getCommitFilesFromFilenames(test.input)
|
||||
assert.Equal(t, test.output, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package loaders
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
|
@ -9,6 +10,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
|
|
@ -22,8 +25,6 @@ import (
|
|||
// be processed as part of a rebase (these won't appear in git log but we
|
||||
// grab them from the rebase-related files in the .git directory to show them
|
||||
|
||||
const SEPARATION_CHAR = "|"
|
||||
|
||||
// CommitLoader returns a list of Commit objects for the current repo
|
||||
type CommitLoader struct {
|
||||
*common.Common
|
||||
|
|
@ -90,14 +91,12 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
|||
}
|
||||
|
||||
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
|
||||
if canExtractCommit(line) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
commit := self.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -158,15 +157,16 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
|
|||
// example input:
|
||||
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
|
||||
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
split := strings.SplitN(line, "\x00", 7)
|
||||
|
||||
sha := split[0]
|
||||
unixTimestamp := split[1]
|
||||
author := split[2]
|
||||
extraInfo := strings.TrimSpace(split[3])
|
||||
parentHashes := split[4]
|
||||
authorName := split[2]
|
||||
authorEmail := split[3]
|
||||
extraInfo := strings.TrimSpace(split[4])
|
||||
parentHashes := split[5]
|
||||
message := split[6]
|
||||
|
||||
message := strings.Join(split[5:], SEPARATION_CHAR)
|
||||
tags := []string{}
|
||||
|
||||
if extraInfo != "" {
|
||||
|
|
@ -179,14 +179,20 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
|||
|
||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
||||
|
||||
parents := []string{}
|
||||
if len(parentHashes) > 0 {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
Parents: strings.Split(parentHashes, " "),
|
||||
AuthorName: authorName,
|
||||
AuthorEmail: authorEmail,
|
||||
Parents: parents,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,16 +206,15 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := make([]string, len(commits))
|
||||
for i, commit := range commits {
|
||||
commitShas[i] = commit.Sha
|
||||
}
|
||||
commitShas := slices.Map(commits, func(commit *models.Commit) string {
|
||||
return commit.Sha
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git show %s --no-patch --oneline %s --abbrev=%d",
|
||||
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
|
||||
strings.Join(commitShas, " "),
|
||||
prettyFormat,
|
||||
20,
|
||||
|
|
@ -219,14 +224,12 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
|||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
if canExtractCommit(line) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
}
|
||||
commit := self.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -306,21 +309,24 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
|
|||
}
|
||||
|
||||
commits := []*models.Commit{}
|
||||
lines := strings.Split(string(bytesContent), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
|
||||
todos, err := todo.Parse(bytes.NewBuffer(bytesContent))
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*models.Commit{{
|
||||
Sha: splitLine[1],
|
||||
Name: strings.Join(splitLine[2:], " "),
|
||||
commits = slices.Prepend(commits, &models.Commit{
|
||||
Sha: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: "rebasing",
|
||||
Action: splitLine[0],
|
||||
}}, commits...)
|
||||
Action: t.Command.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
|
|
@ -429,27 +435,26 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
|||
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git log %s %s %s --oneline %s%s --abbrev=%d%s",
|
||||
"git -c log.showSignature=false log %s %s %s --oneline %s%s --abbrev=%d%s",
|
||||
self.cmd.Quote(opts.RefName),
|
||||
orderFlag,
|
||||
allFlag,
|
||||
prettyFormat,
|
||||
limitFlag,
|
||||
20,
|
||||
40,
|
||||
filterFlag,
|
||||
),
|
||||
).DontLog()
|
||||
}
|
||||
|
||||
var prettyFormat = fmt.Sprintf(
|
||||
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
"--pretty=format:\"%%H%s%%at%s%%aN%s%%ae%s%%d%s%%p%s%%s\"",
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
)
|
||||
|
||||
func canExtractCommit(line string) bool {
|
||||
return line != "" && strings.Split(line, " ")[0] != "gpg:"
|
||||
}
|
||||
const NULL_CODE = "%x00"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package loaders
|
|||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
|
@ -11,32 +12,14 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewDummyCommitLoader() *CommitLoader {
|
||||
cmn := utils.NewDummyCommon()
|
||||
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: nil,
|
||||
getCurrentBranchName: func() (string, string, error) { return "master", "master", nil },
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_NONE, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
walkFiles: func(root string, fn filepath.WalkFunc) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const commitsOutput = `0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield||d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield||985fe482e806b172aea4|refactoring the config struct`
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
|
|
@ -57,7 +40,7 @@ func TestGetCommits(t *testing.T) {
|
|||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, "", nil),
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
|
|
@ -71,7 +54,7 @@ func TestGetCommits(t *testing.T) {
|
|||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, commitsOutput, nil).
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, commitsOutput, nil).
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
|
|
@ -83,7 +66,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
|
|
@ -96,7 +80,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(origin/better-tests)",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640824515,
|
||||
Parents: []string{
|
||||
"e94e8fc5b6fab4cb755f",
|
||||
|
|
@ -109,7 +94,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640823749,
|
||||
Parents: []string{
|
||||
"d8084cd558925eb7c9c3",
|
||||
|
|
@ -122,7 +108,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640821426,
|
||||
Parents: []string{
|
||||
"65f910ebd85283b5cce9",
|
||||
|
|
@ -135,7 +122,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640821275,
|
||||
Parents: []string{
|
||||
"26c07b1ab33860a1a759",
|
||||
|
|
@ -148,7 +136,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640750752,
|
||||
Parents: []string{
|
||||
"3d4470a6c072208722e5",
|
||||
|
|
@ -161,7 +150,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640748818,
|
||||
Parents: []string{
|
||||
"053a66a7be3da43aacdc",
|
||||
|
|
@ -174,7 +164,8 @@ func TestGetCommits(t *testing.T) {
|
|||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640739815,
|
||||
Parents: []string{
|
||||
"985fe482e806b172aea4",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type FileLoaderConfig interface {
|
||||
|
|
@ -54,28 +53,15 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
|
|||
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := status.Change
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := change[1:2]
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||
hasMergeConflicts := hasInlineMergeConflicts || utils.IncludesString([]string{"DD", "AU", "UA", "UD", "DU"}, change)
|
||||
|
||||
file := &models.File{
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: self.getFileType(status.Name),
|
||||
ShortStatus: change,
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
Type: self.getFileType(status.Name),
|
||||
}
|
||||
|
||||
models.SetStatusFields(file, status.Change)
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +111,7 @@ func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
|||
|
||||
if strings.HasPrefix(status.Change, "R") {
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
status.PreviousName = strings.TrimSpace(splitLines[i+1])
|
||||
status.PreviousName = splitLines[i+1]
|
||||
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
|
||||
i++
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,25 +32,32 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
|
|||
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs"%s`, filterPathArg)).DontLog()
|
||||
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
fields := strings.SplitN(line, " ", 3)
|
||||
if len(fields) <= 2 {
|
||||
fields := strings.SplitN(line, "\x00", 4)
|
||||
if len(fields) <= 3 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(fields[1])
|
||||
|
||||
parentHashes := fields[3]
|
||||
parents := []string{}
|
||||
if len(parentHashes) > 0 {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
commit := &models.Commit{
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
Parents: parents,
|
||||
}
|
||||
|
||||
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
|
||||
// so two consequetive reflog entries may have both the same SHA and therefore same timestamp.
|
||||
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
|
||||
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
|
||||
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package loaders
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
|
@ -11,12 +12,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const reflogOutput = `c3c4b66b64c97ffeecde 1643150483 checkout: moving from A to B
|
||||
c3c4b66b64c97ffeecde 1643150483 checkout: moving from B to A
|
||||
c3c4b66b64c97ffeecde 1643150483 checkout: moving from A to B
|
||||
c3c4b66b64c97ffeecde 1643150483 checkout: moving from master to A
|
||||
f4ddf2f0d4be4ccc7efa 1643149435 checkout: moving from A to master
|
||||
`
|
||||
var reflogOutput = strings.Replace(`c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1
|
||||
f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1
|
||||
`, "|", "\x00", -1)
|
||||
|
||||
func TestGetReflogCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
|
|
@ -33,7 +34,7 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
{
|
||||
testName: "no reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, "", nil),
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{},
|
||||
|
|
@ -43,7 +44,7 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
{
|
||||
testName: "some reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, reflogOutput, nil),
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{
|
||||
|
|
@ -52,30 +53,35 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from master to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "f4ddf2f0d4be4ccc7efa",
|
||||
Name: "checkout: moving from A to master",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643149435,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: false,
|
||||
|
|
@ -84,13 +90,14 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
{
|
||||
testName: "some reflog entries where last commit is given",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, reflogOutput, nil),
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
|
|
@ -98,6 +105,7 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: true,
|
||||
|
|
@ -106,13 +114,14 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
{
|
||||
testName: "when passing filterPath",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git log -g --abbrev=20 --format="%h %ct %gs" --follow -- "path"`, reflogOutput, nil),
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
filterPath: "path",
|
||||
expectedCommits: []*models.Commit{
|
||||
|
|
@ -121,6 +130,7 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: true,
|
||||
|
|
@ -129,7 +139,7 @@ func TestGetReflogCommits(t *testing.T) {
|
|||
{
|
||||
testName: "when command returns error",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, "", errors.New("haha")),
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
filterPath: "",
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ package loaders
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
|
|
@ -42,37 +42,35 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
|||
}
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
remotes := make([]*models.Remote, len(goGitRemotes))
|
||||
for i, goGitRemote := range goGitRemotes {
|
||||
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
|
||||
remoteName := goGitRemote.Config().Name
|
||||
|
||||
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
|
||||
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
|
||||
branches := make([]*models.RemoteBranch, len(matches))
|
||||
for j, match := range matches {
|
||||
branches[j] = &models.RemoteBranch{
|
||||
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
|
||||
return &models.RemoteBranch{
|
||||
Name: match[1],
|
||||
RemoteName: remoteName,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
remotes[i] = &models.Remote{
|
||||
return &models.Remote{
|
||||
Name: goGitRemote.Config().Name,
|
||||
Urls: goGitRemote.Config().URLs,
|
||||
Branches: branches,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// now lets sort our remotes by name alphabetically
|
||||
sort.Slice(remotes, func(i, j int) bool {
|
||||
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
|
||||
// we want origin at the top because we'll be most likely to want it
|
||||
if remotes[i].Name == "origin" {
|
||||
if a.Name == "origin" {
|
||||
return true
|
||||
}
|
||||
if remotes[j].Name == "origin" {
|
||||
if b.Name == "origin" {
|
||||
return false
|
||||
}
|
||||
return strings.ToLower(remotes[i].Name) < strings.ToLower(remotes[j].Name)
|
||||
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
|
||||
})
|
||||
|
||||
return remotes, nil
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
|
|
@ -65,11 +66,9 @@ outer:
|
|||
|
||||
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").DontLog().RunWithOutput()
|
||||
stashEntries := []*models.StashEntry{}
|
||||
for i, line := range utils.SplitLines(rawString) {
|
||||
stashEntries = append(stashEntries, self.stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
return slices.MapWithIndex(utils.SplitLines(rawString), func(line string, index int) *models.StashEntry {
|
||||
return self.stashEntryFromLine(line, index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
package loaders
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
|
|
@ -27,25 +26,18 @@ func NewTagLoader(
|
|||
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
|
||||
tagsOutput, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := utils.TrimTrailingNewline(remoteBranchesStr)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
split := utils.SplitLines(tagsOutput)
|
||||
|
||||
split := strings.Split(content, "\n")
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*models.Tag, len(split))
|
||||
for i, tagName := range split {
|
||||
tags[i] = &models.Tag{
|
||||
tags := slices.Map(split, func(tagName string) *models.Tag {
|
||||
return &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
|
|
|||
68
pkg/commands/loaders/tags_test.go
Normal file
68
pkg/commands/loaders/tags_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const tagsOutput = `v0.34
|
||||
v0.33
|
||||
v0.32.2
|
||||
v0.32.1
|
||||
v0.32
|
||||
testtag
|
||||
`
|
||||
|
||||
func TestGetTags(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedTags []*models.Tag
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no tags if there are none",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-creatordate`, "", nil),
|
||||
expectedTags: []*models.Tag{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return tags if present",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-creatordate`, tagsOutput, nil),
|
||||
expectedTags: []*models.Tag{
|
||||
{Name: "v0.34"},
|
||||
{Name: "v0.33"},
|
||||
{Name: "v0.32.2"},
|
||||
{Name: "v0.32.1"},
|
||||
{Name: "v0.32"},
|
||||
{Name: "testtag"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := &TagLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
tags, err := loader.GetTags()
|
||||
|
||||
assert.Equal(t, scenario.expectedTags, tags)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,12 @@ package models
|
|||
type Branch struct {
|
||||
Name string
|
||||
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
|
||||
DisplayName string
|
||||
Recency string
|
||||
Pushables string
|
||||
Pullables string
|
||||
Head bool
|
||||
DisplayName string
|
||||
Recency string
|
||||
Pushables string
|
||||
Pullables string
|
||||
UpstreamGone bool
|
||||
Head bool
|
||||
// if we have a named remote locally this will be the name of that remote e.g.
|
||||
// 'origin' or 'tiwood'. If we don't have the remote locally it'll look like
|
||||
// 'git@github.com:tiwood/lazygit.git'
|
||||
|
|
@ -17,10 +18,18 @@ type Branch struct {
|
|||
UpstreamBranch string
|
||||
}
|
||||
|
||||
func (b *Branch) FullRefName() string {
|
||||
return "refs/heads/" + b.Name
|
||||
}
|
||||
|
||||
func (b *Branch) RefName() string {
|
||||
return b.Name
|
||||
}
|
||||
|
||||
func (b *Branch) ParentRefName() string {
|
||||
return b.RefName() + "^"
|
||||
}
|
||||
|
||||
func (b *Branch) ID() string {
|
||||
return b.RefName()
|
||||
}
|
||||
|
|
@ -39,6 +48,10 @@ func (b *Branch) RemoteBranchStoredLocally() bool {
|
|||
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?"
|
||||
}
|
||||
|
||||
func (b *Branch) RemoteBranchNotStoredLocally() bool {
|
||||
return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?"
|
||||
}
|
||||
|
||||
func (b *Branch) MatchesUpstream() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Special commit hash for empty tree object
|
||||
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
|
|
@ -14,7 +17,8 @@ type Commit struct {
|
|||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
Author string
|
||||
AuthorName string // something like 'Jesse Duffield'
|
||||
AuthorEmail string // something like 'jessedduffield@gmail.com'
|
||||
UnixTimestamp int64
|
||||
|
||||
// SHAs of parent commits (will be multiple if it's a merge commit)
|
||||
|
|
@ -25,10 +29,25 @@ func (c *Commit) ShortSha() string {
|
|||
return utils.ShortSha(c.Sha)
|
||||
}
|
||||
|
||||
func (c *Commit) FullRefName() string {
|
||||
return c.Sha
|
||||
}
|
||||
|
||||
func (c *Commit) RefName() string {
|
||||
return c.Sha
|
||||
}
|
||||
|
||||
func (c *Commit) ParentRefName() string {
|
||||
if c.IsFirstCommit() {
|
||||
return EmptyTreeCommitHash
|
||||
}
|
||||
return c.RefName() + "^"
|
||||
}
|
||||
|
||||
func (c *Commit) IsFirstCommit() bool {
|
||||
return len(c.Parents) == 0
|
||||
}
|
||||
|
||||
func (c *Commit) ID() string {
|
||||
return c.RefName()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// File : A file from git status
|
||||
|
|
@ -27,6 +28,7 @@ type IFile interface {
|
|||
GetHasStagedChanges() bool
|
||||
GetIsTracked() bool
|
||||
GetPath() string
|
||||
GetPreviousPath() string
|
||||
}
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
|
|
@ -85,3 +87,52 @@ func (f *File) GetPath() string {
|
|||
// TODO: remove concept of name; just use path
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *File) GetPreviousPath() string {
|
||||
return f.PreviousName
|
||||
}
|
||||
|
||||
type StatusFields struct {
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Deleted bool
|
||||
Added bool
|
||||
HasMergeConflicts bool
|
||||
HasInlineMergeConflicts bool
|
||||
ShortStatus string
|
||||
}
|
||||
|
||||
func SetStatusFields(file *File, shortStatus string) {
|
||||
derived := deriveStatusFields(shortStatus)
|
||||
|
||||
file.HasStagedChanges = derived.HasStagedChanges
|
||||
file.HasUnstagedChanges = derived.HasUnstagedChanges
|
||||
file.Tracked = derived.Tracked
|
||||
file.Deleted = derived.Deleted
|
||||
file.Added = derived.Added
|
||||
file.HasMergeConflicts = derived.HasMergeConflicts
|
||||
file.HasInlineMergeConflicts = derived.HasInlineMergeConflicts
|
||||
file.ShortStatus = derived.ShortStatus
|
||||
}
|
||||
|
||||
// shortStatus is something like '??' or 'A '
|
||||
func deriveStatusFields(shortStatus string) StatusFields {
|
||||
stagedChange := shortStatus[0:1]
|
||||
unstagedChange := shortStatus[1:2]
|
||||
tracked := !lo.Contains([]string{"??", "A ", "AM"}, shortStatus)
|
||||
hasStagedChanges := !lo.Contains([]string{" ", "U", "?"}, stagedChange)
|
||||
hasInlineMergeConflicts := lo.Contains([]string{"UU", "AA"}, shortStatus)
|
||||
hasMergeConflicts := hasInlineMergeConflicts || lo.Contains([]string{"DD", "AU", "UA", "UD", "DU"}, shortStatus)
|
||||
|
||||
return StatusFields{
|
||||
HasStagedChanges: hasStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: tracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || !tracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
ShortStatus: shortStatus,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,18 @@ func (r *RemoteBranch) FullName() string {
|
|||
return r.RemoteName + "/" + r.Name
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) FullRefName() string {
|
||||
return "refs/remotes/" + r.FullName()
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) RefName() string {
|
||||
return r.FullName()
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) ParentRefName() string {
|
||||
return r.RefName() + "^"
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) ID() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,18 @@ type StashEntry struct {
|
|||
Name string
|
||||
}
|
||||
|
||||
func (s *StashEntry) FullRefName() string {
|
||||
return s.RefName()
|
||||
}
|
||||
|
||||
func (s *StashEntry) RefName() string {
|
||||
return fmt.Sprintf("stash@{%d}", s.Index)
|
||||
}
|
||||
|
||||
func (s *StashEntry) ParentRefName() string {
|
||||
return s.RefName() + "^"
|
||||
}
|
||||
|
||||
func (s *StashEntry) ID() string {
|
||||
return s.RefName()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,18 @@ type Tag struct {
|
|||
Name string
|
||||
}
|
||||
|
||||
func (t *Tag) FullRefName() string {
|
||||
return "refs/tags/" + t.RefName()
|
||||
}
|
||||
|
||||
func (t *Tag) RefName() string {
|
||||
return t.Name
|
||||
}
|
||||
|
||||
func (t *Tag) ParentRefName() string {
|
||||
return t.RefName() + "^"
|
||||
}
|
||||
|
||||
func (t *Tag) ID() string {
|
||||
return t.RefName()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package oscommands
|
|||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
|
|
@ -50,6 +52,9 @@ type ICmdObj interface {
|
|||
PromptOnCredentialRequest() ICmdObj
|
||||
FailOnCredentialRequest() ICmdObj
|
||||
|
||||
WithMutex(mutex *deadlock.Mutex) ICmdObj
|
||||
Mutex() *deadlock.Mutex
|
||||
|
||||
GetCredentialStrategy() CredentialStrategy
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +75,9 @@ type CmdObj struct {
|
|||
|
||||
// if set to true, it means we might be asked to enter a username/password by this command.
|
||||
credentialStrategy CredentialStrategy
|
||||
|
||||
// can be set so that we don't run certain commands simultaneously
|
||||
mutex *deadlock.Mutex
|
||||
}
|
||||
|
||||
type CredentialStrategy int
|
||||
|
|
@ -132,6 +140,16 @@ func (self *CmdObj) IgnoreEmptyError() ICmdObj {
|
|||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) Mutex() *deadlock.Mutex {
|
||||
return self.mutex
|
||||
}
|
||||
|
||||
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) ICmdObj {
|
||||
self.mutex = mutex
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
|
||||
return self.ignoreEmptyError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ type cmdObjRunner struct {
|
|||
var _ ICmdObjRunner = &cmdObjRunner{}
|
||||
|
||||
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
}
|
||||
|
||||
if cmdObj.GetCredentialStrategy() != NONE {
|
||||
return self.runWithCredentialHandling(cmdObj)
|
||||
}
|
||||
|
|
@ -42,17 +47,14 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
|||
return self.runAndStream(cmdObj)
|
||||
}
|
||||
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
_, err := self.RunWithOutputAux(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
if cmdObj.ShouldStreamOutput() {
|
||||
err := self.runAndStream(cmdObj)
|
||||
// for now we're not capturing output, just because it would take a little more
|
||||
// effort and there's currently no use case for it. Some commands call RunWithOutput
|
||||
// but ignore the output, hence why we've got this check here.
|
||||
return "", err
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
}
|
||||
|
||||
if cmdObj.GetCredentialStrategy() != NONE {
|
||||
|
|
@ -63,6 +65,18 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
if cmdObj.ShouldStreamOutput() {
|
||||
err := self.runAndStream(cmdObj)
|
||||
// for now we're not capturing output, just because it would take a little more
|
||||
// effort and there's currently no use case for it. Some commands call RunWithOutput
|
||||
// but ignore the output, hence why we've got this check here.
|
||||
return "", err
|
||||
}
|
||||
|
||||
return self.RunWithOutputAux(cmdObj)
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
|
||||
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
|
|
@ -77,6 +91,11 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
|||
}
|
||||
|
||||
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
if cmdObj.Mutex() != nil {
|
||||
cmdObj.Mutex().Lock()
|
||||
defer cmdObj.Mutex().Unlock()
|
||||
}
|
||||
|
||||
if cmdObj.GetCredentialStrategy() != NONE {
|
||||
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
|
||||
}
|
||||
|
|
@ -104,7 +123,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
|
|||
return err
|
||||
}
|
||||
if stop {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = Kill(cmd)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -188,12 +207,15 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
cmdObj ICmdObj,
|
||||
onRun func(*cmdHandler, io.Writer),
|
||||
) error {
|
||||
// if we're streaming this we don't want any fancy terminal stuff
|
||||
cmdObj.AddEnvVars("TERM=dumb")
|
||||
|
||||
cmdWriter := self.guiIO.newCmdWriterFn()
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
self.logCmdObj(cmdObj)
|
||||
}
|
||||
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
||||
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
|
||||
cmd := cmdObj.GetCmd()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
|
@ -204,6 +226,9 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
return err
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
handler.stdoutPipe = io.TeeReader(handler.stdoutPipe, &stdout)
|
||||
|
||||
defer func() {
|
||||
if closeErr := handler.close(); closeErr != nil {
|
||||
self.log.Error(closeErr)
|
||||
|
|
@ -215,10 +240,14 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
errStr := stderr.String()
|
||||
if cmdObj.ShouldIgnoreEmptyError() && errStr == "" {
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
if cmdObj.ShouldIgnoreEmptyError() {
|
||||
return nil
|
||||
}
|
||||
return errors.New(stderr.String())
|
||||
return errors.New(stdout.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import (
|
|||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m sync.Mutex
|
||||
m deadlock.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
|
|
@ -20,6 +21,7 @@ func (b *Buffer) Read(p []byte) (n int, err error) {
|
|||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func CopyFile(src, dst string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
return
|
||||
return //nolint: nakedret
|
||||
}
|
||||
|
||||
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
|
||||
|
|
@ -133,5 +133,5 @@ func CopyDir(src string, dst string) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
return
|
||||
return //nolint: nakedret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package oscommands
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewDummyOSCommand creates a new dummy OSCommand for testing
|
||||
func NewDummyOSCommand() *OSCommand {
|
||||
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
osCmd := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
|
||||
return osCmd
|
||||
}
|
||||
|
|
@ -56,7 +57,7 @@ var dummyPlatform = &Platform{
|
|||
}
|
||||
|
||||
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
|
||||
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
osCommand := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
|
||||
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
|
||||
|
||||
return osCommand
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ type FakeCmdObjRunner struct {
|
|||
|
||||
var _ ICmdObjRunner = &FakeCmdObjRunner{}
|
||||
|
||||
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner {
|
||||
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner { //nolint:thelper
|
||||
return &FakeCmdObjRunner{t: t}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import (
|
|||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/kill"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
|
|
@ -26,6 +29,8 @@ type OSCommand struct {
|
|||
removeFileFn func(string) error
|
||||
|
||||
Cmd *CmdObjBuilder
|
||||
|
||||
tempDir string
|
||||
}
|
||||
|
||||
// Platform stores the os state
|
||||
|
|
@ -38,13 +43,14 @@ type Platform struct {
|
|||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
func NewOSCommand(common *common.Common, platform *Platform, guiIO *guiIO) *OSCommand {
|
||||
func NewOSCommand(common *common.Common, config config.AppConfigurer, platform *Platform, guiIO *guiIO) *OSCommand {
|
||||
c := &OSCommand{
|
||||
Common: common,
|
||||
Platform: platform,
|
||||
getenvFn: os.Getenv,
|
||||
removeFileFn: os.RemoveAll,
|
||||
guiIO: guiIO,
|
||||
tempDir: config.GetTempDir(),
|
||||
}
|
||||
|
||||
runner := &cmdObjRunner{log: common.Log, guiIO: guiIO}
|
||||
|
|
@ -72,9 +78,14 @@ func FileType(path string) string {
|
|||
}
|
||||
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
return c.OpenFileAtLine(filename, 1)
|
||||
}
|
||||
|
||||
func (c *OSCommand) OpenFileAtLine(filename string, lineNumber int) error {
|
||||
commandTemplate := c.UserConfig.OS.OpenCommand
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
"line": fmt.Sprintf("%d", lineNumber),
|
||||
}
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
return c.Cmd.NewShell(command).Run()
|
||||
|
|
@ -98,40 +109,40 @@ func (c *OSCommand) Quote(message string) string {
|
|||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o600)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString("\n" + line)
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
if info.Size() > 0 {
|
||||
// read last char
|
||||
buf := make([]byte, 1)
|
||||
if _, err := f.ReadAt(buf, info.Size()-1); err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
// if the last byte of the file is not a newline, add it
|
||||
if []byte("\n")[0] != buf[0] {
|
||||
_, err = f.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
_, err = f.WriteString(line + "\n")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTempFile writes a string to a new temp file and returns the file's name
|
||||
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
tmpfile, err := ioutil.TempFile("", filename)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
|
||||
return tmpfile.Name(), nil
|
||||
}
|
||||
|
||||
// CreateFileWithContent creates a file with the given content
|
||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
|
||||
|
|
@ -140,7 +151,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
if err := ioutil.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
c.Log.Error(err)
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
|
@ -168,15 +179,11 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
|||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||
logCmdStr := ""
|
||||
for i, str := range commandStrings {
|
||||
if i > 0 {
|
||||
logCmdStr += " | "
|
||||
}
|
||||
logCmdStr += str
|
||||
cmds[i] = c.Cmd.New(str).GetCmd()
|
||||
}
|
||||
cmds := slices.Map(commandStrings, func(cmdString string) *exec.Cmd {
|
||||
return c.Cmd.New(cmdString).GetCmd()
|
||||
})
|
||||
|
||||
logCmdStr := strings.Join(commandStrings, " | ")
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
|
|
@ -230,12 +237,14 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Kill kills a process. If the process has Setpgid == true, then we have anticipated that it might spawn its own child processes, so we've given it a process group ID (PGID) equal to its process id (PID) and given its child processes will inherit the PGID, we can kill that group, rather than killing the process itself.
|
||||
func Kill(cmd *exec.Cmd) error {
|
||||
if cmd.Process == nil {
|
||||
// somebody got to it before we were able to, poor bastard
|
||||
return nil
|
||||
}
|
||||
return cmd.Process.Kill()
|
||||
return kill.Kill(cmd)
|
||||
}
|
||||
|
||||
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a subprocess, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
|
||||
func PrepareForChildren(cmd *exec.Cmd) {
|
||||
kill.PrepareForChildren(cmd)
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
|
|
@ -255,8 +264,8 @@ func (c *OSCommand) Getenv(key string) string {
|
|||
return c.getenvFn(key)
|
||||
}
|
||||
|
||||
func GetTempDir() string {
|
||||
return filepath.Join(os.TempDir(), "lazygit")
|
||||
func (c *OSCommand) GetTempDir() string {
|
||||
return c.tempDir
|
||||
}
|
||||
|
||||
// GetLazygitPath returns the path of the currently executed file
|
||||
|
|
|
|||
|
|
@ -3,39 +3,12 @@ package oscommands
|
|||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOSCommandRunWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"echo -n '123'",
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(output string, err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Cmd.New(s.command).RunWithOutput())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandRun(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
|
|
@ -141,7 +114,7 @@ func TestOSCommandFileType(t *testing.T) {
|
|||
{
|
||||
"testDirectory",
|
||||
func() {
|
||||
if err := os.Mkdir("testDirectory", 0644); err != nil {
|
||||
if err := os.Mkdir("testDirectory", 0o644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
|
|
@ -165,34 +138,60 @@ func TestOSCommandFileType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOSCommandCreateTempFile(t *testing.T) {
|
||||
func TestOSCommandAppendLineToFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
content string
|
||||
test func(string, error)
|
||||
path string
|
||||
setup func(string)
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"filename",
|
||||
"content",
|
||||
func(path string, err error) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "content", string(content))
|
||||
filepath.Join(os.TempDir(), "testFile"),
|
||||
func(path string) {
|
||||
if err := ioutil.WriteFile(path, []byte("hello"), 0o600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "hello\nworld\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
filepath.Join(os.TempDir(), "emptyTestFile"),
|
||||
func(path string) {
|
||||
if err := ioutil.WriteFile(path, []byte(""), 0o600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "world\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
filepath.Join(os.TempDir(), "testFileWithNewline"),
|
||||
func(path string) {
|
||||
if err := ioutil.WriteFile(path, []byte("hello\n"), 0o600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "hello\nworld\n", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
|
||||
})
|
||||
s.setup(s.path)
|
||||
osCommand := NewDummyOSCommand()
|
||||
if err := osCommand.AppendLineToFile(s.path, "world"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f, err := ioutil.ReadFile(s.path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.test(string(f))
|
||||
_ = os.RemoveAll(s.path)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,34 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOSCommandRunWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"echo -n '123'",
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(output string, err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Cmd.New(s.command).RunWithOutput())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type PatchHunk struct {
|
||||
|
|
@ -54,7 +55,7 @@ func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
|
|||
if line == "" {
|
||||
break
|
||||
}
|
||||
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
|
||||
isLineSelected := lo.Contains(lineIndices, lineIdx)
|
||||
|
||||
firstChar, content := line[:1], line[1:]
|
||||
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
@ -26,8 +28,10 @@ type fileInfo struct {
|
|||
diff string
|
||||
}
|
||||
|
||||
type applyPatchFunc func(patch string, flags ...string) error
|
||||
type loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
type (
|
||||
applyPatchFunc func(patch string, flags ...string) error
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
)
|
||||
|
||||
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
|
||||
type PatchManager struct {
|
||||
|
|
@ -70,8 +74,9 @@ func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
|
|||
func (p *PatchManager) addFileWhole(info *fileInfo) {
|
||||
info.mode = WHOLE
|
||||
lineCount := len(strings.Split(info.diff, "\n"))
|
||||
info.includedLineIndices = make([]int, lineCount)
|
||||
// add every line index
|
||||
// TODO: add tests and then use lo.Range to simplify
|
||||
info.includedLineIndices = make([]int, lineCount)
|
||||
for i := 0; i < lineCount; i++ {
|
||||
info.includedLineIndices[i] = i
|
||||
}
|
||||
|
|
@ -138,7 +143,7 @@ func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineI
|
|||
return err
|
||||
}
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
info.includedLineIndices = lo.Union(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -149,7 +154,7 @@ func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLi
|
|||
return err
|
||||
}
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
p.removeFile(info)
|
||||
}
|
||||
|
|
@ -185,26 +190,20 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
|
|||
parser := NewPatchParser(p.Log, patch)
|
||||
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(-1, -1, nil)
|
||||
return parser.Render(false, -1, -1, nil)
|
||||
}
|
||||
|
||||
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
|
||||
// sort files by name then iterate through and render each patch
|
||||
filenames := make([]string, len(p.fileInfoMap))
|
||||
index := 0
|
||||
for filename := range p.fileInfoMap {
|
||||
filenames[index] = filename
|
||||
index++
|
||||
}
|
||||
filenames := maps.Keys(p.fileInfoMap)
|
||||
|
||||
sort.Strings(filenames)
|
||||
output := []string{}
|
||||
for _, filename := range filenames {
|
||||
patch := p.RenderPatchForFile(filename, plain, false, true)
|
||||
if patch != "" {
|
||||
output = append(output, patch)
|
||||
}
|
||||
}
|
||||
patches := slices.Map(filenames, func(filename string) string {
|
||||
return p.RenderPatchForFile(filename, plain, false, true)
|
||||
})
|
||||
output := slices.Filter(patches, func(patch string) bool {
|
||||
return patch != ""
|
||||
})
|
||||
|
||||
return output
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||
var (
|
||||
hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||
)
|
||||
|
||||
func GetHeaderFromDiff(diff string) string {
|
||||
match := patchHeaderRegexp.FindStringSubmatch(diff)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
@ -182,29 +183,39 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine) {
|
|||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
|
||||
renderedLines := make([]string, len(p.PatchLines))
|
||||
for index, patchLine := range p.PatchLines {
|
||||
selected := index >= firstLineIndex && index <= lastLineIndex
|
||||
included := utils.IncludesInt(incLineIndices, index)
|
||||
renderedLines[index] = patchLine.render(selected, included)
|
||||
}
|
||||
result := strings.Join(renderedLines, "\n")
|
||||
if strings.TrimSpace(utils.Decolorise(result)) == "" {
|
||||
func (p *PatchParser) Render(isFocused bool, firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
|
||||
contentToDisplay := slices.Some(p.PatchLines, func(line *PatchLine) bool {
|
||||
return line.Content != ""
|
||||
})
|
||||
if !contentToDisplay {
|
||||
return ""
|
||||
}
|
||||
|
||||
renderedLines := slices.MapWithIndex(p.PatchLines, func(patchLine *PatchLine, index int) string {
|
||||
selected := isFocused && index >= firstLineIndex && index <= lastLineIndex
|
||||
included := lo.Contains(incLineIndices, index)
|
||||
return patchLine.render(selected, included)
|
||||
})
|
||||
|
||||
result := strings.Join(renderedLines, "\n")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// PlainRenderLines returns the non-coloured string of diff part from firstLineIndex to
|
||||
// lastLineIndex
|
||||
func (p *PatchParser) PlainRenderLines(firstLineIndex, lastLineIndex int) string {
|
||||
linesToCopy := p.PatchLines[firstLineIndex : lastLineIndex+1]
|
||||
func (p *PatchParser) RenderPlain() string {
|
||||
return renderLinesPlain(p.PatchLines)
|
||||
}
|
||||
|
||||
renderedLines := make([]string, len(linesToCopy))
|
||||
for index, line := range linesToCopy {
|
||||
renderedLines[index] = line.Content
|
||||
}
|
||||
// RenderLinesPlain returns the non-coloured string of diff part from firstLineIndex to
|
||||
// lastLineIndex
|
||||
func (p *PatchParser) RenderLinesPlain(firstLineIndex, lastLineIndex int) string {
|
||||
return renderLinesPlain(p.PatchLines[firstLineIndex : lastLineIndex+1])
|
||||
}
|
||||
|
||||
func renderLinesPlain(lines []*PatchLine) string {
|
||||
renderedLines := slices.Map(lines, func(line *PatchLine) string {
|
||||
return line.Content
|
||||
})
|
||||
|
||||
return strings.Join(renderedLines, "\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -14,7 +15,6 @@ import (
|
|||
type AppConfig struct {
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
|
|
@ -27,15 +27,11 @@ 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
|
||||
|
||||
// build info
|
||||
GetVersion() string
|
||||
GetCommit() string
|
||||
GetBuildDate() string
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
|
||||
|
|
@ -43,13 +39,22 @@ type AppConfigurer interface {
|
|||
GetUserConfigPaths() []string
|
||||
GetUserConfigDir() string
|
||||
ReloadUserConfig() error
|
||||
GetTempDir() string
|
||||
|
||||
GetAppState() *AppState
|
||||
SaveAppState() error
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
|
||||
func NewAppConfig(
|
||||
name string,
|
||||
version,
|
||||
commit,
|
||||
date string,
|
||||
buildSource string,
|
||||
debuggingFlag bool,
|
||||
tempDir string,
|
||||
) (*AppConfig, error) {
|
||||
configDir, err := findOrCreateConfigDir()
|
||||
if err != nil && !os.IsPermission(err) {
|
||||
return nil, err
|
||||
|
|
@ -70,21 +75,14 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debuggingFlag = true
|
||||
}
|
||||
|
||||
tempDir := filepath.Join(os.TempDir(), "lazygit")
|
||||
|
||||
appState, err := loadAppState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Name: name,
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
|
|
@ -123,7 +121,7 @@ func configDirForVendor(vendor string) string {
|
|||
|
||||
func findOrCreateConfigDir() (string, error) {
|
||||
folder := ConfigDir()
|
||||
return folder, os.MkdirAll(folder, 0755)
|
||||
return folder, os.MkdirAll(folder, 0o755)
|
||||
}
|
||||
|
||||
func loadUserConfigWithDefaults(configFiles []string) (*UserConfig, error) {
|
||||
|
|
@ -160,7 +158,7 @@ func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error)
|
|||
}
|
||||
|
||||
if err := yaml.Unmarshal(content, base); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("The config at `%s` couldn't be parsed, please inspect it before opening up an issue.\n%w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,14 +173,6 @@ func (c *AppConfig) GetVersion() string {
|
|||
return c.Version
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetCommit() string {
|
||||
return c.Commit
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetBuildDate() string {
|
||||
return c.BuildDate
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetName() string {
|
||||
return c.Name
|
||||
}
|
||||
|
|
@ -221,6 +211,10 @@ func (c *AppConfig) ReloadUserConfig() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetTempDir() string {
|
||||
return c.TempDir
|
||||
}
|
||||
|
||||
func configFilePath(filename string) (string, error) {
|
||||
folder, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
|
|
@ -232,7 +226,7 @@ func configFilePath(filename string) (string, error) {
|
|||
|
||||
var ConfigFilename = "config.yml"
|
||||
|
||||
// ConfigFilename returns the filename of the deafult config file
|
||||
// ConfigFilename returns the filename of the default config file
|
||||
func (c *AppConfig) ConfigFilename() string {
|
||||
return filepath.Join(c.UserConfigDir, ConfigFilename)
|
||||
}
|
||||
|
|
@ -249,7 +243,7 @@ func (c *AppConfig) SaveAppState() error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
err = ioutil.WriteFile(filepath, marshalledAppState, 0o644)
|
||||
if err != nil && os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ package config
|
|||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: "open -- {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isWSL() bool {
|
||||
data, err := ioutil.ReadFile("/proc/sys/kernel/osrelease")
|
||||
return err == nil && strings.Contains(string(data), "microsoft")
|
||||
}
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
if isWSL() {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
|
||||
OpenLinkCommand: `powershell.exe start {{link}} >/dev/null`,
|
||||
}
|
||||
}
|
||||
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: `xdg-open {{filename}} >/dev/null`,
|
||||
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package config
|
|||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
EditCommandTemplate: "",
|
||||
OpenCommand: `start "" {{filename}}`,
|
||||
OpenLinkCommand: `start "" {{link}}`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,11 @@ import (
|
|||
// NewDummyAppConfig creates a new dummy AppConfig for testing
|
||||
func NewDummyAppConfig() *AppConfig {
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
Commit: "",
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: GetDefaultConfig(),
|
||||
AppState: &AppState{},
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
Debug: false,
|
||||
UserConfig: GetDefaultConfig(),
|
||||
AppState: &AppState{},
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserConfig struct {
|
||||
Gui GuiConfig `yaml:"gui"`
|
||||
Git GitConfig `yaml:"git"`
|
||||
|
|
@ -11,11 +15,12 @@ type UserConfig struct {
|
|||
QuitOnTopLevelReturn bool `yaml:"quitOnTopLevelReturn"`
|
||||
Keybinding KeybindingConfig `yaml:"keybinding"`
|
||||
// OS determines what defaults are set for opening files and links
|
||||
OS OSConfig `yaml:"os,omitempty"`
|
||||
DisableStartupPopups bool `yaml:"disableStartupPopups"`
|
||||
CustomCommands []CustomCommand `yaml:"customCommands"`
|
||||
Services map[string]string `yaml:"services"`
|
||||
NotARepository string `yaml:"notARepository"`
|
||||
OS OSConfig `yaml:"os,omitempty"`
|
||||
DisableStartupPopups bool `yaml:"disableStartupPopups"`
|
||||
CustomCommands []CustomCommand `yaml:"customCommands"`
|
||||
Services map[string]string `yaml:"services"`
|
||||
NotARepository string `yaml:"notARepository"`
|
||||
PromptToReturnFromSubprocess bool `yaml:"promptToReturnFromSubprocess"`
|
||||
}
|
||||
|
||||
type RefresherConfig struct {
|
||||
|
|
@ -35,6 +40,7 @@ type GuiConfig struct {
|
|||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||
Language string `yaml:"language"`
|
||||
TimeFormat string `yaml:"timeFormat"`
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
|
|
@ -42,7 +48,10 @@ type GuiConfig struct {
|
|||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
ShowRandomTip bool `yaml:"showRandomTip"`
|
||||
ShowCommandLog bool `yaml:"showCommandLog"`
|
||||
ShowBottomLine bool `yaml:"showBottomLine"`
|
||||
ShowIcons bool `yaml:"showIcons"`
|
||||
CommandLogSize int `yaml:"commandLogSize"`
|
||||
SplitDiff string `yaml:"splitDiff"`
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
|
|
@ -54,6 +63,7 @@ type ThemeConfig struct {
|
|||
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
|
||||
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
|
||||
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
|
||||
UnstagedChangesColor []string `yaml:"unstagedChangesColor"`
|
||||
}
|
||||
|
||||
type CommitLengthConfig struct {
|
||||
|
|
@ -66,6 +76,7 @@ type GitConfig struct {
|
|||
Merging MergingConfig `yaml:"merging"`
|
||||
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
||||
AutoFetch bool `yaml:"autoFetch"`
|
||||
AutoRefresh bool `yaml:"autoRefresh"`
|
||||
BranchLogCmd string `yaml:"branchLogCmd"`
|
||||
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
|
||||
OverrideGpg bool `yaml:"overrideGpg"`
|
||||
|
|
@ -94,8 +105,9 @@ type MergingConfig struct {
|
|||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
|
||||
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
|
||||
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
|
||||
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
|
||||
ShowWholeGraph bool `yaml:"showWholeGraph"`
|
||||
}
|
||||
|
||||
type CommitPrefixConfig struct {
|
||||
|
|
@ -199,7 +211,7 @@ type KeybindingFilesConfig struct {
|
|||
CommitChangesWithoutHook string `yaml:"commitChangesWithoutHook"`
|
||||
AmendLastCommit string `yaml:"amendLastCommit"`
|
||||
CommitChangesWithEditor string `yaml:"commitChangesWithEditor"`
|
||||
IgnoreFile string `yaml:"ignoreFile"`
|
||||
IgnoreOrExcludeFile string `yaml:"IgnoreOrExcludeFile"`
|
||||
RefreshFiles string `yaml:"refreshFiles"`
|
||||
StashAllChanges string `yaml:"stashAllChanges"`
|
||||
ViewStashOptions string `yaml:"viewStashOptions"`
|
||||
|
|
@ -228,28 +240,29 @@ type KeybindingBranchesConfig struct {
|
|||
}
|
||||
|
||||
type KeybindingCommitsConfig struct {
|
||||
SquashDown string `yaml:"squashDown"`
|
||||
RenameCommit string `yaml:"renameCommit"`
|
||||
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
|
||||
CreateFixupCommit string `yaml:"createFixupCommit"`
|
||||
SquashAboveCommits string `yaml:"squashAboveCommits"`
|
||||
MoveDownCommit string `yaml:"moveDownCommit"`
|
||||
MoveUpCommit string `yaml:"moveUpCommit"`
|
||||
AmendToCommit string `yaml:"amendToCommit"`
|
||||
PickCommit string `yaml:"pickCommit"`
|
||||
RevertCommit string `yaml:"revertCommit"`
|
||||
CherryPickCopy string `yaml:"cherryPickCopy"`
|
||||
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
|
||||
PasteCommits string `yaml:"pasteCommits"`
|
||||
TagCommit string `yaml:"tagCommit"`
|
||||
CheckoutCommit string `yaml:"checkoutCommit"`
|
||||
ResetCherryPick string `yaml:"resetCherryPick"`
|
||||
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
OpenInBrowser string `yaml:"openInBrowser"`
|
||||
ViewBisectOptions string `yaml:"viewBisectOptions"`
|
||||
SquashDown string `yaml:"squashDown"`
|
||||
RenameCommit string `yaml:"renameCommit"`
|
||||
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
|
||||
CreateFixupCommit string `yaml:"createFixupCommit"`
|
||||
SquashAboveCommits string `yaml:"squashAboveCommits"`
|
||||
MoveDownCommit string `yaml:"moveDownCommit"`
|
||||
MoveUpCommit string `yaml:"moveUpCommit"`
|
||||
AmendToCommit string `yaml:"amendToCommit"`
|
||||
ResetCommitAuthor string `yaml:"resetCommitAuthor"`
|
||||
PickCommit string `yaml:"pickCommit"`
|
||||
RevertCommit string `yaml:"revertCommit"`
|
||||
CherryPickCopy string `yaml:"cherryPickCopy"`
|
||||
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
|
||||
PasteCommits string `yaml:"pasteCommits"`
|
||||
TagCommit string `yaml:"tagCommit"`
|
||||
CheckoutCommit string `yaml:"checkoutCommit"`
|
||||
ResetCherryPick string `yaml:"resetCherryPick"`
|
||||
CopyCommitAttributeToClipboard string `yaml:"copyCommitAttributeToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
OpenInBrowser string `yaml:"openInBrowser"`
|
||||
ViewBisectOptions string `yaml:"viewBisectOptions"`
|
||||
}
|
||||
|
||||
type KeybindingStashConfig struct {
|
||||
|
|
@ -265,6 +278,7 @@ type KeybindingMainConfig struct {
|
|||
ToggleDragSelectAlt string `yaml:"toggleDragSelect-alt"`
|
||||
ToggleSelectHunk string `yaml:"toggleSelectHunk"`
|
||||
PickBothHunks string `yaml:"pickBothHunks"`
|
||||
EditSelectHunk string `yaml:"editSelectHunk"`
|
||||
}
|
||||
|
||||
type KeybindingSubmodulesConfig struct {
|
||||
|
|
@ -297,15 +311,19 @@ type CustomCommand struct {
|
|||
LoadingText string `yaml:"loadingText"`
|
||||
Description string `yaml:"description"`
|
||||
Stream bool `yaml:"stream"`
|
||||
ShowOutput bool `yaml:"showOutput"`
|
||||
}
|
||||
|
||||
type CustomCommandPrompt struct {
|
||||
Type string `yaml:"type"` // one of 'input' and 'menu'
|
||||
Type string `yaml:"type"` // one of 'input', 'menu', or 'confirm'
|
||||
Title string `yaml:"title"`
|
||||
|
||||
// this only apply to prompts
|
||||
// this only apply to input prompts
|
||||
InitialValue string `yaml:"initialValue"`
|
||||
|
||||
// this only applies to confirm prompts
|
||||
Body string `yaml:"body"`
|
||||
|
||||
// this only applies to menus
|
||||
Options []CustomCommandMenuOption
|
||||
|
||||
|
|
@ -334,29 +352,35 @@ func GetDefaultConfig() *UserConfig {
|
|||
ExpandFocusedSidePanel: false,
|
||||
MainPanelSplitMode: "flexible",
|
||||
Language: "auto",
|
||||
TimeFormat: time.RFC822,
|
||||
Theme: ThemeConfig{
|
||||
LightTheme: false,
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
InactiveBorderColor: []string{"white"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineBgColor: []string{"default"},
|
||||
SelectedLineBgColor: []string{"blue"},
|
||||
SelectedRangeBgColor: []string{"blue"},
|
||||
CherryPickedCommitBgColor: []string{"blue"},
|
||||
CherryPickedCommitFgColor: []string{"cyan"},
|
||||
CherryPickedCommitBgColor: []string{"cyan"},
|
||||
CherryPickedCommitFgColor: []string{"blue"},
|
||||
UnstagedChangesColor: []string{"red"},
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowListFooter: true,
|
||||
ShowCommandLog: true,
|
||||
ShowBottomLine: true,
|
||||
ShowFileTree: true,
|
||||
ShowRandomTip: true,
|
||||
ShowIcons: false,
|
||||
CommandLogSize: 8,
|
||||
SplitDiff: "auto",
|
||||
},
|
||||
Git: GitConfig{
|
||||
Paging: PagingConfig{
|
||||
ColorArg: "always",
|
||||
Pager: "",
|
||||
UseConfig: false},
|
||||
UseConfig: false,
|
||||
},
|
||||
Commit: CommitConfig{
|
||||
SignOff: false,
|
||||
},
|
||||
|
|
@ -365,11 +389,13 @@ func GetDefaultConfig() *UserConfig {
|
|||
Args: "",
|
||||
},
|
||||
Log: LogConfig{
|
||||
Order: "topo-order",
|
||||
ShowGraph: "when-maximised",
|
||||
Order: "topo-order",
|
||||
ShowGraph: "when-maximised",
|
||||
ShowWholeGraph: false,
|
||||
},
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
AutoRefresh: true,
|
||||
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
|
||||
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
||||
DisableForcePushing: false,
|
||||
|
|
@ -467,7 +493,7 @@ func GetDefaultConfig() *UserConfig {
|
|||
CommitChangesWithoutHook: "w",
|
||||
AmendLastCommit: "A",
|
||||
CommitChangesWithEditor: "C",
|
||||
IgnoreFile: "i",
|
||||
IgnoreOrExcludeFile: "i",
|
||||
RefreshFiles: "r",
|
||||
StashAllChanges: "s",
|
||||
ViewStashOptions: "S",
|
||||
|
|
@ -494,28 +520,29 @@ func GetDefaultConfig() *UserConfig {
|
|||
FetchRemote: "f",
|
||||
},
|
||||
Commits: KeybindingCommitsConfig{
|
||||
SquashDown: "s",
|
||||
RenameCommit: "r",
|
||||
RenameCommitWithEditor: "R",
|
||||
ViewResetOptions: "g",
|
||||
MarkCommitAsFixup: "f",
|
||||
CreateFixupCommit: "F",
|
||||
SquashAboveCommits: "S",
|
||||
MoveDownCommit: "<c-j>",
|
||||
MoveUpCommit: "<c-k>",
|
||||
AmendToCommit: "A",
|
||||
PickCommit: "p",
|
||||
RevertCommit: "t",
|
||||
CherryPickCopy: "c",
|
||||
CherryPickCopyRange: "C",
|
||||
PasteCommits: "v",
|
||||
TagCommit: "T",
|
||||
CheckoutCommit: "<space>",
|
||||
ResetCherryPick: "<c-R>",
|
||||
CopyCommitMessageToClipboard: "<c-y>",
|
||||
OpenLogMenu: "<c-l>",
|
||||
OpenInBrowser: "o",
|
||||
ViewBisectOptions: "b",
|
||||
SquashDown: "s",
|
||||
RenameCommit: "r",
|
||||
RenameCommitWithEditor: "R",
|
||||
ViewResetOptions: "g",
|
||||
MarkCommitAsFixup: "f",
|
||||
CreateFixupCommit: "F",
|
||||
SquashAboveCommits: "S",
|
||||
MoveDownCommit: "<c-j>",
|
||||
MoveUpCommit: "<c-k>",
|
||||
AmendToCommit: "A",
|
||||
ResetCommitAuthor: "a",
|
||||
PickCommit: "p",
|
||||
RevertCommit: "t",
|
||||
CherryPickCopy: "c",
|
||||
CherryPickCopyRange: "C",
|
||||
PasteCommits: "v",
|
||||
TagCommit: "T",
|
||||
CheckoutCommit: "<space>",
|
||||
ResetCherryPick: "<c-R>",
|
||||
CopyCommitAttributeToClipboard: "y",
|
||||
OpenLogMenu: "<c-l>",
|
||||
OpenInBrowser: "o",
|
||||
ViewBisectOptions: "b",
|
||||
},
|
||||
Stash: KeybindingStashConfig{
|
||||
PopStash: "g",
|
||||
|
|
@ -528,6 +555,7 @@ func GetDefaultConfig() *UserConfig {
|
|||
ToggleDragSelectAlt: "V",
|
||||
ToggleSelectHunk: "a",
|
||||
PickBothHunks: "b",
|
||||
EditSelectHunk: "E",
|
||||
},
|
||||
Submodules: KeybindingSubmodulesConfig{
|
||||
Init: "i",
|
||||
|
|
@ -535,10 +563,11 @@ func GetDefaultConfig() *UserConfig {
|
|||
BulkMenu: "b",
|
||||
},
|
||||
},
|
||||
OS: GetPlatformDefaultConfig(),
|
||||
DisableStartupPopups: false,
|
||||
CustomCommands: []CustomCommand(nil),
|
||||
Services: map[string]string(nil),
|
||||
NotARepository: "prompt",
|
||||
OS: GetPlatformDefaultConfig(),
|
||||
DisableStartupPopups: false,
|
||||
CustomCommands: []CustomCommand(nil),
|
||||
Services: map[string]string(nil),
|
||||
NotARepository: "prompt",
|
||||
PromptToReturnFromSubprocess: true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
pkg/env/env.go
vendored
6
pkg/env/env.go
vendored
|
|
@ -1,6 +1,10 @@
|
|||
package env
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// This package encapsulates accessing/mutating the ENV of the program.
|
||||
|
||||
func GetGitDirEnv() string {
|
||||
return os.Getenv("GIT_DIR")
|
||||
|
|
|
|||
|
|
@ -1,35 +1,34 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
// statusManager's job is to handle rendering of loading states and toast notifications
|
||||
// that you see at the bottom left of the screen.
|
||||
type statusManager struct {
|
||||
statuses []appStatus
|
||||
nextId int
|
||||
mutex deadlock.Mutex
|
||||
}
|
||||
|
||||
type appStatus struct {
|
||||
message string
|
||||
statusType string
|
||||
id int
|
||||
}
|
||||
|
||||
type statusManager struct {
|
||||
statuses []appStatus
|
||||
nextId int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (m *statusManager) removeStatus(id int) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
newStatuses := []appStatus{}
|
||||
for _, status := range m.statuses {
|
||||
if status.id != id {
|
||||
newStatuses = append(newStatuses, status)
|
||||
}
|
||||
}
|
||||
m.statuses = newStatuses
|
||||
m.statuses = slices.Filter(m.statuses, func(status appStatus) bool {
|
||||
return status.id != id
|
||||
})
|
||||
}
|
||||
|
||||
func (m *statusManager) addWaitingStatus(message string) int {
|
||||
|
|
@ -83,7 +82,7 @@ func (m *statusManager) getStatusString() string {
|
|||
return topStatus.message
|
||||
}
|
||||
|
||||
func (gui *Gui) raiseToast(message string) {
|
||||
func (gui *Gui) toast(message string) {
|
||||
gui.statusManager.addToastStatus(message)
|
||||
|
||||
gui.renderAppStatus()
|
||||
|
|
@ -95,7 +94,7 @@ func (gui *Gui) renderAppStatus() {
|
|||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
gui.OnUIThread(func() error {
|
||||
gui.c.OnUIThread(func() error {
|
||||
return gui.renderString(gui.Views.AppStatus, appStatus)
|
||||
})
|
||||
|
||||
|
|
@ -106,8 +105,8 @@ func (gui *Gui) renderAppStatus() {
|
|||
})
|
||||
}
|
||||
|
||||
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
||||
func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
|
||||
// withWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
||||
func (gui *Gui) withWaitingStatus(message string, f func() error) error {
|
||||
go utils.Safe(func() {
|
||||
id := gui.statusManager.addWaitingStatus(message)
|
||||
|
||||
|
|
@ -118,8 +117,8 @@ func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
|
|||
gui.renderAppStatus()
|
||||
|
||||
if err := f(); err != nil {
|
||||
gui.OnUIThread(func() error {
|
||||
return gui.surfaceError(err)
|
||||
gui.c.OnUIThread(func() error {
|
||||
return gui.c.Error(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,154 +2,17 @@ package gui
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// In this file we use the boxlayout package, along with knowledge about the app's state,
|
||||
// to arrange the windows (i.e. panels) on the screen.
|
||||
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// if we're not in split mode we can just show the one main panel. Likewise if
|
||||
// the main panel is focused and we're in full-screen mode
|
||||
if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
main := "main"
|
||||
secondary := "secondary"
|
||||
if gui.secondaryViewFocused() {
|
||||
// when you think you've focused the secondary view, we've actually just swapped them around in the layout
|
||||
main, secondary = secondary, main
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: main,
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: secondary,
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||
sidePanelWidthRatio := gui.UserConfig.Gui.SidePanelWidth
|
||||
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||
sideSectionWeight := 1
|
||||
|
||||
if gui.splitMainPanelSideBySide() {
|
||||
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
|
||||
}
|
||||
|
||||
if currentWindow == "main" {
|
||||
if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL {
|
||||
sideSectionWeight = 0
|
||||
}
|
||||
} else {
|
||||
if gui.State.ScreenMode == SCREEN_HALF {
|
||||
mainSectionWeight = 1
|
||||
} else if gui.State.ScreenMode == SCREEN_FULL {
|
||||
mainSectionWeight = 0
|
||||
}
|
||||
}
|
||||
|
||||
return sideSectionWeight, mainSectionWeight
|
||||
}
|
||||
|
||||
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
|
||||
if gui.State.Searching.isSearching {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "searchPrefix",
|
||||
Size: len(SEARCH_PREFIX),
|
||||
},
|
||||
{
|
||||
Window: "search",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
result := []*boxlayout.Box{}
|
||||
|
||||
if len(appStatus) > 0 {
|
||||
result = append(result,
|
||||
&boxlayout.Box{
|
||||
Window: "appStatus",
|
||||
Size: len(appStatus) + len(INFO_SECTION_PADDING),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
[]*boxlayout.Box{
|
||||
{
|
||||
Window: "options",
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "information",
|
||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||
Size: len(INFO_SECTION_PADDING) + len(utils.Decolorise(informationStr)),
|
||||
},
|
||||
}...,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) splitMainPanelSideBySide() bool {
|
||||
if !gui.isMainPanelSplit() {
|
||||
return false
|
||||
}
|
||||
|
||||
mainPanelSplitMode := gui.UserConfig.Gui.MainPanelSplitMode
|
||||
width, height := gui.g.Size()
|
||||
|
||||
switch mainPanelSplitMode {
|
||||
case "vertical":
|
||||
return false
|
||||
case "horizontal":
|
||||
return true
|
||||
default:
|
||||
if width < 200 && height > 30 { // 2 80 character width panels + 40 width for side panel
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
|
||||
if !gui.ShowExtrasWindow {
|
||||
return 0
|
||||
}
|
||||
|
||||
var baseSize int
|
||||
if gui.currentStaticContext().GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
baseSize = 1000 // my way of saying 'fill the available space'
|
||||
} else if screenHeight < 40 {
|
||||
baseSize = 1
|
||||
} else {
|
||||
baseSize = gui.UserConfig.Gui.CommandLogSize
|
||||
}
|
||||
|
||||
frameSize := 2
|
||||
return baseSize + frameSize
|
||||
}
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
width, height := gui.g.Size()
|
||||
|
||||
|
|
@ -168,6 +31,12 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||
|
||||
extrasWindowSize := gui.getExtrasWindowSize(height)
|
||||
|
||||
showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || (gui.State.Searching.isSearching || gui.isAnyModeActive())
|
||||
infoSectionSize := 0
|
||||
if showInfoSection {
|
||||
infoSectionSize = 1
|
||||
}
|
||||
|
||||
root := &boxlayout.Box{
|
||||
Direction: boxlayout.ROW,
|
||||
Children: []*boxlayout.Box{
|
||||
|
|
@ -199,13 +68,163 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||
},
|
||||
{
|
||||
Direction: boxlayout.COLUMN,
|
||||
Size: 1,
|
||||
Size: infoSectionSize,
|
||||
Children: gui.infoSectionChildren(informationStr, appStatus),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
|
||||
layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, width, height)
|
||||
limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
|
||||
|
||||
return MergeMaps(layerOneWindows, limitWindows)
|
||||
}
|
||||
|
||||
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
|
||||
result := map[K]V{}
|
||||
for _, currMap := range maps {
|
||||
for key, value := range currMap {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// if we're not in split mode we can just show the one main panel. Likewise if
|
||||
// the main panel is focused and we're in full-screen mode
|
||||
if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "secondary",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||
sidePanelWidthRatio := gui.c.UserConfig.Gui.SidePanelWidth
|
||||
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||
sideSectionWeight := 1
|
||||
|
||||
if gui.splitMainPanelSideBySide() {
|
||||
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
|
||||
}
|
||||
|
||||
if currentWindow == "main" {
|
||||
if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL {
|
||||
sideSectionWeight = 0
|
||||
}
|
||||
} else {
|
||||
if gui.State.ScreenMode == SCREEN_HALF {
|
||||
mainSectionWeight = 1
|
||||
} else if gui.State.ScreenMode == SCREEN_FULL {
|
||||
mainSectionWeight = 0
|
||||
}
|
||||
}
|
||||
|
||||
return sideSectionWeight, mainSectionWeight
|
||||
}
|
||||
|
||||
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
|
||||
if gui.State.Searching.isSearching {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "searchPrefix",
|
||||
Size: runewidth.StringWidth(SEARCH_PREFIX),
|
||||
},
|
||||
{
|
||||
Window: "search",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
result := []*boxlayout.Box{}
|
||||
|
||||
if len(appStatus) > 0 {
|
||||
result = append(result,
|
||||
&boxlayout.Box{
|
||||
Window: "appStatus",
|
||||
Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
[]*boxlayout.Box{
|
||||
{
|
||||
Window: "options",
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "information",
|
||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
|
||||
},
|
||||
}...,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) splitMainPanelSideBySide() bool {
|
||||
if !gui.isMainPanelSplit() {
|
||||
return false
|
||||
}
|
||||
|
||||
mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode
|
||||
width, height := gui.g.Size()
|
||||
|
||||
switch mainPanelSplitMode {
|
||||
case "vertical":
|
||||
return false
|
||||
case "horizontal":
|
||||
return true
|
||||
default:
|
||||
if width < 200 && height > 30 { // 2 80 character width panels + 40 width for side panel
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
|
||||
if !gui.ShowExtrasWindow {
|
||||
return 0
|
||||
}
|
||||
|
||||
var baseSize int
|
||||
if gui.currentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY {
|
||||
baseSize = 1000 // my way of saying 'fill the available space'
|
||||
} else if screenHeight < 40 {
|
||||
baseSize = 1
|
||||
} else {
|
||||
baseSize = gui.c.UserConfig.Gui.CommandLogSize
|
||||
}
|
||||
|
||||
frameSize := 2
|
||||
return baseSize + frameSize
|
||||
}
|
||||
|
||||
// The stash window by default only contains one line so that it's not hogging
|
||||
|
|
@ -259,7 +278,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||
fullHeightBox("stash"),
|
||||
}
|
||||
} else if height >= 28 {
|
||||
accordionMode := gui.UserConfig.Gui.ExpandFocusedSidePanel
|
||||
accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel
|
||||
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordionMode && defaultBox.Window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
|
|
@ -311,6 +330,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getCyclableWindows() []string {
|
||||
return []string{"status", "files", "branches", "commits", "stash"}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideWindowName() string {
|
||||
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
|
||||
gui.State.ContextManager.RLock()
|
||||
|
|
@ -320,7 +343,7 @@ func (gui *Gui) currentSideWindowName() string {
|
|||
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
|
||||
context := gui.State.ContextManager.ContextStack[reversedIdx]
|
||||
|
||||
if context.GetKind() == SIDE_CONTEXT {
|
||||
if context.GetKind() == types.SIDE_CONTEXT {
|
||||
return context.GetWindowName()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
package gui
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func(opts ...OnFocusOpts) error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
// this is for pushing some content to the main view
|
||||
OnRenderToMain func(opts ...OnFocusOpts) error
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
WindowName string
|
||||
OnGetOptionsMap func() map[string]string
|
||||
|
||||
ParentContext Context
|
||||
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
|
||||
hasParent bool
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetOptionsMap() map[string]string {
|
||||
if self.OnGetOptionsMap != nil {
|
||||
return self.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicContext) SetParentContext(context Context) {
|
||||
self.ParentContext = context
|
||||
self.hasParent = true
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetParentContext() (Context, bool) {
|
||||
return self.ParentContext, self.hasParent
|
||||
}
|
||||
|
||||
func (self *BasicContext) SetWindowName(windowName string) {
|
||||
self.WindowName = windowName
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetWindowName() string {
|
||||
windowName := self.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return self.ViewName
|
||||
}
|
||||
|
||||
func (self *BasicContext) HandleRender() error {
|
||||
if self.OnRender != nil {
|
||||
return self.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetViewName() string {
|
||||
return self.ViewName
|
||||
}
|
||||
|
||||
func (self *BasicContext) HandleFocus(opts ...OnFocusOpts) error {
|
||||
if self.OnFocus != nil {
|
||||
if err := self.OnFocus(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if self.OnRenderToMain != nil {
|
||||
if err := self.OnRenderToMain(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicContext) HandleFocusLost() error {
|
||||
if self.OnFocusLost != nil {
|
||||
return self.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicContext) HandleRenderToMain() error {
|
||||
if self.OnRenderToMain != nil {
|
||||
return self.OnRenderToMain()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetKind() ContextKind {
|
||||
return self.Kind
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetKey() ContextKey {
|
||||
return self.Key
|
||||
}
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleOpenBisectMenu() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// no shame in getting this directly rather than using the cached value
|
||||
// given how cheap it is to obtain
|
||||
info := gui.Git.Bisect.GetInfo()
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if info.Started() {
|
||||
return gui.openMidBisectMenu(info, commit)
|
||||
} else {
|
||||
return gui.openStartBisectMenu(info, commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
||||
// if there is not yet a 'current' bisect commit, or if we have
|
||||
// selected the current commit, we need to jump to the next 'current' commit
|
||||
// after we perform a bisect action. The reason we don't unconditionally jump
|
||||
// is that sometimes the user will want to go and mark a few commits as skipped
|
||||
// in a row and they wouldn't want to be jumped back to the current bisect
|
||||
// commit each time.
|
||||
// Originally we were allowing the user to, from the bisect menu, select whether
|
||||
// they were talking about the selected commit or the current bisect commit,
|
||||
// and that was a bit confusing (and required extra keypresses).
|
||||
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
|
||||
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
|
||||
// ref, because we'll be reloading our commits in that case.
|
||||
waitToReselect := selectCurrentAfter && !gui.Git.Bisect.ReachableFromStart(info)
|
||||
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.BisectMark)
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.afterMark(selectCurrentAfter, waitToReselect)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.BisectMark)
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.afterMark(selectCurrentAfter, waitToReselect)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.Skip, commit.ShortSha()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.BisectSkip)
|
||||
if err := gui.Git.Bisect.Skip(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.afterMark(selectCurrentAfter, waitToReselect)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.Bisect.ResetOption,
|
||||
onPress: func() error {
|
||||
return gui.resetBisect()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(
|
||||
gui.Tr.Bisect.BisectMenuTitle,
|
||||
menuItems,
|
||||
createMenuOptions{showCancel: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
||||
return gui.createMenu(
|
||||
gui.Tr.Bisect.BisectMenuTitle,
|
||||
[]*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.StartBisect)
|
||||
if err := gui.Git.Bisect.Start(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.StartBisect)
|
||||
if err := gui.Git.Bisect.Start(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
},
|
||||
},
|
||||
createMenuOptions{showCancel: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) resetBisect() error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.Bisect.ResetTitle,
|
||||
prompt: gui.Tr.Bisect.ResetPrompt,
|
||||
handleConfirm: func() error {
|
||||
gui.logAction(gui.Tr.Actions.ResetBisect)
|
||||
if err := gui.Git.Bisect.Reset(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) showBisectCompleteMessage(candidateShas []string) error {
|
||||
prompt := gui.Tr.Bisect.CompletePrompt
|
||||
if len(candidateShas) > 1 {
|
||||
prompt = gui.Tr.Bisect.CompletePromptIndeterminate
|
||||
}
|
||||
|
||||
formattedCommits, err := gui.Git.Commit.GetCommitsOneline(candidateShas)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.Bisect.CompleteTitle,
|
||||
prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
|
||||
handleConfirm: func() error {
|
||||
gui.logAction(gui.Tr.Actions.ResetBisect)
|
||||
if err := gui.Git.Bisect.Reset(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) afterMark(selectCurrent bool, waitToReselect bool) error {
|
||||
done, candidateShas, err := gui.Git.Bisect.IsDone()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if done {
|
||||
return gui.showBisectCompleteMessage(candidateShas)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) postBisectCommandRefresh() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{}})
|
||||
}
|
||||
|
||||
func (gui *Gui) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
|
||||
selectFn := func() {
|
||||
if selectCurrent {
|
||||
gui.selectCurrentBisectCommit()
|
||||
}
|
||||
}
|
||||
|
||||
if waitToReselect {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{}, then: selectFn})
|
||||
} else {
|
||||
selectFn()
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) selectCurrentBisectCommit() {
|
||||
info := gui.Git.Bisect.GetInfo()
|
||||
if info.GetCurrentSha() != "" {
|
||||
// find index of commit with that sha, move cursor to that.
|
||||
for i, commit := range gui.State.Commits {
|
||||
if commit.Sha == info.GetCurrentSha() {
|
||||
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(i)
|
||||
_ = gui.State.Contexts.BranchCommits.HandleFocus()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package boxlayout
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Dimensions struct {
|
||||
X0 int
|
||||
|
|
@ -69,45 +73,12 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
|
|||
availableSize = height
|
||||
}
|
||||
|
||||
// work out size taken up by children
|
||||
reservedSize := 0
|
||||
totalWeight := 0
|
||||
for _, child := range children {
|
||||
// assuming either size or weight are non-zero
|
||||
reservedSize += child.Size
|
||||
totalWeight += child.Weight
|
||||
}
|
||||
|
||||
remainingSize := availableSize - reservedSize
|
||||
if remainingSize < 0 {
|
||||
remainingSize = 0
|
||||
}
|
||||
|
||||
unitSize := 0
|
||||
extraSize := 0
|
||||
if totalWeight > 0 {
|
||||
unitSize = remainingSize / totalWeight
|
||||
extraSize = remainingSize % totalWeight
|
||||
}
|
||||
sizes := calcSizes(children, availableSize)
|
||||
|
||||
result := map[string]Dimensions{}
|
||||
offset := 0
|
||||
for _, child := range children {
|
||||
var boxSize int
|
||||
if child.isStatic() {
|
||||
boxSize = child.Size
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
if boxSize > availableSize {
|
||||
boxSize = availableSize
|
||||
}
|
||||
} else {
|
||||
// TODO: consider more evenly distributing the remainder
|
||||
boxSize = unitSize * child.Weight
|
||||
boxExtraSize := int(math.Min(float64(extraSize), float64(child.Weight)))
|
||||
boxSize += boxExtraSize
|
||||
extraSize -= boxExtraSize
|
||||
}
|
||||
for i, child := range children {
|
||||
boxSize := sizes[i]
|
||||
|
||||
var resultForChild map[string]Dimensions
|
||||
if direction == COLUMN {
|
||||
|
|
@ -123,6 +94,95 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
|
|||
return result
|
||||
}
|
||||
|
||||
func calcSizes(boxes []*Box, availableSpace int) []int {
|
||||
normalizedWeights := normalizeWeights(slices.Map(boxes, func(box *Box) int { return box.Weight }))
|
||||
|
||||
totalWeight := 0
|
||||
reservedSpace := 0
|
||||
for i, box := range boxes {
|
||||
if box.isStatic() {
|
||||
reservedSpace += box.Size
|
||||
} else {
|
||||
totalWeight += normalizedWeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
dynamicSpace := utils.Max(0, availableSpace-reservedSpace)
|
||||
|
||||
unitSize := 0
|
||||
extraSpace := 0
|
||||
if totalWeight > 0 {
|
||||
unitSize = dynamicSpace / totalWeight
|
||||
extraSpace = dynamicSpace % totalWeight
|
||||
}
|
||||
|
||||
result := make([]int, len(boxes))
|
||||
for i, box := range boxes {
|
||||
if box.isStatic() {
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
result[i] = utils.Min(availableSpace, box.Size)
|
||||
} else {
|
||||
result[i] = unitSize * normalizedWeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
// distribute the remainder across dynamic boxes.
|
||||
for extraSpace > 0 {
|
||||
for i, weight := range normalizedWeights {
|
||||
if weight > 0 {
|
||||
result[i]++
|
||||
extraSpace--
|
||||
normalizedWeights[i]--
|
||||
|
||||
if extraSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2.
|
||||
func normalizeWeights(weights []int) []int {
|
||||
if len(weights) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// to spare us some computation we'll exit early if any of our weights is 1
|
||||
if slices.Some(weights, func(weight int) bool { return weight == 1 }) {
|
||||
return weights
|
||||
}
|
||||
|
||||
// map weights to factorSlices and find the lowest common factor
|
||||
positiveWeights := slices.Filter(weights, func(weight int) bool { return weight > 0 })
|
||||
factorSlices := slices.Map(positiveWeights, func(weight int) []int { return calcFactors(weight) })
|
||||
commonFactors := factorSlices[0]
|
||||
for _, factors := range factorSlices {
|
||||
commonFactors = lo.Intersect(commonFactors, factors)
|
||||
}
|
||||
|
||||
if len(commonFactors) == 0 {
|
||||
return weights
|
||||
}
|
||||
|
||||
newWeights := slices.Map(weights, func(weight int) int { return weight / commonFactors[0] })
|
||||
|
||||
return normalizeWeights(newWeights)
|
||||
}
|
||||
|
||||
func calcFactors(n int) []int {
|
||||
factors := []int{}
|
||||
for i := 2; i <= n; i++ {
|
||||
if n%i == 0 {
|
||||
factors = append(factors, i)
|
||||
}
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
func (b *Box) isStatic() bool {
|
||||
return b.Size > 0
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue